diff --git a/.idea/orm.iml b/.idea/orm.iml index 5ef6ddeb..f6a57903 100644 --- a/.idea/orm.iml +++ b/.idea/orm.iml @@ -2,10 +2,12 @@ + + diff --git a/angel_orm/.analysis-options b/angel_orm/.analysis-options deleted file mode 100644 index 518eb901..00000000 --- a/angel_orm/.analysis-options +++ /dev/null @@ -1,2 +0,0 @@ -analyzer: - strong-mode: true \ No newline at end of file diff --git a/angel_orm/.gitignore b/angel_orm/.gitignore index 99e7978e..fe4c4cfa 100644 --- a/angel_orm/.gitignore +++ b/angel_orm/.gitignore @@ -54,3 +54,5 @@ com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties + +.dart_tool \ No newline at end of file diff --git a/angel_orm/CHANGELOG.md b/angel_orm/CHANGELOG.md index 2e8b867d..1cd54709 100644 --- a/angel_orm/CHANGELOG.md +++ b/angel_orm/CHANGELOG.md @@ -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 * Removed PostgreSQL-specific functionality, so that the ORM can ultimately target all services. diff --git a/angel_orm/analysis_options.yaml b/angel_orm/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/angel_orm/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/angel_orm/example/main.dart b/angel_orm/example/main.dart index 40b6ae01..800bb932 100644 --- a/angel_orm/example/main.dart +++ b/angel_orm/example/main.dart @@ -1,27 +1,27 @@ import 'package:angel_model/angel_model.dart'; import 'package:angel_orm/angel_orm.dart'; -Query findEmployees(Company company) { - return new Query() - ..['company_id'] = equals(company.id) - ..['first_name'] = notNull() & (equals('John')) - ..['salary'] = greaterThanOrEqual(100000.0); -} +main() { -@ORM('api/companies') -class Company extends Model { - String name; - bool isFortune500; } @orm -class Employee extends Model { +abstract class Company extends Model { + String get name; + + bool get isFortune500; +} + +@orm +abstract class _Employee extends Model { @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; } diff --git a/angel_orm/lib/angel_orm.dart b/angel_orm/lib/angel_orm.dart index 9243760e..1af57ece 100644 --- a/angel_orm/lib/angel_orm.dart +++ b/angel_orm/lib/angel_orm.dart @@ -1,3 +1,4 @@ export 'src/annotations.dart'; -export 'src/query.dart'; -export 'src/relations.dart'; \ No newline at end of file +export 'src/migration.dart'; +export 'src/relations.dart'; +export 'src/query.dart'; \ No newline at end of file diff --git a/angel_orm/lib/src/annotations.dart b/angel_orm/lib/src/annotations.dart index 677ce0a1..ea2c049d 100644 --- a/angel_orm/lib/src/annotations.dart +++ b/angel_orm/lib/src/annotations.dart @@ -1,30 +1,17 @@ const ORM orm = const ORM(); class ORM { - /// The path to an Angel service that queries objects of the - /// annotated type at runtime. - /// - /// Ex. `api/users`, etc. - final String servicePath; - const ORM([this.servicePath]); + final String tableName; + + const ORM([this.tableName]); } -/// Specifies that the ORM should build a join builder -/// that combines the results of queries on two services. -class Join { - /// The [Model] type to join against. +class CanJoin { final Type type; - - /// 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 String foreignKey; 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]. diff --git a/angel_orm/lib/src/migration.dart b/angel_orm/lib/src/migration.dart new file mode 100644 index 00000000..d3b50736 --- /dev/null +++ b/angel_orm/lib/src/migration.dart @@ -0,0 +1,124 @@ +const List 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'); +} diff --git a/angel_orm/lib/src/query.dart b/angel_orm/lib/src/query.dart index cefb89fa..406222fc 100644 --- a/angel_orm/lib/src/query.dart +++ b/angel_orm/lib/src/query.dart @@ -1,114 +1,340 @@ -/// Expects a field to be equal to a given [value]. -Predicate equals(T value) => - new Predicate._(PredicateType.equals, value); +import 'package:intl/intl.dart'; +import 'package:string_scanner/string_scanner.dart'; -/// Expects at least one of the given [predicates] to be true. -Predicate anyOf(Iterable> predicates) => - new MultiPredicate._(PredicateType.any, predicates); +final DateFormat dateYmd = new DateFormat('yyyy-MM-dd'); +final DateFormat dateYmdHms = new DateFormat('yyyy-MM-dd HH:mm:ss'); -/// Expects a field to be contained within a set of [values]. -Predicate isIn(Iterable values) => new Predicate._(PredicateType.isIn, null, values); +/// Cleans an input SQL expression of common SQL injection points. +String sanitizeExpression(String unsafe) { + var buf = new StringBuffer(); + var scanner = new StringScanner(unsafe); + int ch; -/// Expects a field to be `null`. -Predicate isNull() => equals(null); + while (!scanner.isDone) { + // Ignore comment starts + if (scanner.scan('--') || scanner.scan('/*')) + continue; -/// Expects a given [predicate] to not be true. -Predicate not(Predicate predicate) => - new MultiPredicate._(PredicateType.negate, [predicate]); + // Ignore all single quotes and attempted escape sequences + else if (scanner.scan("'") || scanner.scan('\\')) + continue; -/// Expects a field to be not be `null`. -Predicate notNull() => not(isNull()); - -/// Expects a field to be less than a given [value]. -Predicate lessThan(T value) => - new Predicate._(PredicateType.less, value); - -/// Expects a field to be less than or equal to a given [value]. -Predicate lessThanOrEqual(T value) => lessThan(value) | equals(value); - -/// Expects a field to be greater than a given [value]. -Predicate greaterThan(T value) => - new Predicate._(PredicateType.greater, value); - -/// Expects a field to be greater than or equal to a given [value]. -Predicate greaterThanOrEqual(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 _fields = {}; - final Map _sort = {}; - - /// Each field in a query is actually a [Predicate], and therefore acts as a contract - /// with the underlying service. - Map get fields => - new Map.unmodifiable(_fields); - - /// The sorting order applied to this query. - Map get sorting => - new Map.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 { - /// The type of expectation we are declaring. - final PredicateType type; - - /// The single argument of this target. - final T target; - final Iterable args; - - Predicate._(this.type, this.target, [this.args]); - - Predicate operator &(Predicate other) => and(other); - - Predicate operator |(Predicate other) => or(other); - - Predicate and(Predicate other) { - return new MultiPredicate._(PredicateType.and, [this, other]); + // Otherwise, add the next char, unless it's a null byte. + else if ((ch = scanner.readChar()) != 0 && ch != null) + buf.writeCharCode(ch); } - Predicate or(Predicate other) { - return new MultiPredicate._(PredicateType.or, [this, other]); + return buf.toString(); +} + +abstract class SqlExpressionBuilder { + bool get hasValue; + + String compile(); + + void isBetween(T lower, T upper); + + void isNotBetween(T lower, T upper); + + void isIn(Iterable values); + + void isNotIn(Iterable values); +} + +class NumericSqlExpressionBuilder + implements SqlExpressionBuilder { + 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 values) { + _raw = 'IN (' + values.join(', ') + ')'; + _hasValue = true; + } + + @override + void isNotIn(Iterable values) { + _raw = 'NOT IN (' + values.join(', ') + ')'; + _hasValue = true; } } -/// An advanced [Predicate] that performs an operation of multiple other predicates. -class MultiPredicate extends Predicate { - final Iterable> targets; +class StringSqlExpressionBuilder implements SqlExpressionBuilder { + bool _hasValue = false; + String _op = '=', _raw, _value; - MultiPredicate._(PredicateType type, this.targets) : super._(type, null); + @override + bool get hasValue => _hasValue; - /// Use [targets] instead. - @deprecated - T get target => throw new UnsupportedError( - 'IterablePredicate has no `target`. Use `targets` instead.'); + bool _change(String op, String value) { + _raw = null; + _op = op; + _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 values) { + _raw = 'IN (' + + values.map(sanitizeExpression).map((s) => "'$s'").join(', ') + + ')'; + _hasValue = true; + } + + @override + void isNotIn(Iterable values) { + _raw = 'NOT IN (' + + values.map(sanitizeExpression).map((s) => "'$s'").join(', ') + + ')'; + _hasValue = true; + } } -/// The various types of predicate. -enum PredicateType { - equals, - any, - isIn, - negate, - and, - or, - less, - greater, +class BooleanSqlExpressionBuilder implements SqlExpressionBuilder { + bool _hasValue = false; + String _op = '=', _raw; + bool _value; + + @override + bool get hasValue => _hasValue; + + bool _change(String op, bool value) { + _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 values) { + _raw = 'IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')'; + _hasValue = true; + } + + @override + void isNotIn(Iterable values) { + _raw = + 'NOT IN (' + values.map((b) => b ? 'TRUE' : 'FALSE').join(', ') + ')'; + _hasValue = true; + } } -/// The various modes of sorting. -enum SortType { ascending, descending } +class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder { + final NumericSqlExpressionBuilder year = + new NumericSqlExpressionBuilder(), + month = new NumericSqlExpressionBuilder(), + day = new NumericSqlExpressionBuilder(), + hour = new NumericSqlExpressionBuilder(), + minute = new NumericSqlExpressionBuilder(), + second = new NumericSqlExpressionBuilder(); + 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 values) { + _raw = '$columnName IN (' + + values.map(dateYmdHms.format).map((s) => '$s').join(', ') + + ')'; + } + + @override + void isNotIn(Iterable 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 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 '); + } +} diff --git a/angel_orm/lib/src/relations.dart b/angel_orm/lib/src/relations.dart index 8eb57c26..29885767 100644 --- a/angel_orm/lib/src/relations.dart +++ b/angel_orm/lib/src/relations.dart @@ -1,8 +1,8 @@ abstract class RelationshipType { - static const int HAS_MANY = 0; - static const int HAS_ONE = 1; - static const int BELONGS_TO = 2; - static const int BELONGS_TO_MANY = 3; + static const int hasMany = 0; + static const int hasOne = 1; + static const int belongsTo = 2; + static const int belongsToMany = 3; } class Relationship { @@ -25,7 +25,7 @@ class HasMany extends Relationship { String foreignKey, String foreignTable, bool cascadeOnDelete: false}) - : super(RelationshipType.HAS_MANY, + : super(RelationshipType.hasMany, localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable, @@ -40,7 +40,7 @@ class HasOne extends Relationship { String foreignKey, String foreignTable, bool cascadeOnDelete: false}) - : super(RelationshipType.HAS_ONE, + : super(RelationshipType.hasOne, localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable, @@ -52,7 +52,7 @@ const HasOne hasOne = const HasOne(); class BelongsTo extends Relationship { const BelongsTo( {String localKey: 'id', String foreignKey, String foreignTable}) - : super(RelationshipType.BELONGS_TO, + : super(RelationshipType.belongsTo, localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable); @@ -63,7 +63,7 @@ const BelongsTo belongsTo = const BelongsTo(); class BelongsToMany extends Relationship { const BelongsToMany( {String localKey: 'id', String foreignKey, String foreignTable}) - : super(RelationshipType.BELONGS_TO_MANY, + : super(RelationshipType.belongsToMany, localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable); diff --git a/angel_orm/pubspec.yaml b/angel_orm/pubspec.yaml index 3fd8d3ec..2bdfdd9d 100644 --- a/angel_orm/pubspec.yaml +++ b/angel_orm/pubspec.yaml @@ -1,10 +1,13 @@ name: angel_orm -version: 1.0.0-alpha+11 +version: 2.0.0-dev description: Runtime support for Angel's ORM. author: Tobe O homepage: https://github.com/angel-dart/orm environment: - sdk: '>=2.0.0-dev.1.2 <2.0.0' + sdk: '>=2.0.0-dev.1.2 <3.0.0' dependencies: - angel_model: ^1.0.0 - meta: ^1.0.0 \ No newline at end of file + intl: ^0.15.7 + meta: ^1.0.0 + string_scanner: ^1.0.0 +dev_dependencies: + angel_model: ^1.0.0 \ No newline at end of file diff --git a/angel_orm_generator/lib/angel_orm_generator.dart b/angel_orm_generator/lib/angel_orm_generator.dart index 24a7bbad..e69de29b 100644 --- a/angel_orm_generator/lib/angel_orm_generator.dart +++ b/angel_orm_generator/lib/angel_orm_generator.dart @@ -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'; \ No newline at end of file diff --git a/angel_orm_generator/lib/src/builder/orm/build_context.dart b/angel_orm_generator/lib/src/builder/orm/build_context.dart deleted file mode 100644 index 0f99e9d1..00000000 --- a/angel_orm_generator/lib/src/builder/orm/build_context.dart +++ /dev/null @@ -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 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 fieldNames = []; - List 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); -} diff --git a/angel_orm_generator/lib/src/builder/orm/migration.dart b/angel_orm_generator/lib/src/builder/orm/migration.dart deleted file mode 100644 index 61505f5f..00000000 --- a/angel_orm_generator/lib/src/builder/orm/migration.dart +++ /dev/null @@ -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 { - 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 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 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 positional = [literal(key)]; - Map 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 = []; - - 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}')")); - }); - } -} diff --git a/angel_orm_generator/lib/src/builder/orm/postgres_build_context.dart b/angel_orm_generator/lib/src/builder/orm/postgres_build_context.dart deleted file mode 100644 index c765bfb5..00000000 --- a/angel_orm_generator/lib/src/builder/orm/postgres_build_context.dart +++ /dev/null @@ -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 _populatedRelationships = {}; - final Map columnInfo = {}; - final Map indices = {}; - final Map> joins = {}; - final Map 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 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 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 get aliases => raw.aliases; - - Map 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`, 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 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); - } -} diff --git a/angel_orm_generator/lib/src/builder/orm/service.dart b/angel_orm_generator/lib/src/builder/orm/service.dart deleted file mode 100644 index 8cd95e05..00000000 --- a/angel_orm_generator/lib/src/builder/orm/service.dart +++ /dev/null @@ -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 { - static const List 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 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 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 contexts = {}; - List 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> 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')); - } -} diff --git a/angel_orm_generator/test/models/author.dart b/angel_orm_generator/test/models/author.dart index 66d316e9..274685d9 100644 --- a/angel_orm_generator/test/models/author.dart +++ b/angel_orm_generator/test/models/author.dart @@ -8,6 +8,6 @@ part 'author.g.dart'; @serializable @orm class _Author extends Model { - @Column(length: 255, index: IndexType.UNIQUE, defaultValue: 'Tobe Osakwe') + @Column(length: 255, indexType: IndexType.UNIQUE, defaultValue: 'Tobe Osakwe') String name; } diff --git a/angel_orm_generator/test/models/tree.dart b/angel_orm_generator/test/models/tree.dart index 9935e86e..01c946a8 100644 --- a/angel_orm_generator/test/models/tree.dart +++ b/angel_orm_generator/test/models/tree.dart @@ -9,7 +9,7 @@ part 'tree.g.dart'; @serializable @orm class _Tree extends Model { - @Column(index: IndexType.UNIQUE, type: ColumnType.SMALL_INT) + @Column(indexType: IndexType.UNIQUE, type: ColumnType.smallInt) int rings; @hasMany diff --git a/angel_orm_generator/test/standalone_test.dart b/angel_orm_generator/test/standalone_test.dart index 8d7abffa..7efa36ef 100644 --- a/angel_orm_generator/test/standalone_test.dart +++ b/angel_orm_generator/test/standalone_test.dart @@ -28,9 +28,9 @@ main() { 'Mazda', 'CX9', true, - DATE_YMD_HMS.format(MILENNIUM), - DATE_YMD_HMS.format(MILENNIUM), - DATE_YMD_HMS.format(MILENNIUM) + dateYmdHms.format(MILENNIUM), + dateYmdHms.format(MILENNIUM), + dateYmdHms.format(MILENNIUM) ]; print(row); var car = CarQuery.parseRow(row); @@ -167,7 +167,7 @@ main() { expect(car.description, 'Hello'); expect(car.familyFriendly, isTrue); 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))); }); @@ -183,8 +183,8 @@ main() { expect(car.make, beetle.make); expect(car.description, beetle.description); expect(car.familyFriendly, beetle.familyFriendly); - expect(DATE_YMD_HMS.format(car.recalledAt), - DATE_YMD_HMS.format(beetle.recalledAt)); + expect(dateYmdHms.format(car.recalledAt), + dateYmdHms.format(beetle.recalledAt)); }); }); }