diff --git a/angel_orm/lib/src/annotations.dart b/angel_orm/lib/src/annotations.dart index ef2c3c1e..f5976d73 100644 --- a/angel_orm/lib/src/annotations.dart +++ b/angel_orm/lib/src/annotations.dart @@ -14,6 +14,7 @@ class Orm { const Orm({this.tableName, this.generateMigrations: true}); } +@deprecated class Join { final Type against; final String foreignKey; diff --git a/angel_orm/lib/src/builder.dart b/angel_orm/lib/src/builder.dart index 736682a9..7ee0ec83 100644 --- a/angel_orm/lib/src/builder.dart +++ b/angel_orm/lib/src/builder.dart @@ -399,3 +399,39 @@ class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder { return parts.isEmpty ? null : parts.join(' AND '); } } + +class MapSqlExpressionBuilder, + Value extends SqlExpressionBuilder> extends SqlExpressionBuilder { + final Key key; + final Value value; + bool _hasValue = false; + String _raw; + + MapSqlExpressionBuilder(Query query, String columnName, this.key, this.value) + : super(query, columnName); + + UnsupportedError _unsupported() => + UnsupportedError('JSON/JSONB does not support this operation.'); + + @override + String compile() { + var parts = [_raw, key.compile(), value.compile()]; + parts.removeWhere((s) => s == null); + return parts.isEmpty ? null : parts.join(' && '); + } + + @override + bool get hasValue => key.hasValue || value.hasValue || _hasValue; + + @override + void isBetween(lower, upper) => throw _unsupported(); + + @override + void isIn(Iterable values) => throw _unsupported(); + + @override + void isNotBetween(lower, upper) => throw _unsupported(); + + @override + void isNotIn(Iterable values) => throw _unsupported(); +} diff --git a/angel_orm/lib/src/migration.dart b/angel_orm/lib/src/migration.dart index 40615282..d5edd31f 100644 --- a/angel_orm/lib/src/migration.dart +++ b/angel_orm/lib/src/migration.dart @@ -111,6 +111,10 @@ class ColumnType { static const ColumnType varBinaryMax = const ColumnType('varbinary(max)'); static const ColumnType image = const ColumnType('image'); + // JSON. + static const ColumnType json = const ColumnType('json'); + static const ColumnType jsonb = const ColumnType('jsonb'); + // Misc. static const ColumnType sqlVariant = const ColumnType('sql_variant'); static const ColumnType uniqueIdentifier = diff --git a/angel_orm_generator/build.yaml b/angel_orm_generator/build.yaml index ba5de6e8..9aef2e06 100644 --- a/angel_orm_generator/build.yaml +++ b/angel_orm_generator/build.yaml @@ -26,12 +26,14 @@ targets: - test/models/customer.dart - test/models/foot.dart - test/models/fruit.dart + - test/models/has_map.dart - test/models/role.dart $default: dependencies: - :_standalone sources: - test/models/book.dart + # - test/models/has_car.dart - test/models/leg.dart - test/models/order.dart - test/models/tree.dart diff --git a/angel_orm_generator/lib/src/migration_generator.dart b/angel_orm_generator/lib/src/migration_generator.dart index 28911ac4..e0a246e4 100644 --- a/angel_orm_generator/lib/src/migration_generator.dart +++ b/angel_orm_generator/lib/src/migration_generator.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; +import 'package:angel_model/angel_model.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize_generator/angel_serialize_generator.dart'; import 'package:build/build.dart'; @@ -11,8 +12,7 @@ import 'orm_build_context.dart'; Builder migrationBuilder(BuilderOptions options) { return new SharedPartBuilder([ new MigrationGenerator( - autoSnakeCaseNames: options.config['auto_snake_case_names'] != false, - autoIdAndDateFields: options.config['auto_id_and_date_fields'] != false) + autoSnakeCaseNames: options.config['auto_snake_case_names'] != false) ], 'angel_migration'); } @@ -25,11 +25,7 @@ class MigrationGenerator extends GeneratorForAnnotation { /// 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}); + const MigrationGenerator({this.autoSnakeCaseNames: true}); @override Future generateForAnnotatedElement( @@ -45,13 +41,8 @@ class MigrationGenerator extends GeneratorForAnnotation { } var resolver = await buildStep.resolver; - var ctx = await buildOrmContext( - element as ClassElement, - annotation, - buildStep, - resolver, - autoSnakeCaseNames != false, - autoIdAndDateFields != false); + var ctx = await buildOrmContext(element as ClassElement, annotation, + buildStep, resolver, autoSnakeCaseNames != false); var lib = generateMigrationLibrary( ctx, element as ClassElement, resolver, buildStep); if (lib == null) return null; @@ -73,6 +64,8 @@ class MigrationGenerator extends GeneratorForAnnotation { Method buildUpMigration(OrmBuildContext ctx, LibraryBuilder lib) { return new Method((meth) { + var autoIdAndDateFields = const TypeChecker.fromRuntime(Model) + .isAssignableFromType(ctx.buildContext.clazz.type); meth ..name = 'up' ..annotations.add(refer('override')) diff --git a/angel_orm_generator/lib/src/orm_build_context.dart b/angel_orm_generator/lib/src/orm_build_context.dart index 4d9d0ea2..3746207b 100644 --- a/angel_orm_generator/lib/src/orm_build_context.dart +++ b/angel_orm_generator/lib/src/orm_build_context.dart @@ -27,7 +27,6 @@ Future buildOrmContext( BuildStep buildStep, Resolver resolver, bool autoSnakeCaseNames, - bool autoIdAndDateFields, {bool heedExclude: true}) async { // Check for @generatedSerializable // ignore: unused_local_variable @@ -44,8 +43,8 @@ Future buildOrmContext( if (_cache.containsKey(id)) { return _cache[id]; } - var buildCtx = await buildContext(clazz, annotation, buildStep, resolver, - autoSnakeCaseNames, autoIdAndDateFields, + var buildCtx = await buildContext( + clazz, annotation, buildStep, resolver, autoSnakeCaseNames, heedExclude: heedExclude); var ormAnnotation = reviveORMAnnotation(annotation); var ctx = new OrmBuildContext( @@ -66,7 +65,10 @@ Future buildOrmContext( column = reviveColumn(new ConstantReader(columnAnnotation)); } - if (column == null && field.name == 'id' && autoIdAndDateFields == true) { + if (column == null && + field.name == 'id' && + const TypeChecker.fromRuntime(Model) + .isAssignableFromType(buildCtx.clazz.type)) { // This is only for PostgreSQL, so implementations without a `serial` type // must handle it accordingly, of course. column = const Column(type: ColumnType.serial); @@ -76,7 +78,7 @@ Future buildOrmContext( // Guess what kind of column this is... column = new Column( type: inferColumnType( - field.type, + buildCtx.resolveSerializedFieldType(field.name), ), ); } @@ -133,8 +135,7 @@ Future buildOrmContext( .firstAnnotationOf(modelType.element)), buildStep, resolver, - autoSnakeCaseNames, - autoIdAndDateFields); + autoSnakeCaseNames); var ormAnn = const TypeChecker.fromRuntime(Orm) .firstAnnotationOf(modelType.element); @@ -176,7 +177,9 @@ Future buildOrmContext( ctx.buildContext.aliases[name] = relation.localKey; if (!ctx.effectiveFields.any((f) => f.name == field.name)) { - if (field.name != 'id' || !autoIdAndDateFields) { + if (field.name != 'id' || + !const TypeChecker.fromRuntime(Model) + .isAssignableFromType(ctx.buildContext.clazz.type)) { var rf = new RelationFieldImpl(name, field.type.element.context.typeProvider.intType, field.name); ctx.effectiveFields.add(rf); @@ -212,6 +215,8 @@ ColumnType inferColumnType(DartType type) { return ColumnType.boolean; if (const TypeChecker.fromRuntime(DateTime).isAssignableFromType(type)) return ColumnType.timeStamp; + if (const TypeChecker.fromRuntime(Map).isAssignableFromType(type)) + return ColumnType.jsonb; return null; } diff --git a/angel_orm_generator/lib/src/orm_generator.dart b/angel_orm_generator/lib/src/orm_generator.dart index dea1b079..5545cb2f 100644 --- a/angel_orm_generator/lib/src/orm_generator.dart +++ b/angel_orm_generator/lib/src/orm_generator.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:angel_model/angel_model.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize_generator/angel_serialize_generator.dart'; import 'package:angel_serialize_generator/build_context.dart'; @@ -12,8 +14,7 @@ import 'orm_build_context.dart'; Builder ormBuilder(BuilderOptions options) { return new SharedPartBuilder([ new OrmGenerator( - autoSnakeCaseNames: options.config['auto_snake_case_names'] != false, - autoIdAndDateFields: options.config['auto_id_and_date_fields'] != false) + autoSnakeCaseNames: options.config['auto_snake_case_names'] != false) ], 'angel_orm'); } @@ -26,16 +27,15 @@ TypeReference futureOf(String type) { /// Builder that generates `.orm.g.dart`, with an abstract `FooOrm` class. class OrmGenerator extends GeneratorForAnnotation { final bool autoSnakeCaseNames; - final bool autoIdAndDateFields; - OrmGenerator({this.autoSnakeCaseNames, this.autoIdAndDateFields}); + OrmGenerator({this.autoSnakeCaseNames}); @override Future generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep) async { if (element is ClassElement) { var ctx = await buildOrmContext(element, annotation, buildStep, - buildStep.resolver, autoSnakeCaseNames, autoIdAndDateFields); + buildStep.resolver, autoSnakeCaseNames); var lib = buildOrmLibrary(buildStep.inputId, ctx); return lib.accept(new DartEmitter()).toString(); } else { @@ -146,10 +146,10 @@ class OrmGenerator extends GeneratorForAnnotation { for (var field in ctx.effectiveFields) { Reference type = convertTypeReference(field.type); - if (isSpecialId(field)) type = refer('int'); + if (isSpecialId(ctx, field)) type = refer('int'); var expr = (refer('row').index(literalNum(i++))); - if (isSpecialId(field)) + if (isSpecialId(ctx, field)) expr = expr.property('toString').call([]); else if (field is RelationFieldImpl) continue; @@ -229,7 +229,7 @@ class OrmGenerator extends GeneratorForAnnotation { relation.type == RelationshipType.hasMany) { var foreign = ctx.relationTypes[relation]; var additionalFields = foreign.effectiveFields - .where((f) => f.name != 'id' || !isSpecialId(f)) + .where((f) => f.name != 'id' || !isSpecialId(ctx, f)) .map((f) => literalString( foreign.buildContext.resolveFieldName(f.name))); var joinArgs = [relation.localKey, relation.foreignKey] @@ -284,7 +284,7 @@ class OrmGenerator extends GeneratorForAnnotation { // Just call getOne() again if (ctx.effectiveFields.any((f) => - isSpecialId(f) || + isSpecialId(ctx, f) || (ctx.columns[f.name]?.indexType == IndexType.primaryKey))) { b.addExpression(refer('where') @@ -370,7 +370,7 @@ class OrmGenerator extends GeneratorForAnnotation { 'but ${ctx.buildContext.clazz.name} has a @HasMany() relation that expects such a field.'; }); - var queryValue = (isSpecialId(localField)) + var queryValue = (isSpecialId(ctx, localField)) ? 'int.parse(model.id)' : 'model.${localField.name}'; var cascadeText = @@ -445,10 +445,12 @@ class OrmGenerator extends GeneratorForAnnotation { }); } - bool isSpecialId(FieldElement field) { + bool isSpecialId(OrmBuildContext ctx, FieldElement field) { return field is ShimFieldImpl && field is! RelationFieldImpl && - (field.name == 'id' && autoIdAndDateFields); + (field.name == 'id' && + const TypeChecker.fromRuntime(Model) + .isAssignableFromType(ctx.buildContext.clazz.type)); } Class buildWhereClass(OrmBuildContext ctx) { @@ -475,23 +477,31 @@ class OrmGenerator extends GeneratorForAnnotation { // Add builders for each field for (var field in ctx.effectiveFields) { var name = field.name; + DartType type; Reference builderType; - if (const TypeChecker.fromRuntime(int).isExactlyType(field.type) || - const TypeChecker.fromRuntime(double).isExactlyType(field.type) || - isSpecialId(field)) { + try { + type = ctx.buildContext.resolveSerializedFieldType(field.name); + } on StateError { + type = field.type; + } + + if (const TypeChecker.fromRuntime(int).isExactlyType(type) || + const TypeChecker.fromRuntime(double).isExactlyType(type) || + isSpecialId(ctx, field)) { builderType = new TypeReference((b) => b ..symbol = 'NumericSqlExpressionBuilder' - ..types.add(refer(isSpecialId(field) ? 'int' : field.type.name))); - } else if (const TypeChecker.fromRuntime(String) - .isExactlyType(field.type)) { + ..types.add(refer(isSpecialId(ctx, field) ? 'int' : type.name))); + } else if (const TypeChecker.fromRuntime(String).isExactlyType(type)) { builderType = refer('StringSqlExpressionBuilder'); - } else if (const TypeChecker.fromRuntime(bool) - .isExactlyType(field.type)) { + } else if (const TypeChecker.fromRuntime(bool).isExactlyType(type)) { builderType = refer('BooleanSqlExpressionBuilder'); } else if (const TypeChecker.fromRuntime(DateTime) - .isExactlyType(field.type)) { + .isExactlyType(type)) { builderType = refer('DateTimeSqlExpressionBuilder'); + } else if (const TypeChecker.fromRuntime(Map) + .isAssignableFromType(type)) { + builderType = refer('MapSqlExpressionBuilder'); } else if (ctx.relations.containsKey(field.name)) { var relation = ctx.relations[field.name]; if (relation.type != RelationshipType.belongsTo) @@ -545,7 +555,7 @@ class OrmGenerator extends GeneratorForAnnotation { // Each field generates a getter for setter for (var field in ctx.effectiveFields) { var name = ctx.buildContext.resolveFieldName(field.name); - var type = isSpecialId(field) + var type = isSpecialId(ctx, field) ? refer('int') : convertTypeReference(field.type); @@ -584,7 +594,8 @@ class OrmGenerator extends GeneratorForAnnotation { var args = {}; for (var field in ctx.effectiveFields) { - if (isSpecialId(field) || field is RelationFieldImpl) continue; + if (isSpecialId(ctx, field) || field is RelationFieldImpl) + continue; args[ctx.buildContext.resolveFieldName(field.name)] = refer('model').property(field.name); } diff --git a/angel_orm_generator/test/has_one_test.dart b/angel_orm_generator/test/has_one_test.dart index 9ab6d798..37132bca 100644 --- a/angel_orm_generator/test/has_one_test.dart +++ b/angel_orm_generator/test/has_one_test.dart @@ -1,7 +1,6 @@ /// Tests for @hasOne... library angel_orm_generator.test.has_one_test; -import 'package:angel_orm/angel_orm.dart'; import 'package:test/test.dart'; import 'models/foot.dart'; import 'models/leg.dart'; diff --git a/angel_orm_generator/test/models/author.dart b/angel_orm_generator/test/models/author.dart index 58042ec0..311cdc89 100644 --- a/angel_orm_generator/test/models/author.dart +++ b/angel_orm_generator/test/models/author.dart @@ -10,6 +10,6 @@ part 'author.g.dart'; @orm abstract class _Author extends Model { @Column(length: 255, indexType: IndexType.unique) - @DefaultValue('Tobe Osakwe') + @SerializableField(defaultValue: 'Tobe Osakwe') String get name; } diff --git a/angel_orm_generator/test/models/has_car.dart b/angel_orm_generator/test/models/has_car.dart new file mode 100644 index 00000000..7b296828 --- /dev/null +++ b/angel_orm_generator/test/models/has_car.dart @@ -0,0 +1,12 @@ +import 'package:angel_migration/angel_migration.dart'; +import 'package:angel_model/angel_model.dart'; +import 'package:angel_orm/angel_orm.dart'; +import 'package:angel_serialize/angel_serialize.dart'; +import 'car.dart'; +// part 'has_car.g.dart'; + +@orm +@serializable +abstract class _PackageJson extends Model { + Car get car; +} diff --git a/angel_orm_generator/test/models/has_map.dart b/angel_orm_generator/test/models/has_map.dart new file mode 100644 index 00000000..8eb37822 --- /dev/null +++ b/angel_orm_generator/test/models/has_map.dart @@ -0,0 +1,12 @@ +import 'package:angel_migration/angel_migration.dart'; +import 'package:angel_model/angel_model.dart'; +import 'package:angel_orm/angel_orm.dart'; +import 'package:angel_serialize/angel_serialize.dart'; +import 'package:collection/collection.dart'; +part 'has_map.g.dart'; + +@orm +@serializable +abstract class _HasMap { + Map get value; +} diff --git a/angel_orm_generator/test/models/has_map.g.dart b/angel_orm_generator/test/models/has_map.g.dart new file mode 100644 index 00000000..ad82bf46 --- /dev/null +++ b/angel_orm_generator/test/models/has_map.g.dart @@ -0,0 +1,148 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'has_map.dart'; + +// ************************************************************************** +// MigrationGenerator +// ************************************************************************** + +class HasMapMigration extends Migration { + @override + up(Schema schema) { + schema.create('has_maps', (table) { + table.declare('value', new ColumnType('jsonb')); + }); + } + + @override + down(Schema schema) { + schema.drop('has_maps'); + } +} + +// ************************************************************************** +// OrmGenerator +// ************************************************************************** + +class HasMapQuery extends Query { + HasMapQuery() { + _where = new HasMapQueryWhere(this); + } + + @override + final HasMapQueryValues values = new HasMapQueryValues(); + + HasMapQueryWhere _where; + + @override + get tableName { + return 'has_maps'; + } + + @override + get fields { + return const ['value']; + } + + @override + HasMapQueryWhere get where { + return _where; + } + + @override + HasMapQueryWhere newWhereClause() { + return new HasMapQueryWhere(this); + } + + static HasMap parseRow(List row) { + if (row.every((x) => x == null)) return null; + var model = new HasMap(value: (row[0] as Map)); + return model; + } + + @override + deserialize(List row) { + return parseRow(row); + } +} + +class HasMapQueryWhere extends QueryWhere { + HasMapQueryWhere(HasMapQuery query) + : value = new MapSqlExpressionBuilder(query, 'value'); + + final MapSqlExpressionBuilder value; + + @override + get expressionBuilders { + return [value]; + } +} + +class HasMapQueryValues extends MapQueryValues { + Map get value { + return (values['value'] as Map); + } + + set value(Map value) => values['value'] = value; + void copyFrom(HasMap model) { + values.addAll({'value': model.value}); + } +} + +// ************************************************************************** +// JsonModelGenerator +// ************************************************************************** + +@generatedSerializable +class HasMap implements _HasMap { + const HasMap({Map this.value}); + + @override + final Map value; + + HasMap copyWith({Map value}) { + return new HasMap(value: value ?? this.value); + } + + bool operator ==(other) { + return other is _HasMap && + const MapEquality( + keys: const DefaultEquality(), values: const DefaultEquality()) + .equals(other.value, value); + } + + @override + int get hashCode { + return hashObjects([value]); + } + + Map toJson() { + return HasMapSerializer.toMap(this); + } +} + +// ************************************************************************** +// SerializerGenerator +// ************************************************************************** + +abstract class HasMapSerializer { + static HasMap fromMap(Map map) { + return new HasMap( + value: map['value'] is Map + ? (map['value'] as Map).cast() + : null); + } + + static Map toMap(_HasMap model) { + if (model == null) { + return null; + } + return {'value': model.value}; + } +} + +abstract class HasMapFields { + static const List allFields = const [value]; + + static const String value = 'value'; +}