Restore old annotations
This commit is contained in:
parent
5dca761978
commit
1e234ea177
20 changed files with 506 additions and 1289 deletions
|
@ -2,10 +2,12 @@
|
||||||
<module type="WEB_MODULE" version="4">
|
<module type="WEB_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$/angel_orm">
|
<content url="file://$MODULE_DIR$/angel_orm">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/angel_orm/.dart_tool" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm/.pub" />
|
<excludeFolder url="file://$MODULE_DIR$/angel_orm/.pub" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm/build" />
|
<excludeFolder url="file://$MODULE_DIR$/angel_orm/build" />
|
||||||
</content>
|
</content>
|
||||||
<content url="file://$MODULE_DIR$/angel_orm_generator">
|
<content url="file://$MODULE_DIR$/angel_orm_generator">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/.dart_tool" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/.pub" />
|
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/.pub" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/build" />
|
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/build" />
|
||||||
</content>
|
</content>
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
analyzer:
|
|
||||||
strong-mode: true
|
|
2
angel_orm/.gitignore
vendored
2
angel_orm/.gitignore
vendored
|
@ -54,3 +54,5 @@ com_crashlytics_export_strings.xml
|
||||||
crashlytics.properties
|
crashlytics.properties
|
||||||
crashlytics-build.properties
|
crashlytics-build.properties
|
||||||
fabric.properties
|
fabric.properties
|
||||||
|
|
||||||
|
.dart_tool
|
|
@ -1,3 +1,8 @@
|
||||||
|
# 2.0.0-dev
|
||||||
|
* Restored all old PostgreSQL-specific annotations. Rather than a smart runtime,
|
||||||
|
having a codegen capable of building ORM's for multiple databases can potentially
|
||||||
|
provide a very fast ORM for everyone.
|
||||||
|
|
||||||
# 1.0.0-alpha+11
|
# 1.0.0-alpha+11
|
||||||
* Removed PostgreSQL-specific functionality, so that the ORM can ultimately
|
* Removed PostgreSQL-specific functionality, so that the ORM can ultimately
|
||||||
target all services.
|
target all services.
|
||||||
|
|
3
angel_orm/analysis_options.yaml
Normal file
3
angel_orm/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
|
@ -1,27 +1,27 @@
|
||||||
import 'package:angel_model/angel_model.dart';
|
import 'package:angel_model/angel_model.dart';
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
import 'package:angel_orm/angel_orm.dart';
|
||||||
|
|
||||||
Query findEmployees(Company company) {
|
main() {
|
||||||
return new Query()
|
|
||||||
..['company_id'] = equals(company.id)
|
|
||||||
..['first_name'] = notNull() & (equals('John'))
|
|
||||||
..['salary'] = greaterThanOrEqual(100000.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ORM('api/companies')
|
|
||||||
class Company extends Model {
|
|
||||||
String name;
|
|
||||||
bool isFortune500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@orm
|
@orm
|
||||||
class Employee extends Model {
|
abstract class Company extends Model {
|
||||||
|
String get name;
|
||||||
|
|
||||||
|
bool get isFortune500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@orm
|
||||||
|
abstract class _Employee extends Model {
|
||||||
@belongsTo
|
@belongsTo
|
||||||
Company company;
|
Company get company;
|
||||||
|
|
||||||
String firstName, lastName;
|
String get firstName;
|
||||||
|
|
||||||
double salary;
|
String get lastName;
|
||||||
|
|
||||||
|
double get salary;
|
||||||
|
|
||||||
bool get isFortune500Employee => company.isFortune500;
|
bool get isFortune500Employee => company.isFortune500;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export 'src/annotations.dart';
|
export 'src/annotations.dart';
|
||||||
export 'src/query.dart';
|
export 'src/migration.dart';
|
||||||
export 'src/relations.dart';
|
export 'src/relations.dart';
|
||||||
|
export 'src/query.dart';
|
|
@ -1,30 +1,17 @@
|
||||||
const ORM orm = const ORM();
|
const ORM orm = const ORM();
|
||||||
|
|
||||||
class ORM {
|
class ORM {
|
||||||
/// The path to an Angel service that queries objects of the
|
final String tableName;
|
||||||
/// annotated type at runtime.
|
|
||||||
///
|
const ORM([this.tableName]);
|
||||||
/// Ex. `api/users`, etc.
|
|
||||||
final String servicePath;
|
|
||||||
const ORM([this.servicePath]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specifies that the ORM should build a join builder
|
class CanJoin {
|
||||||
/// that combines the results of queries on two services.
|
|
||||||
class Join {
|
|
||||||
/// The [Model] type to join against.
|
|
||||||
final Type type;
|
final Type type;
|
||||||
|
final String foreignKey;
|
||||||
/// The path to an Angel service that queries objects of the
|
|
||||||
/// [type] being joined against, at runtime.
|
|
||||||
///
|
|
||||||
/// Ex. `api/users`, etc.
|
|
||||||
final String servicePath;
|
|
||||||
|
|
||||||
/// The type of join this is.
|
|
||||||
final JoinType joinType;
|
final JoinType joinType;
|
||||||
|
|
||||||
const Join(this.type, this.servicePath, [this.joinType = JoinType.join]);
|
const CanJoin(this.type, this.foreignKey, {this.joinType: JoinType.full});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The various types of [Join].
|
/// The various types of [Join].
|
||||||
|
|
124
angel_orm/lib/src/migration.dart
Normal file
124
angel_orm/lib/src/migration.dart
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
const List<String> SQL_RESERVED_WORDS = const [
|
||||||
|
'SELECT',
|
||||||
|
'UPDATE',
|
||||||
|
'INSERT',
|
||||||
|
'DELETE',
|
||||||
|
'FROM',
|
||||||
|
'ASC',
|
||||||
|
'DESC',
|
||||||
|
'VALUES',
|
||||||
|
'RETURNING',
|
||||||
|
'ORDER',
|
||||||
|
'BY',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Applies additional attributes to a database column.
|
||||||
|
class Column {
|
||||||
|
/// If `true`, a SQL field will be nullable.
|
||||||
|
final bool isNullable;
|
||||||
|
|
||||||
|
/// Specifies the length of a `VARCHAR`.
|
||||||
|
final int length;
|
||||||
|
|
||||||
|
/// Explicitly defines a SQL type for this column.
|
||||||
|
final ColumnType type;
|
||||||
|
|
||||||
|
/// Specifies what kind of index this column is, if any.
|
||||||
|
final IndexType indexType;
|
||||||
|
|
||||||
|
/// The default value of this field.
|
||||||
|
final defaultValue;
|
||||||
|
|
||||||
|
const Column(
|
||||||
|
{this.isNullable: true,
|
||||||
|
this.length,
|
||||||
|
this.type,
|
||||||
|
this.indexType: IndexType.none,
|
||||||
|
this.defaultValue});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrimaryKey extends Column {
|
||||||
|
const PrimaryKey({ColumnType columnType})
|
||||||
|
: super(
|
||||||
|
type: columnType ?? ColumnType.serial, indexType: IndexType.primaryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Column primaryKey = const PrimaryKey();
|
||||||
|
|
||||||
|
/// Maps to SQL index types.
|
||||||
|
enum IndexType {
|
||||||
|
none,
|
||||||
|
|
||||||
|
/// Standard index.
|
||||||
|
standardIndex,
|
||||||
|
|
||||||
|
/// A primary key.
|
||||||
|
primaryKey,
|
||||||
|
|
||||||
|
/// A *unique* index.
|
||||||
|
unique
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps to SQL data types.
|
||||||
|
///
|
||||||
|
/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm
|
||||||
|
class ColumnType {
|
||||||
|
/// The name of this data type.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const ColumnType(this.name);
|
||||||
|
|
||||||
|
static const ColumnType boolean = const ColumnType('boolean');
|
||||||
|
|
||||||
|
static const ColumnType smallSerial = const ColumnType('smallserial');
|
||||||
|
static const ColumnType serial = const ColumnType('serial');
|
||||||
|
static const ColumnType bigSerial = const ColumnType('bigserial');
|
||||||
|
|
||||||
|
// Numbers
|
||||||
|
static const ColumnType bigInt = const ColumnType('bigint');
|
||||||
|
static const ColumnType int = const ColumnType('int');
|
||||||
|
static const ColumnType smallInt = const ColumnType('smallint');
|
||||||
|
static const ColumnType tinyInt = const ColumnType('tinyint');
|
||||||
|
static const ColumnType bit = const ColumnType('bit');
|
||||||
|
static const ColumnType decimal = const ColumnType('decimal');
|
||||||
|
static const ColumnType numeric = const ColumnType('numeric');
|
||||||
|
static const ColumnType money = const ColumnType('money');
|
||||||
|
static const ColumnType smallMoney = const ColumnType('smallmoney');
|
||||||
|
static const ColumnType float = const ColumnType('float');
|
||||||
|
static const ColumnType real = const ColumnType('real');
|
||||||
|
|
||||||
|
// Dates and times
|
||||||
|
static const ColumnType dateTime = const ColumnType('datetime');
|
||||||
|
static const ColumnType smallDateTime = const ColumnType('smalldatetime');
|
||||||
|
static const ColumnType date = const ColumnType('date');
|
||||||
|
static const ColumnType time = const ColumnType('time');
|
||||||
|
static const ColumnType timeStamp = const ColumnType('timestamp');
|
||||||
|
static const ColumnType timeStampWithTimeZone =
|
||||||
|
const ColumnType('timestamp with time zone');
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
static const ColumnType char = const ColumnType('char');
|
||||||
|
static const ColumnType varChar = const ColumnType('varchar');
|
||||||
|
static const ColumnType varCharMax = const ColumnType('varchar(max)');
|
||||||
|
static const ColumnType text = const ColumnType('text');
|
||||||
|
|
||||||
|
// Unicode strings
|
||||||
|
static const ColumnType nChar = const ColumnType('nchar');
|
||||||
|
static const ColumnType nVarChar = const ColumnType('nvarchar');
|
||||||
|
static const ColumnType nVarCharMax = const ColumnType('nvarchar(max)');
|
||||||
|
static const ColumnType nText = const ColumnType('ntext');
|
||||||
|
|
||||||
|
// Binary
|
||||||
|
static const ColumnType binary = const ColumnType('binary');
|
||||||
|
static const ColumnType varBinary = const ColumnType('varbinary');
|
||||||
|
static const ColumnType varBinaryMax = const ColumnType('varbinary(max)');
|
||||||
|
static const ColumnType image = const ColumnType('image');
|
||||||
|
|
||||||
|
// Misc.
|
||||||
|
static const ColumnType sqlVariant = const ColumnType('sql_variant');
|
||||||
|
static const ColumnType uniqueIdentifier =
|
||||||
|
const ColumnType('uniqueidentifier');
|
||||||
|
static const ColumnType xml = const ColumnType('xml');
|
||||||
|
static const ColumnType cursor = const ColumnType('cursor');
|
||||||
|
static const ColumnType table = const ColumnType('table');
|
||||||
|
}
|
|
@ -1,114 +1,340 @@
|
||||||
/// Expects a field to be equal to a given [value].
|
import 'package:intl/intl.dart';
|
||||||
Predicate<T> equals<T>(T value) =>
|
import 'package:string_scanner/string_scanner.dart';
|
||||||
new Predicate<T>._(PredicateType.equals, value);
|
|
||||||
|
|
||||||
/// Expects at least one of the given [predicates] to be true.
|
final DateFormat dateYmd = new DateFormat('yyyy-MM-dd');
|
||||||
Predicate<T> anyOf<T>(Iterable<Predicate<T>> predicates) =>
|
final DateFormat dateYmdHms = new DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
new MultiPredicate<T>._(PredicateType.any, predicates);
|
|
||||||
|
|
||||||
/// Expects a field to be contained within a set of [values].
|
/// Cleans an input SQL expression of common SQL injection points.
|
||||||
Predicate<T> isIn<T>(Iterable<T> values) => new Predicate<T>._(PredicateType.isIn, null, values);
|
String sanitizeExpression(String unsafe) {
|
||||||
|
var buf = new StringBuffer();
|
||||||
|
var scanner = new StringScanner(unsafe);
|
||||||
|
int ch;
|
||||||
|
|
||||||
/// Expects a field to be `null`.
|
while (!scanner.isDone) {
|
||||||
Predicate<T> isNull<T>() => equals(null);
|
// Ignore comment starts
|
||||||
|
if (scanner.scan('--') || scanner.scan('/*'))
|
||||||
|
continue;
|
||||||
|
|
||||||
/// Expects a given [predicate] to not be true.
|
// Ignore all single quotes and attempted escape sequences
|
||||||
Predicate<T> not<T>(Predicate<T> predicate) =>
|
else if (scanner.scan("'") || scanner.scan('\\'))
|
||||||
new MultiPredicate<T>._(PredicateType.negate, [predicate]);
|
continue;
|
||||||
|
|
||||||
/// Expects a field to be not be `null`.
|
// Otherwise, add the next char, unless it's a null byte.
|
||||||
Predicate<T> notNull<T>() => not(isNull());
|
else if ((ch = scanner.readChar()) != 0 && ch != null)
|
||||||
|
buf.writeCharCode(ch);
|
||||||
/// Expects a field to be less than a given [value].
|
|
||||||
Predicate<T> lessThan<T>(T value) =>
|
|
||||||
new Predicate<T>._(PredicateType.less, value);
|
|
||||||
|
|
||||||
/// Expects a field to be less than or equal to a given [value].
|
|
||||||
Predicate<T> lessThanOrEqual<T>(T value) => lessThan(value) | equals(value);
|
|
||||||
|
|
||||||
/// Expects a field to be greater than a given [value].
|
|
||||||
Predicate<T> greaterThan<T>(T value) =>
|
|
||||||
new Predicate<T>._(PredicateType.greater, value);
|
|
||||||
|
|
||||||
/// Expects a field to be greater than or equal to a given [value].
|
|
||||||
Predicate<T> greaterThanOrEqual<T>(T value) =>
|
|
||||||
greaterThan(value) | equals(value);
|
|
||||||
|
|
||||||
/// A generic query class.
|
|
||||||
///
|
|
||||||
/// Angel services can translate these into driver-specific queries.
|
|
||||||
/// This allows the Angel ORM to be flexible and support multiple platforms.
|
|
||||||
class Query {
|
|
||||||
final Map<String, Predicate> _fields = {};
|
|
||||||
final Map<String, SortType> _sort = {};
|
|
||||||
|
|
||||||
/// Each field in a query is actually a [Predicate], and therefore acts as a contract
|
|
||||||
/// with the underlying service.
|
|
||||||
Map<String, Predicate> get fields =>
|
|
||||||
new Map<String, Predicate>.unmodifiable(_fields);
|
|
||||||
|
|
||||||
/// The sorting order applied to this query.
|
|
||||||
Map<String, SortType> get sorting =>
|
|
||||||
new Map<String, SortType>.unmodifiable(_sort);
|
|
||||||
|
|
||||||
/// Sets the [Predicate] assigned to the given [key].
|
|
||||||
void operator []=(String key, Predicate value) => _fields[key] = value;
|
|
||||||
|
|
||||||
/// Gets the [Predicate] assigned to the given [key].
|
|
||||||
Predicate operator [](String key) => _fields[key];
|
|
||||||
|
|
||||||
/// Sort output by the given [key].
|
|
||||||
void sortBy(String key, [SortType type = SortType.descending]) =>
|
|
||||||
_sort[key] = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A mechanism used to express an expectation about some object ([target]).
|
|
||||||
class Predicate<T> {
|
|
||||||
/// The type of expectation we are declaring.
|
|
||||||
final PredicateType type;
|
|
||||||
|
|
||||||
/// The single argument of this target.
|
|
||||||
final T target;
|
|
||||||
final Iterable<T> args;
|
|
||||||
|
|
||||||
Predicate._(this.type, this.target, [this.args]);
|
|
||||||
|
|
||||||
Predicate<T> operator &(Predicate<T> other) => and(other);
|
|
||||||
|
|
||||||
Predicate<T> operator |(Predicate<T> other) => or(other);
|
|
||||||
|
|
||||||
Predicate<T> and(Predicate<T> other) {
|
|
||||||
return new MultiPredicate._(PredicateType.and, [this, other]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate<T> or(Predicate<T> other) {
|
return buf.toString();
|
||||||
return new MultiPredicate._(PredicateType.or, [this, other]);
|
}
|
||||||
|
|
||||||
|
abstract class SqlExpressionBuilder<T> {
|
||||||
|
bool get hasValue;
|
||||||
|
|
||||||
|
String compile();
|
||||||
|
|
||||||
|
void isBetween(T lower, T upper);
|
||||||
|
|
||||||
|
void isNotBetween(T lower, T upper);
|
||||||
|
|
||||||
|
void isIn(Iterable<T> values);
|
||||||
|
|
||||||
|
void isNotIn(Iterable<T> values);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumericSqlExpressionBuilder<T extends num>
|
||||||
|
implements SqlExpressionBuilder<T> {
|
||||||
|
bool _hasValue = false;
|
||||||
|
String _op = '=';
|
||||||
|
String _raw;
|
||||||
|
T _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue => _hasValue;
|
||||||
|
|
||||||
|
bool _change(String op, T value) {
|
||||||
|
_raw = null;
|
||||||
|
_op = op;
|
||||||
|
_value = value;
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile() {
|
||||||
|
if (_raw != null) return _raw;
|
||||||
|
if (_value == null) return null;
|
||||||
|
return '$_op $_value';
|
||||||
|
}
|
||||||
|
|
||||||
|
operator <(T value) => _change('<', value);
|
||||||
|
|
||||||
|
operator >(T value) => _change('>', value);
|
||||||
|
|
||||||
|
operator <=(T value) => _change('<=', value);
|
||||||
|
|
||||||
|
operator >=(T value) => _change('>=', value);
|
||||||
|
|
||||||
|
void lessThan(T value) {
|
||||||
|
_change('<', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lessThanOrEqualTo(T value) {
|
||||||
|
_change('<=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThan(T value) {
|
||||||
|
_change('>', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThanOrEqualTo(T value) {
|
||||||
|
_change('>=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void equals(T value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notEquals(T value) {
|
||||||
|
_change('!=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isBetween(T lower, T upper) {
|
||||||
|
_raw = 'BETWEEN $lower AND $upper';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotBetween(T lower, T upper) {
|
||||||
|
_raw = 'NOT BETWEEN $lower AND $upper';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isIn(Iterable<T> values) {
|
||||||
|
_raw = 'IN (' + values.join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotIn(Iterable<T> values) {
|
||||||
|
_raw = 'NOT IN (' + values.join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An advanced [Predicate] that performs an operation of multiple other predicates.
|
class StringSqlExpressionBuilder implements SqlExpressionBuilder<String> {
|
||||||
class MultiPredicate<T> extends Predicate<T> {
|
bool _hasValue = false;
|
||||||
final Iterable<Predicate<T>> targets;
|
String _op = '=', _raw, _value;
|
||||||
|
|
||||||
MultiPredicate._(PredicateType type, this.targets) : super._(type, null);
|
@override
|
||||||
|
bool get hasValue => _hasValue;
|
||||||
|
|
||||||
/// Use [targets] instead.
|
bool _change(String op, String value) {
|
||||||
@deprecated
|
_raw = null;
|
||||||
T get target => throw new UnsupportedError(
|
_op = op;
|
||||||
'IterablePredicate has no `target`. Use `targets` instead.');
|
_value = value;
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile() {
|
||||||
|
if (_raw != null) return _raw;
|
||||||
|
if (_value == null) return null;
|
||||||
|
var v = sanitizeExpression(_value);
|
||||||
|
return "$_op '$v'";
|
||||||
|
}
|
||||||
|
|
||||||
|
void isEmpty() => equals('');
|
||||||
|
|
||||||
|
void equals(String value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notEquals(String value) {
|
||||||
|
_change('!=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void like(String value) {
|
||||||
|
_change('LIKE', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isBetween(String lower, String upper) {
|
||||||
|
var l = sanitizeExpression(lower), u = sanitizeExpression(upper);
|
||||||
|
_raw = "BETWEEN '$l' AND '$u'";
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotBetween(String lower, String upper) {
|
||||||
|
var l = sanitizeExpression(lower), u = sanitizeExpression(upper);
|
||||||
|
_raw = "NOT BETWEEN '$l' AND '$u'";
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isIn(Iterable<String> values) {
|
||||||
|
_raw = 'IN (' +
|
||||||
|
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
|
||||||
|
')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotIn(Iterable<String> values) {
|
||||||
|
_raw = 'NOT IN (' +
|
||||||
|
values.map(sanitizeExpression).map((s) => "'$s'").join(', ') +
|
||||||
|
')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The various types of predicate.
|
class BooleanSqlExpressionBuilder implements SqlExpressionBuilder<bool> {
|
||||||
enum PredicateType {
|
bool _hasValue = false;
|
||||||
equals,
|
String _op = '=', _raw;
|
||||||
any,
|
bool _value;
|
||||||
isIn,
|
|
||||||
negate,
|
@override
|
||||||
and,
|
bool get hasValue => _hasValue;
|
||||||
or,
|
|
||||||
less,
|
bool _change(String op, bool value) {
|
||||||
greater,
|
_raw = null;
|
||||||
|
_op = op;
|
||||||
|
_value = value;
|
||||||
|
return _hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile() {
|
||||||
|
if (_raw != null) return _raw;
|
||||||
|
if (_value == null) return null;
|
||||||
|
var v = _value ? 'TRUE' : 'FALSE';
|
||||||
|
return '$_op $v';
|
||||||
|
}
|
||||||
|
|
||||||
|
void equals(bool value) {
|
||||||
|
_change('=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notEquals(bool value) {
|
||||||
|
_change('!=', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isBetween(bool lower, bool upper) => throw new UnsupportedError(
|
||||||
|
'Booleans do not support BETWEEN expressions.');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotBetween(bool lower, bool upper) => isBetween(lower, upper);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isIn(Iterable<bool> values) {
|
||||||
|
_raw = 'IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotIn(Iterable<bool> values) {
|
||||||
|
_raw =
|
||||||
|
'NOT IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The various modes of sorting.
|
class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder<DateTime> {
|
||||||
enum SortType { ascending, descending }
|
final NumericSqlExpressionBuilder<int> year =
|
||||||
|
new NumericSqlExpressionBuilder<int>(),
|
||||||
|
month = new NumericSqlExpressionBuilder<int>(),
|
||||||
|
day = new NumericSqlExpressionBuilder<int>(),
|
||||||
|
hour = new NumericSqlExpressionBuilder<int>(),
|
||||||
|
minute = new NumericSqlExpressionBuilder<int>(),
|
||||||
|
second = new NumericSqlExpressionBuilder<int>();
|
||||||
|
final String columnName;
|
||||||
|
String _raw;
|
||||||
|
|
||||||
|
DateTimeSqlExpressionBuilder(this.columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasValue =>
|
||||||
|
_raw?.isNotEmpty == true ||
|
||||||
|
year.hasValue ||
|
||||||
|
month.hasValue ||
|
||||||
|
day.hasValue ||
|
||||||
|
hour.hasValue ||
|
||||||
|
minute.hasValue ||
|
||||||
|
second.hasValue;
|
||||||
|
|
||||||
|
bool _change(String _op, DateTime dt, bool time) {
|
||||||
|
var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt);
|
||||||
|
_raw = '$columnName $_op \'$dateString\'';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
operator <(DateTime value) => _change('<', value, true);
|
||||||
|
|
||||||
|
operator <=(DateTime value) => _change('<=', value, true);
|
||||||
|
|
||||||
|
operator >(DateTime value) => _change('>', value, true);
|
||||||
|
|
||||||
|
operator >=(DateTime value) => _change('>=', value, true);
|
||||||
|
|
||||||
|
void equals(DateTime value, {bool includeTime: true}) {
|
||||||
|
_change('=', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lessThan(DateTime value, {bool includeTime: true}) {
|
||||||
|
_change('<', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lessThanOrEqualTo(DateTime value, {bool includeTime: true}) {
|
||||||
|
_change('<=', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThan(DateTime value, {bool includeTime: true}) {
|
||||||
|
_change('>', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void greaterThanOrEqualTo(DateTime value, {bool includeTime: true}) {
|
||||||
|
_change('>=', value, includeTime != false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isIn(Iterable<DateTime> values) {
|
||||||
|
_raw = '$columnName IN (' +
|
||||||
|
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotIn(Iterable<DateTime> values) {
|
||||||
|
_raw = '$columnName NOT IN (' +
|
||||||
|
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isBetween(DateTime lower, DateTime upper) {
|
||||||
|
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
|
||||||
|
_raw = "$columnName BETWEEN '$l' and '$u'";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void isNotBetween(DateTime lower, DateTime upper) {
|
||||||
|
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
|
||||||
|
_raw = "$columnName NOT BETWEEN '$l' and '$u'";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile() {
|
||||||
|
if (_raw?.isNotEmpty == true) return _raw;
|
||||||
|
List<String> parts = [];
|
||||||
|
if (year.hasValue) parts.add('YEAR($columnName) ${year.compile()}');
|
||||||
|
if (month.hasValue) parts.add('MONTH($columnName) ${month.compile()}');
|
||||||
|
if (day.hasValue) parts.add('DAY($columnName) ${day.compile()}');
|
||||||
|
if (hour.hasValue) parts.add('HOUR($columnName) ${hour.compile()}');
|
||||||
|
if (minute.hasValue) parts.add('MINUTE($columnName) ${minute.compile()}');
|
||||||
|
if (second.hasValue) parts.add('SECOND($columnName) ${second.compile()}');
|
||||||
|
|
||||||
|
return parts.isEmpty ? null : parts.join(' AND ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
abstract class RelationshipType {
|
abstract class RelationshipType {
|
||||||
static const int HAS_MANY = 0;
|
static const int hasMany = 0;
|
||||||
static const int HAS_ONE = 1;
|
static const int hasOne = 1;
|
||||||
static const int BELONGS_TO = 2;
|
static const int belongsTo = 2;
|
||||||
static const int BELONGS_TO_MANY = 3;
|
static const int belongsToMany = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Relationship {
|
class Relationship {
|
||||||
|
@ -25,7 +25,7 @@ class HasMany extends Relationship {
|
||||||
String foreignKey,
|
String foreignKey,
|
||||||
String foreignTable,
|
String foreignTable,
|
||||||
bool cascadeOnDelete: false})
|
bool cascadeOnDelete: false})
|
||||||
: super(RelationshipType.HAS_MANY,
|
: super(RelationshipType.hasMany,
|
||||||
localKey: localKey,
|
localKey: localKey,
|
||||||
foreignKey: foreignKey,
|
foreignKey: foreignKey,
|
||||||
foreignTable: foreignTable,
|
foreignTable: foreignTable,
|
||||||
|
@ -40,7 +40,7 @@ class HasOne extends Relationship {
|
||||||
String foreignKey,
|
String foreignKey,
|
||||||
String foreignTable,
|
String foreignTable,
|
||||||
bool cascadeOnDelete: false})
|
bool cascadeOnDelete: false})
|
||||||
: super(RelationshipType.HAS_ONE,
|
: super(RelationshipType.hasOne,
|
||||||
localKey: localKey,
|
localKey: localKey,
|
||||||
foreignKey: foreignKey,
|
foreignKey: foreignKey,
|
||||||
foreignTable: foreignTable,
|
foreignTable: foreignTable,
|
||||||
|
@ -52,7 +52,7 @@ const HasOne hasOne = const HasOne();
|
||||||
class BelongsTo extends Relationship {
|
class BelongsTo extends Relationship {
|
||||||
const BelongsTo(
|
const BelongsTo(
|
||||||
{String localKey: 'id', String foreignKey, String foreignTable})
|
{String localKey: 'id', String foreignKey, String foreignTable})
|
||||||
: super(RelationshipType.BELONGS_TO,
|
: super(RelationshipType.belongsTo,
|
||||||
localKey: localKey,
|
localKey: localKey,
|
||||||
foreignKey: foreignKey,
|
foreignKey: foreignKey,
|
||||||
foreignTable: foreignTable);
|
foreignTable: foreignTable);
|
||||||
|
@ -63,7 +63,7 @@ const BelongsTo belongsTo = const BelongsTo();
|
||||||
class BelongsToMany extends Relationship {
|
class BelongsToMany extends Relationship {
|
||||||
const BelongsToMany(
|
const BelongsToMany(
|
||||||
{String localKey: 'id', String foreignKey, String foreignTable})
|
{String localKey: 'id', String foreignKey, String foreignTable})
|
||||||
: super(RelationshipType.BELONGS_TO_MANY,
|
: super(RelationshipType.belongsToMany,
|
||||||
localKey: localKey,
|
localKey: localKey,
|
||||||
foreignKey: foreignKey,
|
foreignKey: foreignKey,
|
||||||
foreignTable: foreignTable);
|
foreignTable: foreignTable);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
name: angel_orm
|
name: angel_orm
|
||||||
version: 1.0.0-alpha+11
|
version: 2.0.0-dev
|
||||||
description: Runtime support for Angel's ORM.
|
description: Runtime support for Angel's ORM.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/orm
|
homepage: https://github.com/angel-dart/orm
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.0.0-dev.1.2 <2.0.0'
|
sdk: '>=2.0.0-dev.1.2 <3.0.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_model: ^1.0.0
|
intl: ^0.15.7
|
||||||
meta: ^1.0.0
|
meta: ^1.0.0
|
||||||
|
string_scanner: ^1.0.0
|
||||||
|
dev_dependencies:
|
||||||
|
angel_model: ^1.0.0
|
|
@ -1,4 +0,0 @@
|
||||||
export 'src/builder/orm/migration.dart';
|
|
||||||
export 'src/builder/orm/postgres.dart';
|
|
||||||
export 'src/builder/orm/service.dart';
|
|
||||||
export 'src/builder/orm/sql_migration.dart';
|
|
|
@ -1,207 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:analyzer/dart/constant/value.dart';
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
|
||||||
import 'package:analyzer/src/dart/element/element.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:angel_serialize_generator/build_context.dart' as serialize;
|
|
||||||
import 'package:angel_serialize_generator/context.dart' as serialize;
|
|
||||||
import 'package:build/build.dart';
|
|
||||||
import 'package:inflection/inflection.dart';
|
|
||||||
import 'package:recase/recase.dart';
|
|
||||||
import 'package:source_gen/source_gen.dart';
|
|
||||||
import 'postgres_build_context.dart';
|
|
||||||
|
|
||||||
const TypeChecker columnTypeChecker = const TypeChecker.fromRuntime(Column),
|
|
||||||
dateTimeTypeChecker = const TypeChecker.fromRuntime(DateTime),
|
|
||||||
ormTypeChecker = const TypeChecker.fromRuntime(ORM),
|
|
||||||
relationshipTypeChecker = const TypeChecker.fromRuntime(Relationship);
|
|
||||||
|
|
||||||
const TypeChecker hasOneTypeChecker = const TypeChecker.fromRuntime(HasOne),
|
|
||||||
hasManyTypeChecker = const TypeChecker.fromRuntime(HasMany),
|
|
||||||
belongsToTypeChecker = const TypeChecker.fromRuntime(BelongsTo),
|
|
||||||
belongsToManyTypeChecker = const TypeChecker.fromRuntime(BelongsToMany);
|
|
||||||
|
|
||||||
ColumnType inferColumnType(DartType type) {
|
|
||||||
if (const TypeChecker.fromRuntime(String).isAssignableFromType(type))
|
|
||||||
return ColumnType.VAR_CHAR;
|
|
||||||
if (const TypeChecker.fromRuntime(int).isAssignableFromType(type))
|
|
||||||
return ColumnType.INT;
|
|
||||||
if (const TypeChecker.fromRuntime(double).isAssignableFromType(type))
|
|
||||||
return ColumnType.DECIMAL;
|
|
||||||
if (const TypeChecker.fromRuntime(num).isAssignableFromType(type))
|
|
||||||
return ColumnType.NUMERIC;
|
|
||||||
if (const TypeChecker.fromRuntime(bool).isAssignableFromType(type))
|
|
||||||
return ColumnType.BOOLEAN;
|
|
||||||
if (const TypeChecker.fromRuntime(DateTime).isAssignableFromType(type))
|
|
||||||
return ColumnType.TIME_STAMP;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Column reviveColumn(ConstantReader cr) {
|
|
||||||
// TODO: Get index type, column type...
|
|
||||||
var args = cr.revive().namedArguments;
|
|
||||||
IndexType indexType = IndexType.NONE;
|
|
||||||
ColumnType columnType;
|
|
||||||
|
|
||||||
if (args.containsKey('index')) {
|
|
||||||
indexType = IndexType.values[args['index'].getField('index').toIntValue()];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.containsKey('type')) {
|
|
||||||
columnType = new _ColumnType(args['type'].getField('name').toStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Column(
|
|
||||||
nullable: cr.peek('nullable')?.boolValue,
|
|
||||||
length: cr.peek('length')?.intValue,
|
|
||||||
defaultValue: cr.peek('defaultValue')?.literalValue,
|
|
||||||
type: columnType,
|
|
||||||
index: indexType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ORM reviveOrm(ConstantReader cr) {
|
|
||||||
return new ORM(cr.peek('tableName')?.stringValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
Relationship reviveRelationship(DartObject relationshipAnnotation) {
|
|
||||||
var cr = new ConstantReader(relationshipAnnotation);
|
|
||||||
var r = cr.revive().namedArguments;
|
|
||||||
int type = -1;
|
|
||||||
|
|
||||||
if (cr.instanceOf(hasOneTypeChecker))
|
|
||||||
type = RelationshipType.HAS_ONE;
|
|
||||||
else if (cr.instanceOf(hasManyTypeChecker))
|
|
||||||
type = RelationshipType.HAS_MANY;
|
|
||||||
else if (cr.instanceOf(belongsToTypeChecker))
|
|
||||||
type = RelationshipType.BELONGS_TO;
|
|
||||||
else if (cr.instanceOf(belongsToManyTypeChecker))
|
|
||||||
type = RelationshipType.BELONGS_TO_MANY;
|
|
||||||
else
|
|
||||||
throw new UnsupportedError(
|
|
||||||
'Unsupported relationship type "${relationshipAnnotation.type.name}".');
|
|
||||||
|
|
||||||
return new Relationship(type,
|
|
||||||
localKey: r['localKey']?.toStringValue(),
|
|
||||||
foreignKey: r['foreignKey']?.toStringValue(),
|
|
||||||
foreignTable: r['foreignTable']?.toStringValue(),
|
|
||||||
cascadeOnDelete: r['cascadeOnDelete']?.toBoolValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<PostgresBuildContext> buildContext(
|
|
||||||
ClassElement clazz,
|
|
||||||
ORM annotation,
|
|
||||||
BuildStep buildStep,
|
|
||||||
Resolver resolver,
|
|
||||||
bool autoSnakeCaseNames,
|
|
||||||
bool autoIdAndDateFields) async {
|
|
||||||
var raw = await serialize.buildContext(clazz, null, buildStep, resolver,
|
|
||||||
autoSnakeCaseNames != false, autoIdAndDateFields != false);
|
|
||||||
var ctx = await PostgresBuildContext.create(
|
|
||||||
clazz, raw, annotation, resolver, buildStep,
|
|
||||||
tableName: (annotation.tableName?.isNotEmpty == true)
|
|
||||||
? annotation.tableName
|
|
||||||
: pluralize(new ReCase(clazz.name).snakeCase),
|
|
||||||
autoSnakeCaseNames: autoSnakeCaseNames != false,
|
|
||||||
autoIdAndDateFields: autoIdAndDateFields != false);
|
|
||||||
List<String> fieldNames = [];
|
|
||||||
List<FieldElement> fields = [];
|
|
||||||
|
|
||||||
for (var field in raw.fields) {
|
|
||||||
fieldNames.add(field.name);
|
|
||||||
|
|
||||||
// Check for joins.
|
|
||||||
var canJoins = canJoinTypeChecker.annotationsOf(field);
|
|
||||||
|
|
||||||
for (var ann in canJoins) {
|
|
||||||
var cr = new ConstantReader(ann);
|
|
||||||
ctx.joins[field.name] ??= [];
|
|
||||||
ctx.joins[field.name].add(new JoinContext(
|
|
||||||
resolveModelAncestor(cr.read('type').typeValue),
|
|
||||||
cr.read('foreignKey').stringValue,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for relationship. If so, skip.
|
|
||||||
var relationshipAnnotation =
|
|
||||||
relationshipTypeChecker.firstAnnotationOf(field);
|
|
||||||
|
|
||||||
if (relationshipAnnotation != null) {
|
|
||||||
ctx.relationshipFields.add(field);
|
|
||||||
ctx.relationships[field.name] =
|
|
||||||
reviveRelationship(relationshipAnnotation);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for column annotation...
|
|
||||||
Column column;
|
|
||||||
var columnAnnotation = columnTypeChecker.firstAnnotationOf(field);
|
|
||||||
|
|
||||||
if (columnAnnotation != null) {
|
|
||||||
column = reviveColumn(new ConstantReader(columnAnnotation));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column == null && field.name == 'id' && ctx.shimmed['id'] == true) {
|
|
||||||
column = const Column(type: ColumnType.SERIAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column == null) {
|
|
||||||
// Guess what kind of column this is...
|
|
||||||
column = new Column(
|
|
||||||
type: inferColumnType(
|
|
||||||
field.type,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column != null && column.type == null) {
|
|
||||||
column = new Column(
|
|
||||||
nullable: column.nullable,
|
|
||||||
length: column.length,
|
|
||||||
index: column.index,
|
|
||||||
defaultValue: column.defaultValue,
|
|
||||||
type: inferColumnType(field.type),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column?.type == null)
|
|
||||||
throw 'Cannot infer SQL column type for field "${field.name}" with type "${field.type.name}".';
|
|
||||||
ctx.columnInfo[field.name] = column;
|
|
||||||
fields.add(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fields.addAll(fields);
|
|
||||||
|
|
||||||
// Add belongs to fields
|
|
||||||
// TODO: Do this for belongs to many as well
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
var rc = new ReCase(relationship.localKey);
|
|
||||||
|
|
||||||
if (relationship.type == RelationshipType.BELONGS_TO) {
|
|
||||||
ctx.fields.removeWhere((f) => f.name == rc.camelCase);
|
|
||||||
var field = new RelationshipConstraintField(
|
|
||||||
rc.camelCase, ctx.typeProvider.intType, name);
|
|
||||||
ctx.fields.add(field);
|
|
||||||
ctx.aliases[field.name] = relationship.localKey;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RelationshipConstraintField extends FieldElementImpl {
|
|
||||||
@override
|
|
||||||
final DartType type;
|
|
||||||
final String originalName;
|
|
||||||
RelationshipConstraintField(String name, this.type, this.originalName)
|
|
||||||
: super(name, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ColumnType implements ColumnType {
|
|
||||||
@override
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
_ColumnType(this.name);
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:build/build.dart';
|
|
||||||
import 'package:code_builder/code_builder.dart';
|
|
||||||
import 'package:source_gen/source_gen.dart' hide LibraryBuilder;
|
|
||||||
import 'build_context.dart';
|
|
||||||
import 'postgres_build_context.dart';
|
|
||||||
import 'lib_core.dart' as lib$core;
|
|
||||||
|
|
||||||
class MigrationGenerator extends GeneratorForAnnotation<ORM> {
|
|
||||||
static final Parameter _schemaParam = new Parameter((b) {
|
|
||||||
b
|
|
||||||
..name = 'schema'
|
|
||||||
..type = new TypeReference((b) => b.symbol = 'Schema');
|
|
||||||
});
|
|
||||||
static final Expression _schema = new CodeExpression(new Code('schema'));
|
|
||||||
|
|
||||||
/// If `true` (default), then field names will automatically be (de)serialized as snake_case.
|
|
||||||
final bool autoSnakeCaseNames;
|
|
||||||
|
|
||||||
/// If `true` (default), then the schema will automatically add id, created_at and updated_at fields.
|
|
||||||
final bool autoIdAndDateFields;
|
|
||||||
|
|
||||||
const MigrationGenerator(
|
|
||||||
{this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> generateForAnnotatedElement(
|
|
||||||
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
|
||||||
if (buildStep.inputId.path.contains('.migration.g.dart')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element is! ClassElement)
|
|
||||||
throw 'Only classes can be annotated with @ORM().';
|
|
||||||
var resolver = await buildStep.resolver;
|
|
||||||
var ctx = await buildContext(element, reviveOrm(annotation), buildStep,
|
|
||||||
resolver, autoSnakeCaseNames != false, autoIdAndDateFields != false);
|
|
||||||
var lib = generateMigrationLibrary(ctx, element, resolver, buildStep);
|
|
||||||
if (lib == null) return null;
|
|
||||||
var emitter = new DartEmitter();
|
|
||||||
return lib.accept(emitter).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Library generateMigrationLibrary(PostgresBuildContext ctx,
|
|
||||||
ClassElement element, Resolver resolver, BuildStep buildStep) {
|
|
||||||
return new Library((lib) {
|
|
||||||
lib.directives.add([
|
|
||||||
new Directive.import('package:angel_migration/angel_migration.dart'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
lib.body.add(new Class((b) {
|
|
||||||
b.name = '${ctx.modelClassName}Migration';
|
|
||||||
b.extend = new Reference('Migration');
|
|
||||||
}));
|
|
||||||
|
|
||||||
lib.methods.add(buildUpMigration(ctx, lib));
|
|
||||||
lib.methods.add(buildDownMigration(ctx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Method buildUpMigration(PostgresBuildContext ctx, LibraryBuilder lib) {
|
|
||||||
return new Method((meth) {
|
|
||||||
meth.name = 'up';
|
|
||||||
meth.annotations.add(lib$core.override);
|
|
||||||
meth.requiredParameters.add(_schemaParam);
|
|
||||||
|
|
||||||
var closure = new Method((closure) {
|
|
||||||
closure.requiredParameters.add(new Parameter((b) => b.name = 'table'));
|
|
||||||
var table = new Reference('table');
|
|
||||||
|
|
||||||
List<String> dup = [];
|
|
||||||
bool hasOrmImport = false;
|
|
||||||
ctx.columnInfo.forEach((name, col) {
|
|
||||||
var key = ctx.resolveFieldName(name);
|
|
||||||
|
|
||||||
if (dup.contains(key))
|
|
||||||
return;
|
|
||||||
else {
|
|
||||||
if (key != 'id' || autoIdAndDateFields == false) {
|
|
||||||
// Check for relationships that might duplicate
|
|
||||||
for (var rName in ctx.relationships.keys) {
|
|
||||||
var relationship = ctx.populateRelationship(rName);
|
|
||||||
if (relationship.localKey == key) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dup.add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
String methodName;
|
|
||||||
List<Expression> positional = [literal(key)];
|
|
||||||
Map<String, Expression> named = {};
|
|
||||||
|
|
||||||
if (autoIdAndDateFields != false && name == 'id') methodName = 'serial';
|
|
||||||
|
|
||||||
if (methodName == null) {
|
|
||||||
switch (col.type) {
|
|
||||||
case ColumnType.VAR_CHAR:
|
|
||||||
methodName = 'varchar';
|
|
||||||
if (col.length != null) named['length'] = literal(col.length);
|
|
||||||
break;
|
|
||||||
case ColumnType.SERIAL:
|
|
||||||
methodName = 'serial';
|
|
||||||
break;
|
|
||||||
case ColumnType.INT:
|
|
||||||
methodName = 'integer';
|
|
||||||
break;
|
|
||||||
case ColumnType.FLOAT:
|
|
||||||
methodName = 'float';
|
|
||||||
break;
|
|
||||||
case ColumnType.NUMERIC:
|
|
||||||
methodName = 'numeric';
|
|
||||||
break;
|
|
||||||
case ColumnType.BOOLEAN:
|
|
||||||
methodName = 'boolean';
|
|
||||||
break;
|
|
||||||
case ColumnType.DATE:
|
|
||||||
methodName = 'date';
|
|
||||||
break;
|
|
||||||
case ColumnType.DATE_TIME:
|
|
||||||
methodName = 'dateTime';
|
|
||||||
break;
|
|
||||||
case ColumnType.TIME_STAMP:
|
|
||||||
methodName = 'timeStamp';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (!hasOrmImport) {
|
|
||||||
hasOrmImport = true;
|
|
||||||
lib.directives.add(new Directive.import('package:angel_orm/angel_orm.dart'));
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression provColumn;
|
|
||||||
|
|
||||||
if (col.length == null) {
|
|
||||||
methodName = 'declare';
|
|
||||||
provColumn = new CodeExpression(new Code("new ColumnType('${col.type.name}')"));
|
|
||||||
} else {
|
|
||||||
methodName = 'declareColumn';
|
|
||||||
provColumn = new CodeExpression(new Code("new Column({type: new Column('${col.type.name}'), length: ${col.length})"));
|
|
||||||
}
|
|
||||||
|
|
||||||
positional.add(provColumn);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = table.property(methodName).call(positional, named);
|
|
||||||
var cascade = <Expression Function(Expression)>[];
|
|
||||||
|
|
||||||
if (col.defaultValue != null) {
|
|
||||||
cascade
|
|
||||||
.add((e) => e.property('defaultsTo').call([literal(col.defaultValue)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (col.index == IndexType.PRIMARY_KEY ||
|
|
||||||
(autoIdAndDateFields != false && name == 'id'))
|
|
||||||
cascade.add((e) => e.property('primaryKey').call([]));
|
|
||||||
else if (col.index == IndexType.UNIQUE)
|
|
||||||
cascade.add((e) => e.property('unique').call([]));
|
|
||||||
|
|
||||||
if (col.nullable != true) cascade.add((e) => e.property('notNull').call([]));
|
|
||||||
|
|
||||||
field = cascade.isEmpty
|
|
||||||
? field
|
|
||||||
: field.cascade((e) => cascade.map((f) => f(e)).toList());
|
|
||||||
closure.addStatement(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
|
|
||||||
if (relationship.isBelongsTo) {
|
|
||||||
var key = relationship.localKey;
|
|
||||||
|
|
||||||
var field = table.property('integer').call([literal(key)]);
|
|
||||||
// .references('user', 'id').onDeleteCascade()
|
|
||||||
var ref = field.property('references').call([
|
|
||||||
literal(relationship.foreignTable),
|
|
||||||
literal(relationship.foreignKey),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (relationship.cascadeOnDelete != false && relationship.isSingular)
|
|
||||||
ref = ref.property('onDeleteCascade').call([]);
|
|
||||||
return closure.addStatement(ref);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
meth.addStatement(_schema.property('create').call([
|
|
||||||
literal(ctx.tableName),
|
|
||||||
closure,
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Method buildDownMigration(PostgresBuildContext ctx) {
|
|
||||||
return new Method((b) {
|
|
||||||
b.name = 'down';
|
|
||||||
b.requiredParameters.add(_schemaParam);
|
|
||||||
b.annotations.add(lib$core.override);
|
|
||||||
b.body.add(new Code("schema.drop('${ctx.tableName}')"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,318 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:analyzer/dart/constant/value.dart';
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
|
||||||
import 'package:analyzer/src/generated/resolver.dart';
|
|
||||||
import 'package:angel_model/angel_model.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:angel_serialize_generator/context.dart';
|
|
||||||
import 'package:build/build.dart';
|
|
||||||
import 'package:code_builder/code_builder.dart';
|
|
||||||
import 'package:inflection/inflection.dart';
|
|
||||||
import 'package:recase/recase.dart';
|
|
||||||
import 'package:source_gen/source_gen.dart';
|
|
||||||
import 'build_context.dart';
|
|
||||||
|
|
||||||
const TypeChecker canJoinTypeChecker = const TypeChecker.fromRuntime(CanJoin);
|
|
||||||
|
|
||||||
DartType resolveModelAncestor(DartType type) {
|
|
||||||
DartType refType = type;
|
|
||||||
|
|
||||||
while (refType != null) {
|
|
||||||
if (!const TypeChecker.fromRuntime(Model).isAssignableFromType(refType)) {
|
|
||||||
var parent = (refType.element as ClassElement).allSupertypes[0];
|
|
||||||
if (parent != refType)
|
|
||||||
refType = parent;
|
|
||||||
else
|
|
||||||
refType = null;
|
|
||||||
} else
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refType != null) return refType;
|
|
||||||
|
|
||||||
throw '${type.name} does not extend Model.';
|
|
||||||
}
|
|
||||||
|
|
||||||
class JoinContext {
|
|
||||||
final DartType type;
|
|
||||||
final String foreignKey;
|
|
||||||
JoinContext(this.type, this.foreignKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
class PostgresBuildContext extends BuildContext {
|
|
||||||
LibraryElement _libraryCache;
|
|
||||||
TypeProvider _typeProviderCache;
|
|
||||||
TypeBuilder _modelClassBuilder,
|
|
||||||
_queryClassBuilder,
|
|
||||||
_whereClassBuilder,
|
|
||||||
_postgresqlConnectionBuilder;
|
|
||||||
String _prefix;
|
|
||||||
final Map<String, Relationship> _populatedRelationships = {};
|
|
||||||
final Map<String, Column> columnInfo = {};
|
|
||||||
final Map<String, IndexType> indices = {};
|
|
||||||
final Map<String, List<JoinContext>> joins = {};
|
|
||||||
final Map<String, Relationship> relationships = {};
|
|
||||||
final bool autoSnakeCaseNames, autoIdAndDateFields;
|
|
||||||
final String tableName;
|
|
||||||
final ORM ormAnnotation;
|
|
||||||
final ClassElement element;
|
|
||||||
final BuildContext raw;
|
|
||||||
final Resolver resolver;
|
|
||||||
final BuildStep buildStep;
|
|
||||||
ReCase _reCase;
|
|
||||||
String primaryKeyName = 'id';
|
|
||||||
|
|
||||||
PostgresBuildContext._(
|
|
||||||
this.element, this.raw, this.ormAnnotation, this.resolver, this.buildStep,
|
|
||||||
{this.tableName, this.autoSnakeCaseNames, this.autoIdAndDateFields})
|
|
||||||
: super(raw.annotation,
|
|
||||||
originalClassName: raw.originalClassName,
|
|
||||||
sourceFilename: raw.sourceFilename);
|
|
||||||
|
|
||||||
static Future<PostgresBuildContext> create(
|
|
||||||
ClassElement element,
|
|
||||||
BuildContext raw,
|
|
||||||
ORM ormAnnotation,
|
|
||||||
Resolver resolver,
|
|
||||||
BuildStep buildStep,
|
|
||||||
{String tableName,
|
|
||||||
bool autoSnakeCaseNames,
|
|
||||||
bool autoIdAndDateFields}) async {
|
|
||||||
var ctx = new PostgresBuildContext._(
|
|
||||||
element,
|
|
||||||
raw,
|
|
||||||
ormAnnotation,
|
|
||||||
resolver,
|
|
||||||
buildStep,
|
|
||||||
tableName: tableName,
|
|
||||||
autoSnakeCaseNames: autoSnakeCaseNames,
|
|
||||||
autoIdAndDateFields: autoIdAndDateFields,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Library
|
|
||||||
ctx._libraryCache = await resolver.libraryFor(buildStep.inputId);
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<FieldElement> fields = [], relationshipFields = [];
|
|
||||||
|
|
||||||
ReCase get reCase => _reCase ?? new ReCase(modelClassName);
|
|
||||||
|
|
||||||
TypeBuilder get modelClassBuilder =>
|
|
||||||
_modelClassBuilder ??= new TypeBuilder(modelClassName);
|
|
||||||
|
|
||||||
TypeBuilder get queryClassBuilder =>
|
|
||||||
_queryClassBuilder ??= new TypeBuilder(queryClassName);
|
|
||||||
|
|
||||||
TypeBuilder get whereClassBuilder =>
|
|
||||||
_whereClassBuilder ??= new TypeBuilder(whereClassName);
|
|
||||||
|
|
||||||
TypeBuilder get postgreSQLConnectionBuilder =>
|
|
||||||
_postgresqlConnectionBuilder ??= new TypeBuilder('PostgreSQLConnection');
|
|
||||||
|
|
||||||
String get prefix {
|
|
||||||
if (_prefix != null) return _prefix;
|
|
||||||
if (relationships.isEmpty)
|
|
||||||
return _prefix = '';
|
|
||||||
else
|
|
||||||
return _prefix = tableName + '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> get aliases => raw.aliases;
|
|
||||||
|
|
||||||
Map<String, bool> get shimmed => raw.shimmed;
|
|
||||||
|
|
||||||
String get sourceFilename => raw.sourceFilename;
|
|
||||||
|
|
||||||
String get modelClassName => raw.modelClassName;
|
|
||||||
|
|
||||||
String get originalClassName => raw.originalClassName;
|
|
||||||
|
|
||||||
String get queryClassName => modelClassName + 'Query';
|
|
||||||
String get whereClassName => queryClassName + 'Where';
|
|
||||||
|
|
||||||
LibraryElement get library => _libraryCache;
|
|
||||||
|
|
||||||
TypeProvider get typeProvider =>
|
|
||||||
_typeProviderCache ??= library.context.typeProvider;
|
|
||||||
|
|
||||||
FieldElement resolveRelationshipField(String name) =>
|
|
||||||
relationshipFields.firstWhere((f) => f.name == name, orElse: () => null);
|
|
||||||
|
|
||||||
PopulatedRelationship populateRelationship(String name) {
|
|
||||||
return _populatedRelationships.putIfAbsent(name, () {
|
|
||||||
var f = raw.fields.firstWhere((f) => f.name == name);
|
|
||||||
var relationship = relationships[name];
|
|
||||||
DartType refType = f.type;
|
|
||||||
|
|
||||||
if (refType.isAssignableTo(typeProvider.listType) ||
|
|
||||||
refType.name == 'List') {
|
|
||||||
var iType = refType as InterfaceType;
|
|
||||||
|
|
||||||
if (iType.typeArguments.isEmpty)
|
|
||||||
throw 'Relationship "${f.name}" cannot be modeled as a generic List.';
|
|
||||||
|
|
||||||
refType = iType.typeArguments.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
var typeName = refType.name.startsWith('_')
|
|
||||||
? refType.name.substring(1)
|
|
||||||
: refType.name;
|
|
||||||
var rc = new ReCase(typeName);
|
|
||||||
|
|
||||||
if (relationship.type == RelationshipType.HAS_ONE ||
|
|
||||||
relationship.type == RelationshipType.HAS_MANY) {
|
|
||||||
//print('Has many $tableName');
|
|
||||||
var single = singularize(tableName);
|
|
||||||
var foreignKey = relationship.foreignTable ??
|
|
||||||
(autoSnakeCaseNames != false ? '${single}_id' : '${single}Id');
|
|
||||||
var localKey = relationship.localKey ?? 'id';
|
|
||||||
var foreignTable = relationship.foreignTable ??
|
|
||||||
(autoSnakeCaseNames != false
|
|
||||||
? pluralize(rc.snakeCase)
|
|
||||||
: pluralize(typeName));
|
|
||||||
return new PopulatedRelationship(
|
|
||||||
relationship.type,
|
|
||||||
f.name,
|
|
||||||
f.type,
|
|
||||||
buildStep,
|
|
||||||
resolver,
|
|
||||||
autoSnakeCaseNames,
|
|
||||||
autoIdAndDateFields,
|
|
||||||
relationship.type == RelationshipType.HAS_ONE,
|
|
||||||
typeProvider,
|
|
||||||
localKey: localKey,
|
|
||||||
foreignKey: foreignKey,
|
|
||||||
foreignTable: foreignTable,
|
|
||||||
cascadeOnDelete: relationship.cascadeOnDelete);
|
|
||||||
} else if (relationship.type == RelationshipType.BELONGS_TO ||
|
|
||||||
relationship.type == RelationshipType.BELONGS_TO_MANY) {
|
|
||||||
var localKey = relationship.localKey ??
|
|
||||||
(autoSnakeCaseNames != false
|
|
||||||
? '${rc.snakeCase}_id'
|
|
||||||
: '${typeName}Id');
|
|
||||||
var foreignKey = relationship.foreignKey ?? 'id';
|
|
||||||
var foreignTable = relationship.foreignTable ??
|
|
||||||
(autoSnakeCaseNames != false
|
|
||||||
? pluralize(rc.snakeCase)
|
|
||||||
: pluralize(typeName));
|
|
||||||
return new PopulatedRelationship(
|
|
||||||
relationship.type,
|
|
||||||
f.name,
|
|
||||||
f.type,
|
|
||||||
buildStep,
|
|
||||||
resolver,
|
|
||||||
autoSnakeCaseNames,
|
|
||||||
autoIdAndDateFields,
|
|
||||||
relationship.type == RelationshipType.BELONGS_TO,
|
|
||||||
typeProvider,
|
|
||||||
localKey: localKey,
|
|
||||||
foreignKey: foreignKey,
|
|
||||||
foreignTable: foreignTable,
|
|
||||||
cascadeOnDelete: relationship.cascadeOnDelete);
|
|
||||||
} else
|
|
||||||
throw new UnsupportedError(
|
|
||||||
'Invalid relationship type: ${relationship.type}');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'PostgresBuildContext: $originalClassName';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PopulatedRelationship extends Relationship {
|
|
||||||
bool _isList;
|
|
||||||
DartType _modelType;
|
|
||||||
PostgresBuildContext _modelTypeContext;
|
|
||||||
DartObject _modelTypeORM;
|
|
||||||
final String originalName;
|
|
||||||
final DartType dartType;
|
|
||||||
final BuildStep buildStep;
|
|
||||||
final Resolver resolver;
|
|
||||||
final bool autoSnakeCaseNames, autoIdAndDateFields;
|
|
||||||
final bool isSingular;
|
|
||||||
final TypeProvider typeProvider;
|
|
||||||
|
|
||||||
PopulatedRelationship(
|
|
||||||
int type,
|
|
||||||
this.originalName,
|
|
||||||
this.dartType,
|
|
||||||
this.buildStep,
|
|
||||||
this.resolver,
|
|
||||||
this.autoSnakeCaseNames,
|
|
||||||
this.autoIdAndDateFields,
|
|
||||||
this.isSingular,
|
|
||||||
this.typeProvider,
|
|
||||||
{String localKey,
|
|
||||||
String foreignKey,
|
|
||||||
String foreignTable,
|
|
||||||
bool cascadeOnDelete})
|
|
||||||
: super(type,
|
|
||||||
localKey: localKey,
|
|
||||||
foreignKey: foreignKey,
|
|
||||||
foreignTable: foreignTable,
|
|
||||||
cascadeOnDelete: cascadeOnDelete);
|
|
||||||
|
|
||||||
bool get isBelongsTo =>
|
|
||||||
type == RelationshipType.BELONGS_TO ||
|
|
||||||
type == RelationshipType.BELONGS_TO_MANY;
|
|
||||||
|
|
||||||
bool get isHas =>
|
|
||||||
type == RelationshipType.HAS_ONE || type == RelationshipType.HAS_MANY;
|
|
||||||
|
|
||||||
bool get isList => _isList ??=
|
|
||||||
dartType.isAssignableTo(typeProvider.listType) || dartType.name == 'List';
|
|
||||||
|
|
||||||
DartType get modelType {
|
|
||||||
if (_modelType != null) return _modelType;
|
|
||||||
DartType searchType = dartType;
|
|
||||||
var ormChecker = new TypeChecker.fromRuntime(ORM);
|
|
||||||
|
|
||||||
// Get inner type from List if any...
|
|
||||||
if (!isSingular) {
|
|
||||||
if (!isList)
|
|
||||||
throw '"$originalName" is a many-to-one relationship, and thus it should be represented as a List within your Dart class. You have it represented as ${dartType.name}.';
|
|
||||||
else {
|
|
||||||
var iType = dartType as InterfaceType;
|
|
||||||
if (iType.typeArguments.isEmpty)
|
|
||||||
throw '"$originalName" is a many-to-one relationship, and should be modeled as a List that references another model type. Example: `List<T>`, where T is a model type.';
|
|
||||||
else
|
|
||||||
searchType = iType.typeArguments.first;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (searchType != null) {
|
|
||||||
var classElement = searchType.element as ClassElement;
|
|
||||||
var ormAnnotation = ormChecker.firstAnnotationOf(classElement);
|
|
||||||
|
|
||||||
if (ormAnnotation != null) {
|
|
||||||
_modelTypeORM = ormAnnotation;
|
|
||||||
return _modelType = searchType;
|
|
||||||
} else {
|
|
||||||
// If we didn't find an @ORM(), then refer to the parent type.
|
|
||||||
searchType = classElement.supertype;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new StateError(
|
|
||||||
'Neither ${dartType.name} nor its parent types are annotated with an @ORM() annotation. It is impossible to compute this relationship.');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<PostgresBuildContext> get modelTypeContext async {
|
|
||||||
if (_modelTypeContext != null) return _modelTypeContext;
|
|
||||||
var reader = new ConstantReader(_modelTypeORM);
|
|
||||||
if (reader.isNull)
|
|
||||||
reader = null;
|
|
||||||
else
|
|
||||||
reader = reader.read('tableName');
|
|
||||||
var orm = reader == null
|
|
||||||
? new ORM()
|
|
||||||
: new ORM(reader.isString ? reader.stringValue : null);
|
|
||||||
return _modelTypeContext = await buildContext(modelType.element, orm,
|
|
||||||
buildStep, resolver, autoSnakeCaseNames, autoIdAndDateFields);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,399 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:analyzer/dart/ast/ast.dart';
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:build/build.dart';
|
|
||||||
import 'package:code_builder/code_builder.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:recase/recase.dart';
|
|
||||||
import 'package:source_gen/source_gen.dart' hide LibraryBuilder;
|
|
||||||
import 'build_context.dart';
|
|
||||||
import 'postgres_build_context.dart';
|
|
||||||
|
|
||||||
class PostgresServiceGenerator extends GeneratorForAnnotation<ORM> {
|
|
||||||
static const List<TypeChecker> primitives = const [
|
|
||||||
const TypeChecker.fromRuntime(String),
|
|
||||||
const TypeChecker.fromRuntime(int),
|
|
||||||
const TypeChecker.fromRuntime(bool),
|
|
||||||
const TypeChecker.fromRuntime(double),
|
|
||||||
const TypeChecker.fromRuntime(num),
|
|
||||||
];
|
|
||||||
|
|
||||||
static final ExpressionBuilder id = reference('id'),
|
|
||||||
params = reference('params'),
|
|
||||||
connection = reference('connection'),
|
|
||||||
query = reference('query'),
|
|
||||||
buildQuery = reference('buildQuery'),
|
|
||||||
applyData = reference('applyData'),
|
|
||||||
where = reference('query').property('where'),
|
|
||||||
toId = reference('toId'),
|
|
||||||
data = reference('data');
|
|
||||||
|
|
||||||
final bool autoSnakeCaseNames;
|
|
||||||
|
|
||||||
final bool autoIdAndDateFields;
|
|
||||||
|
|
||||||
const PostgresServiceGenerator(
|
|
||||||
{this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> generateForAnnotatedElement(
|
|
||||||
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
|
||||||
if (buildStep.inputId.path.contains('.service.g.dart')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element is! ClassElement)
|
|
||||||
throw 'Only classes can be annotated with @ORM().';
|
|
||||||
var resolver = await buildStep.resolver;
|
|
||||||
var lib = await generateOrmLibrary(element.library, resolver, buildStep)
|
|
||||||
.then((l) => l.buildAst());
|
|
||||||
if (lib == null) return null;
|
|
||||||
return prettyToSource(lib);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LibraryBuilder> generateOrmLibrary(LibraryElement libraryElement,
|
|
||||||
Resolver resolver, BuildStep buildStep) async {
|
|
||||||
var lib = new LibraryBuilder();
|
|
||||||
lib.addDirective(new ImportBuilder('dart:async'));
|
|
||||||
lib.addDirective(
|
|
||||||
new ImportBuilder('package:angel_framework/angel_framework.dart'));
|
|
||||||
lib.addDirective(new ImportBuilder('package:postgres/postgres.dart'));
|
|
||||||
lib.addDirective(new ImportBuilder(p.basename(buildStep.inputId.path)));
|
|
||||||
|
|
||||||
var pathName = p.basenameWithoutExtension(
|
|
||||||
p.basenameWithoutExtension(buildStep.inputId.path));
|
|
||||||
lib.addDirective(new ImportBuilder('$pathName.orm.g.dart'));
|
|
||||||
|
|
||||||
var elements = libraryElement.definingCompilationUnit.unit.declarations
|
|
||||||
.where((el) => el is ClassDeclaration);
|
|
||||||
Map<ClassElement, PostgresBuildContext> contexts = {};
|
|
||||||
List<String> done = [];
|
|
||||||
|
|
||||||
for (ClassDeclaration element in elements) {
|
|
||||||
if (!done.contains(element.name)) {
|
|
||||||
var ann = ormTypeChecker.firstAnnotationOf(element.element);
|
|
||||||
if (ann != null) {
|
|
||||||
contexts[element.element] = await buildContext(
|
|
||||||
element.element,
|
|
||||||
reviveOrm(new ConstantReader(ann)),
|
|
||||||
buildStep,
|
|
||||||
resolver,
|
|
||||||
autoSnakeCaseNames != false,
|
|
||||||
autoIdAndDateFields != false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contexts.isEmpty) return null;
|
|
||||||
|
|
||||||
done.clear();
|
|
||||||
for (var element in contexts.keys) {
|
|
||||||
if (!done.contains(element.name)) {
|
|
||||||
var ctx = contexts[element];
|
|
||||||
lib.addMember(buildServiceClass(ctx));
|
|
||||||
done.add(element.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lib;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClassBuilder buildServiceClass(PostgresBuildContext ctx) {
|
|
||||||
var rc = new ReCase(ctx.modelClassName);
|
|
||||||
var clazz = new ClassBuilder('${rc.pascalCase}Service',
|
|
||||||
asExtends: new TypeBuilder('Service'));
|
|
||||||
|
|
||||||
// Add fields
|
|
||||||
// connection, allowRemoveAll, allowQuery
|
|
||||||
|
|
||||||
clazz
|
|
||||||
..addField(varFinal('connection', type: ctx.postgreSQLConnectionBuilder))
|
|
||||||
..addField(varFinal('allowRemoveAll', type: lib$core.bool))
|
|
||||||
..addField(varFinal('allowQuery', type: lib$core.bool));
|
|
||||||
|
|
||||||
clazz.addConstructor(constructor([
|
|
||||||
thisField(parameter('connection')),
|
|
||||||
thisField(named(parameter('allowRemoveAll', [literal(false)]))),
|
|
||||||
thisField(named(parameter('allowQuery', [literal(false)])))
|
|
||||||
]));
|
|
||||||
|
|
||||||
clazz.addMethod(buildQueryMethod(ctx));
|
|
||||||
clazz.addMethod(buildToIdMethod(ctx));
|
|
||||||
clazz.addMethod(buildApplyDataMethod(ctx));
|
|
||||||
|
|
||||||
clazz.addMethod(buildIndexMethod(ctx));
|
|
||||||
clazz.addMethod(buildCreateMethod(ctx));
|
|
||||||
clazz.addMethod(buildReadOrDeleteMethod('read', 'get', ctx));
|
|
||||||
clazz.addMethod(buildReadOrDeleteMethod('remove', 'delete', ctx));
|
|
||||||
clazz.addMethod(buildUpdateMethod(ctx));
|
|
||||||
clazz.addMethod(buildModifyMethod(ctx));
|
|
||||||
|
|
||||||
return clazz;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildQueryMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth =
|
|
||||||
new MethodBuilder('buildQuery', returnType: ctx.queryClassBuilder)
|
|
||||||
..addPositional(parameter('params', [lib$core.Map]));
|
|
||||||
var paramQuery = params[literal('query')];
|
|
||||||
meth.addStatement(
|
|
||||||
varField('query', value: ctx.queryClassBuilder.newInstance([])));
|
|
||||||
var ifStmt = ifThen(paramQuery.isInstanceOf(lib$core.Map));
|
|
||||||
|
|
||||||
ctx.fields.forEach((f) {
|
|
||||||
var alias = ctx.resolveFieldName(f.name);
|
|
||||||
var queryKey = paramQuery[literal(alias)];
|
|
||||||
|
|
||||||
if (f.type.isDynamic ||
|
|
||||||
f.type.isObject ||
|
|
||||||
f.type.isObject ||
|
|
||||||
primitives.any((t) => t.isAssignableFromType(f.type))) {
|
|
||||||
ifStmt
|
|
||||||
.addStatement(where.property(f.name).invoke('equals', [queryKey]));
|
|
||||||
} else if (dateTimeTypeChecker.isAssignableFromType(f.type)) {
|
|
||||||
var dt = queryKey
|
|
||||||
.isInstanceOf(lib$core.String)
|
|
||||||
.ternary(lib$core.DateTime.invoke('parse', [queryKey]), queryKey);
|
|
||||||
ifStmt.addStatement(
|
|
||||||
where.property(f.name).invoke('equals', [updatedAt(dt)]));
|
|
||||||
} else {
|
|
||||||
print(
|
|
||||||
'Cannot compute service query binding for field "${f.name}" in ${ctx.originalClassName}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
meth.addStatement(ifStmt);
|
|
||||||
meth.addStatement(query.asReturn());
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildToIdMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('toId', returnType: lib$core.int)
|
|
||||||
..addPositional(parameter('id'));
|
|
||||||
|
|
||||||
meth.addStatement(ifThen(id.isInstanceOf(lib$core.int), [
|
|
||||||
id.asReturn(),
|
|
||||||
elseThen([
|
|
||||||
ifThen(id.equals(literal('null')).or(id.equals(literal(null))), [
|
|
||||||
literal(null).asReturn(),
|
|
||||||
elseThen([
|
|
||||||
lib$core.int.invoke('parse', [id.invoke('toString', [])]).asReturn()
|
|
||||||
])
|
|
||||||
])
|
|
||||||
])
|
|
||||||
]));
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildIndexMethod(PostgresBuildContext ctx) {
|
|
||||||
// Future<List<T>> index([p]) => buildQuery(p).get(connection).toList();
|
|
||||||
return method('index', [
|
|
||||||
new TypeBuilder('Future', genericTypes: [
|
|
||||||
new TypeBuilder('List', genericTypes: [ctx.modelClassBuilder])
|
|
||||||
]),
|
|
||||||
parameter('params', [lib$core.Map]).asOptional(),
|
|
||||||
reference('buildQuery').call([params]).invoke('get', [connection]).invoke(
|
|
||||||
'toList',
|
|
||||||
[],
|
|
||||||
).asReturn(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildReadOrDeleteMethod(
|
|
||||||
String name, String operation, PostgresBuildContext ctx) {
|
|
||||||
var throw404 = new MethodBuilder.closure()..addPositional(parameter('_'));
|
|
||||||
throw404.addStatement(new TypeBuilder('AngelHttpException').newInstance(
|
|
||||||
[],
|
|
||||||
constructor: 'notFound',
|
|
||||||
named: {
|
|
||||||
'message':
|
|
||||||
literal('No record found for ID ') + id.invoke('toString', []),
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
return method(name, [
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]),
|
|
||||||
parameter('id'),
|
|
||||||
parameter('params', [lib$core.Map]).asOptional(),
|
|
||||||
varField('query', value: buildQuery.call([params])),
|
|
||||||
where.property('id').invoke('equals', [
|
|
||||||
toId.call([id])
|
|
||||||
]),
|
|
||||||
query
|
|
||||||
.invoke(operation, [connection])
|
|
||||||
.property('first')
|
|
||||||
.invoke('catchError', [
|
|
||||||
throw404,
|
|
||||||
])
|
|
||||||
.asReturn(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildApplyDataMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth =
|
|
||||||
new MethodBuilder('applyData', returnType: ctx.modelClassBuilder);
|
|
||||||
meth.addPositional(parameter('data'));
|
|
||||||
|
|
||||||
meth.addStatement(ifThen(
|
|
||||||
data.isInstanceOf(ctx.modelClassBuilder).or(data.equals(literal(null))),
|
|
||||||
[
|
|
||||||
data.asReturn(),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
var ifStmt = new IfStatementBuilder(data.isInstanceOf(lib$core.Map));
|
|
||||||
ifStmt.addStatement(
|
|
||||||
varField('query', value: ctx.modelClassBuilder.newInstance([])));
|
|
||||||
|
|
||||||
applyFieldsToInstance(ctx, query, ifStmt.addStatement);
|
|
||||||
|
|
||||||
ifStmt.addStatement(query.asReturn());
|
|
||||||
|
|
||||||
ifStmt.setElse(
|
|
||||||
new TypeBuilder('AngelHttpException')
|
|
||||||
.newInstance([],
|
|
||||||
constructor: 'badRequest',
|
|
||||||
named: {'message': literal('Invalid data.')})
|
|
||||||
.asThrow(),
|
|
||||||
);
|
|
||||||
|
|
||||||
meth.addStatement(ifStmt);
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildCreateMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('create',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth
|
|
||||||
..addPositional(parameter('data'))
|
|
||||||
..addPositional(parameter('params', [lib$core.Map]).asOptional());
|
|
||||||
|
|
||||||
var rc = new ReCase(ctx.modelClassName);
|
|
||||||
meth.addStatement(
|
|
||||||
ctx.queryClassBuilder.invoke('insert${rc.pascalCase}', [
|
|
||||||
connection,
|
|
||||||
applyData.call([data])
|
|
||||||
]).asReturn(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildModifyMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('modify',
|
|
||||||
modifier: MethodModifier.asAsync,
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth
|
|
||||||
..addPositional(parameter('id'))
|
|
||||||
..addPositional(parameter('data'))
|
|
||||||
..addPositional(parameter('params', [lib$core.Map]).asOptional());
|
|
||||||
|
|
||||||
// read() by id
|
|
||||||
meth.addStatement(varField(
|
|
||||||
'query',
|
|
||||||
value: reference('read').call(
|
|
||||||
[
|
|
||||||
toId.call([id]),
|
|
||||||
params
|
|
||||||
],
|
|
||||||
).asAwait(),
|
|
||||||
));
|
|
||||||
|
|
||||||
var rc = new ReCase(ctx.modelClassName);
|
|
||||||
|
|
||||||
meth.addStatement(ifThen(data.isInstanceOf(ctx.modelClassBuilder), [
|
|
||||||
data.asAssign(query),
|
|
||||||
]));
|
|
||||||
|
|
||||||
var ifStmt = ifThen(data.isInstanceOf(lib$core.Map));
|
|
||||||
|
|
||||||
applyFieldsToInstance(ctx, query, ifStmt.addStatement);
|
|
||||||
meth.addStatement(ifStmt);
|
|
||||||
meth.addStatement(
|
|
||||||
ctx.queryClassBuilder
|
|
||||||
.invoke('update${rc.pascalCase}', [connection, query])
|
|
||||||
.asAwait()
|
|
||||||
.asReturn(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildUpdateMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('update',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth
|
|
||||||
..addPositional(parameter('id'))
|
|
||||||
..addPositional(parameter('data'))
|
|
||||||
..addPositional(parameter('params', [lib$core.Map]).asOptional());
|
|
||||||
|
|
||||||
var rc = new ReCase(ctx.modelClassName);
|
|
||||||
meth.addStatement(
|
|
||||||
ctx.queryClassBuilder.invoke('update${rc.pascalCase}', [
|
|
||||||
connection,
|
|
||||||
applyData.call([data])
|
|
||||||
]).asReturn(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void parseParams(MethodBuilder meth, PostgresBuildContext ctx, {bool id}) {
|
|
||||||
meth.addStatement(varField('query',
|
|
||||||
value: buildQuery.call([
|
|
||||||
reference('params')
|
|
||||||
.notEquals(literal(null))
|
|
||||||
.ternary(reference('params'), map({}))
|
|
||||||
])));
|
|
||||||
|
|
||||||
if (id == true) {
|
|
||||||
meth.addStatement(
|
|
||||||
reference('query').property('where').property('id').invoke('equals', [
|
|
||||||
reference('toId').call([reference('id')])
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyFieldsToInstance(PostgresBuildContext ctx, ExpressionBuilder query,
|
|
||||||
void addStatement(StatementBuilder statement)) {
|
|
||||||
ctx.fields.forEach((f) {
|
|
||||||
var alias = ctx.resolveFieldName(f.name);
|
|
||||||
var dataKey = data[literal(alias)];
|
|
||||||
ExpressionBuilder target;
|
|
||||||
|
|
||||||
// Skip `id`
|
|
||||||
if (autoIdAndDateFields != false && f.name == 'id') return;
|
|
||||||
|
|
||||||
if (f.type.isDynamic ||
|
|
||||||
f.type.isObject ||
|
|
||||||
primitives.any((t) => t.isAssignableFromType(f.type))) {
|
|
||||||
target = dataKey;
|
|
||||||
} else if (dateTimeTypeChecker.isAssignableFromType(f.type)) {
|
|
||||||
var dt = dataKey
|
|
||||||
.isInstanceOf(lib$core.String)
|
|
||||||
.ternary(lib$core.DateTime.invoke('parse', [dataKey]), dataKey);
|
|
||||||
target = updatedAt(dt);
|
|
||||||
} else {
|
|
||||||
print(
|
|
||||||
'Cannot compute service applyData() binding for field "${f.name}" in ${ctx.originalClassName}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target != null) {
|
|
||||||
addStatement(ifThen(data.invoke('containsKey', [literal(alias)]),
|
|
||||||
[target.asAssign(query.property(f.name))]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpressionBuilder updatedAt(ExpressionBuilder dt) {
|
|
||||||
if (autoIdAndDateFields == false) return dt;
|
|
||||||
return dt
|
|
||||||
.notEquals(literal(null))
|
|
||||||
.ternary(dt, lib$core.DateTime.newInstance([], constructor: 'now'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,6 +8,6 @@ part 'author.g.dart';
|
||||||
@serializable
|
@serializable
|
||||||
@orm
|
@orm
|
||||||
class _Author extends Model {
|
class _Author extends Model {
|
||||||
@Column(length: 255, index: IndexType.UNIQUE, defaultValue: 'Tobe Osakwe')
|
@Column(length: 255, indexType: IndexType.UNIQUE, defaultValue: 'Tobe Osakwe')
|
||||||
String name;
|
String name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ part 'tree.g.dart';
|
||||||
@serializable
|
@serializable
|
||||||
@orm
|
@orm
|
||||||
class _Tree extends Model {
|
class _Tree extends Model {
|
||||||
@Column(index: IndexType.UNIQUE, type: ColumnType.SMALL_INT)
|
@Column(indexType: IndexType.UNIQUE, type: ColumnType.smallInt)
|
||||||
int rings;
|
int rings;
|
||||||
|
|
||||||
@hasMany
|
@hasMany
|
||||||
|
|
|
@ -28,9 +28,9 @@ main() {
|
||||||
'Mazda',
|
'Mazda',
|
||||||
'CX9',
|
'CX9',
|
||||||
true,
|
true,
|
||||||
DATE_YMD_HMS.format(MILENNIUM),
|
dateYmdHms.format(MILENNIUM),
|
||||||
DATE_YMD_HMS.format(MILENNIUM),
|
dateYmdHms.format(MILENNIUM),
|
||||||
DATE_YMD_HMS.format(MILENNIUM)
|
dateYmdHms.format(MILENNIUM)
|
||||||
];
|
];
|
||||||
print(row);
|
print(row);
|
||||||
var car = CarQuery.parseRow(row);
|
var car = CarQuery.parseRow(row);
|
||||||
|
@ -167,7 +167,7 @@ main() {
|
||||||
expect(car.description, 'Hello');
|
expect(car.description, 'Hello');
|
||||||
expect(car.familyFriendly, isTrue);
|
expect(car.familyFriendly, isTrue);
|
||||||
expect(
|
expect(
|
||||||
DATE_YMD_HMS.format(car.recalledAt), DATE_YMD_HMS.format(recalledAt));
|
dateYmdHms.format(car.recalledAt), dateYmdHms.format(recalledAt));
|
||||||
expect(car.createdAt, allOf(isNotNull, equals(car.updatedAt)));
|
expect(car.createdAt, allOf(isNotNull, equals(car.updatedAt)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -183,8 +183,8 @@ main() {
|
||||||
expect(car.make, beetle.make);
|
expect(car.make, beetle.make);
|
||||||
expect(car.description, beetle.description);
|
expect(car.description, beetle.description);
|
||||||
expect(car.familyFriendly, beetle.familyFriendly);
|
expect(car.familyFriendly, beetle.familyFriendly);
|
||||||
expect(DATE_YMD_HMS.format(car.recalledAt),
|
expect(dateYmdHms.format(car.recalledAt),
|
||||||
DATE_YMD_HMS.format(beetle.recalledAt));
|
dateYmdHms.format(beetle.recalledAt));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue