diff --git a/angel_orm_generator/build.yaml b/angel_orm_generator/build.yaml index 9aef2e06..97b7656b 100644 --- a/angel_orm_generator/build.yaml +++ b/angel_orm_generator/build.yaml @@ -33,7 +33,7 @@ targets: - :_standalone sources: - test/models/book.dart - # - test/models/has_car.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/orm_build_context.dart b/angel_orm_generator/lib/src/orm_build_context.dart index 3746207b..ddabcccc 100644 --- a/angel_orm_generator/lib/src/orm_build_context.dart +++ b/angel_orm_generator/lib/src/orm_build_context.dart @@ -217,6 +217,7 @@ ColumnType inferColumnType(DartType type) { return ColumnType.timeStamp; if (const TypeChecker.fromRuntime(Map).isAssignableFromType(type)) return ColumnType.jsonb; + if (type is InterfaceType && type.element.isEnum) return ColumnType.int; return null; } diff --git a/angel_orm_generator/lib/src/orm_generator.dart b/angel_orm_generator/lib/src/orm_generator.dart index 5e5dce62..c93e5862 100644 --- a/angel_orm_generator/lib/src/orm_generator.dart +++ b/angel_orm_generator/lib/src/orm_generator.dart @@ -145,6 +145,7 @@ class OrmGenerator extends GeneratorForAnnotation { var args = {}; for (var field in ctx.effectiveFields) { + var fType = field.type; Reference type = convertTypeReference(field.type); if (isSpecialId(ctx, field)) type = refer('int'); @@ -157,6 +158,8 @@ class OrmGenerator extends GeneratorForAnnotation { expr = refer('json') .property('decode') .call([expr.asA(refer('String'))]).asA(type); + } else if (fType is InterfaceType && fType.element.isEnum) { + expr = type.property('values').index(expr.asA(refer('int'))); } else expr = expr.asA(type); @@ -481,6 +484,7 @@ class OrmGenerator extends GeneratorForAnnotation { // Add builders for each field for (var field in ctx.effectiveFields) { var name = field.name; + var args = []; DartType type; Reference builderType; @@ -496,6 +500,11 @@ class OrmGenerator extends GeneratorForAnnotation { builderType = new TypeReference((b) => b ..symbol = 'NumericSqlExpressionBuilder' ..types.add(refer(isSpecialId(ctx, field) ? 'int' : type.name))); + } else if (type is InterfaceType && type.element.isEnum) { + builderType = new TypeReference((b) => b + ..symbol = 'EnumSqlExpressionBuilder' + ..types.add(convertTypeReference(type))); + args.add(CodeExpression(Code('(v) => v.index'))); } else if (const TypeChecker.fromRuntime(String).isExactlyType(type)) { builderType = refer('StringSqlExpressionBuilder'); } else if (const TypeChecker.fromRuntime(bool).isExactlyType(type)) { @@ -532,7 +541,7 @@ class OrmGenerator extends GeneratorForAnnotation { .assign(builderType.newInstance([ refer('query'), literalString(ctx.buildContext.resolveFieldName(field.name)) - ])) + ].followedBy(args))) .code, ); })); @@ -558,31 +567,45 @@ class OrmGenerator extends GeneratorForAnnotation { // Each field generates a getter for setter for (var field in ctx.effectiveFields) { + var fType = field.type; var name = ctx.buildContext.resolveFieldName(field.name); var type = isSpecialId(ctx, field) ? refer('int') : convertTypeReference(field.type); clazz.methods.add(new Method((b) { + var value = refer('values').index(literalString(name)); + + if (fType is InterfaceType && fType.element.isEnum) { + var asInt = value.asA(refer('int')); + var t = convertTypeReference(fType); + value = t.property('values').index(asInt); + } else { + value = value.asA(type); + } + b ..name = field.name ..type = MethodType.getter ..returns = type - ..body = new Block((b) => b.addExpression( - refer('values').index(literalString(name)).asA(type).returned)); + ..body = new Block((b) => b.addExpression(value.returned)); })); clazz.methods.add(new Method((b) { + Expression value = refer('value'); + + if (fType is InterfaceType && fType.element.isEnum) { + value = value.property('index'); + } + b ..name = field.name ..type = MethodType.setter ..requiredParameters.add(new Parameter((b) => b ..name = 'value' ..type = type)) - ..body = refer('values') - .index(literalString(name)) - .assign(refer('value')) - .code; + ..body = + refer('values').index(literalString(name)).assign(value).code; })); } diff --git a/angel_orm_generator/test/enum_and_nested_test.dart b/angel_orm_generator/test/enum_and_nested_test.dart new file mode 100644 index 00000000..b9658a93 --- /dev/null +++ b/angel_orm_generator/test/enum_and_nested_test.dart @@ -0,0 +1,36 @@ +import 'package:test/test.dart'; +import 'models/has_car.dart'; +import 'common.dart'; + +main() { + PostgresExecutor executor; + + setUp(() async { + executor = await connectToPostgres(['has_car']); + }); + + test('insert', () async { + var query = HasCarQuery()..values.type = CarType.sedan; + var result = await query.insert(executor); + expect(result.type, CarType.sedan); + }); + + group('query', () { + HasCar initialValue; + + setUp(() async { + var query = HasCarQuery(); + query.values.type = CarType.sedan; + initialValue = await query.insert(executor); + }); + + test('query by enum', () async { + // Check for mismatched type + var query = HasCarQuery()..where.type.equals(CarType.atv); + expect(await query.get(executor), isEmpty); + + query = HasCarQuery()..where.type.equals(initialValue.type); + expect(await query.getOne(executor), initialValue); + }); + }); +} diff --git a/angel_orm_generator/test/migrations/has_car.sql b/angel_orm_generator/test/migrations/has_car.sql new file mode 100644 index 00000000..67794aed --- /dev/null +++ b/angel_orm_generator/test/migrations/has_car.sql @@ -0,0 +1,6 @@ +CREATE TEMPORARY TABLE "has_cars" ( + id serial PRIMARY KEY, + type int not null, + created_at timestamp, + updated_at timestamp +); \ No newline at end of file diff --git a/angel_orm_generator/test/models/has_car.dart b/angel_orm_generator/test/models/has_car.dart index 7b296828..a8c83c24 100644 --- a/angel_orm_generator/test/models/has_car.dart +++ b/angel_orm_generator/test/models/has_car.dart @@ -2,11 +2,24 @@ 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:meta/meta.dart'; import 'car.dart'; -// part 'has_car.g.dart'; +part 'has_car.g.dart'; + +Map _carToMap(Car car) => car.toJson(); + +Car _carFromMap(map) => CarSerializer.fromMap(map as Map); + +enum CarType { sedan, suv, atv } @orm @serializable -abstract class _PackageJson extends Model { - Car get car; +abstract class _HasCar extends Model { + // TODO: Do this without explicit serializers + // @SerializableField( + // serializesTo: Map, serializer: #_carToMap, deserializer: #_carFromMap) + // Car get car; + + @SerializableField(isNullable: false) + CarType get type; } diff --git a/angel_orm_generator/test/models/has_car.g.dart b/angel_orm_generator/test/models/has_car.g.dart new file mode 100644 index 00000000..ed539de2 --- /dev/null +++ b/angel_orm_generator/test/models/has_car.g.dart @@ -0,0 +1,234 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'has_car.dart'; + +// ************************************************************************** +// MigrationGenerator +// ************************************************************************** + +class HasCarMigration extends Migration { + @override + up(Schema schema) { + schema.create('has_cars', (table) { + table.serial('id')..primaryKey(); + table.integer('type'); + table.timeStamp('created_at'); + table.timeStamp('updated_at'); + }); + } + + @override + down(Schema schema) { + schema.drop('has_cars'); + } +} + +// ************************************************************************** +// OrmGenerator +// ************************************************************************** + +class HasCarQuery extends Query { + HasCarQuery() { + _where = new HasCarQueryWhere(this); + } + + @override + final HasCarQueryValues values = new HasCarQueryValues(); + + HasCarQueryWhere _where; + + @override + get tableName { + return 'has_cars'; + } + + @override + get fields { + return const ['id', 'type', 'created_at', 'updated_at']; + } + + @override + HasCarQueryWhere get where { + return _where; + } + + @override + HasCarQueryWhere newWhereClause() { + return new HasCarQueryWhere(this); + } + + static HasCar parseRow(List row) { + if (row.every((x) => x == null)) return null; + var model = new HasCar( + id: row[0].toString(), + type: CarType.values[(row[1] as int)], + createdAt: (row[2] as DateTime), + updatedAt: (row[3] as DateTime)); + return model; + } + + @override + deserialize(List row) { + return parseRow(row); + } +} + +class HasCarQueryWhere extends QueryWhere { + HasCarQueryWhere(HasCarQuery query) + : id = new NumericSqlExpressionBuilder(query, 'id'), + type = new EnumSqlExpressionBuilder( + query, 'type', (v) => v.index), + createdAt = new DateTimeSqlExpressionBuilder(query, 'created_at'), + updatedAt = new DateTimeSqlExpressionBuilder(query, 'updated_at'); + + final NumericSqlExpressionBuilder id; + + final EnumSqlExpressionBuilder type; + + final DateTimeSqlExpressionBuilder createdAt; + + final DateTimeSqlExpressionBuilder updatedAt; + + @override + get expressionBuilders { + return [id, type, createdAt, updatedAt]; + } +} + +class HasCarQueryValues extends MapQueryValues { + int get id { + return (values['id'] as int); + } + + set id(int value) => values['id'] = value; + CarType get type { + return CarType.values[(values['type'] as int)]; + } + + set type(CarType value) => values['type'] = value.index; + DateTime get createdAt { + return (values['created_at'] as DateTime); + } + + set createdAt(DateTime value) => values['created_at'] = value; + DateTime get updatedAt { + return (values['updated_at'] as DateTime); + } + + set updatedAt(DateTime value) => values['updated_at'] = value; + void copyFrom(HasCar model) { + values.addAll({ + 'type': model.type, + 'created_at': model.createdAt, + 'updated_at': model.updatedAt + }); + } +} + +// ************************************************************************** +// JsonModelGenerator +// ************************************************************************** + +@generatedSerializable +class HasCar extends _HasCar { + HasCar({this.id, @required this.type, this.createdAt, this.updatedAt}); + + @override + final String id; + + @override + final CarType type; + + @override + final DateTime createdAt; + + @override + final DateTime updatedAt; + + HasCar copyWith( + {String id, CarType type, DateTime createdAt, DateTime updatedAt}) { + return new HasCar( + id: id ?? this.id, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt); + } + + bool operator ==(other) { + return other is _HasCar && + other.id == id && + other.type == type && + other.createdAt == createdAt && + other.updatedAt == updatedAt; + } + + @override + int get hashCode { + return hashObjects([id, type, createdAt, updatedAt]); + } + + Map toJson() { + return HasCarSerializer.toMap(this); + } +} + +// ************************************************************************** +// SerializerGenerator +// ************************************************************************** + +abstract class HasCarSerializer { + static HasCar fromMap(Map map) { + if (map['type'] == null) { + throw new FormatException("Missing required field 'type' on HasCar."); + } + + return new HasCar( + id: map['id'] as String, + type: map['type'] is CarType + ? (map['type'] as CarType) + : (map['type'] is int ? CarType.values[map['type'] as int] : null), + createdAt: map['created_at'] != null + ? (map['created_at'] is DateTime + ? (map['created_at'] as DateTime) + : DateTime.parse(map['created_at'].toString())) + : null, + updatedAt: map['updated_at'] != null + ? (map['updated_at'] is DateTime + ? (map['updated_at'] as DateTime) + : DateTime.parse(map['updated_at'].toString())) + : null); + } + + static Map toMap(_HasCar model) { + if (model == null) { + return null; + } + if (model.type == null) { + throw new FormatException("Missing required field 'type' on HasCar."); + } + + return { + 'id': model.id, + 'type': model.type == null ? null : CarType.values.indexOf(model.type), + 'created_at': model.createdAt?.toIso8601String(), + 'updated_at': model.updatedAt?.toIso8601String() + }; + } +} + +abstract class HasCarFields { + static const List allFields = const [ + id, + type, + createdAt, + updatedAt + ]; + + static const String id = 'id'; + + static const String type = 'type'; + + static const String createdAt = 'created_at'; + + static const String updatedAt = 'updated_at'; +} diff --git a/angel_orm_generator/test/models/order.dart b/angel_orm_generator/test/models/order.dart index 1403aaf1..ebd3bc03 100644 --- a/angel_orm_generator/test/models/order.dart +++ b/angel_orm_generator/test/models/order.dart @@ -9,13 +9,13 @@ part 'order.g.dart'; @orm @serializable -class _Order extends Model { - @Join(Customer, CustomerFields.id) - int customerId; +abstract class _Order extends Model { + @belongsTo + Customer get customer; - int employeeId; + int get employeeId; - DateTime orderDate; + DateTime get orderDate; - int shipperId; + int get shipperId; } diff --git a/angel_orm_generator/test/models/order.g.dart b/angel_orm_generator/test/models/order.g.dart index ba0ad236..9112dd0f 100644 --- a/angel_orm_generator/test/models/order.g.dart +++ b/angel_orm_generator/test/models/order.g.dart @@ -11,12 +11,12 @@ class OrderMigration extends Migration { up(Schema schema) { schema.create('orders', (table) { table.serial('id')..primaryKey(); - table.integer('customer_id'); table.integer('employee_id'); table.timeStamp('order_date'); table.integer('shipper_id'); table.timeStamp('created_at'); table.timeStamp('updated_at'); + table.integer('customer_id').references('customers', 'id'); }); } @@ -33,6 +33,8 @@ class OrderMigration extends Migration { class OrderQuery extends Query { OrderQuery() { _where = new OrderQueryWhere(this); + leftJoin('customers', 'customer_id', 'id', + additionalFields: const ['created_at', 'updated_at']); } @override @@ -72,12 +74,15 @@ class OrderQuery extends Query { if (row.every((x) => x == null)) return null; var model = new Order( id: row[0].toString(), - customerId: (row[1] as int), employeeId: (row[2] as int), orderDate: (row[3] as DateTime), shipperId: (row[4] as int), createdAt: (row[5] as DateTime), updatedAt: (row[6] as DateTime)); + if (row.length > 7) { + model = model.copyWith( + customer: CustomerQuery.parseRow(row.skip(7).toList())); + } return model; } @@ -163,13 +168,15 @@ class OrderQueryValues extends MapQueryValues { set updatedAt(DateTime value) => values['updated_at'] = value; void copyFrom(Order model) { values.addAll({ - 'customer_id': model.customerId, 'employee_id': model.employeeId, 'order_date': model.orderDate, 'shipper_id': model.shipperId, 'created_at': model.createdAt, 'updated_at': model.updatedAt }); + if (model.customer != null) { + values['customer_id'] = int.parse(model.customer.id); + } } } @@ -181,7 +188,7 @@ class OrderQueryValues extends MapQueryValues { class Order extends _Order { Order( {this.id, - this.customerId, + this.customer, this.employeeId, this.orderDate, this.shipperId, @@ -192,7 +199,7 @@ class Order extends _Order { final String id; @override - final int customerId; + final Customer customer; @override final int employeeId; @@ -211,7 +218,7 @@ class Order extends _Order { Order copyWith( {String id, - int customerId, + Customer customer, int employeeId, DateTime orderDate, int shipperId, @@ -219,7 +226,7 @@ class Order extends _Order { DateTime updatedAt}) { return new Order( id: id ?? this.id, - customerId: customerId ?? this.customerId, + customer: customer ?? this.customer, employeeId: employeeId ?? this.employeeId, orderDate: orderDate ?? this.orderDate, shipperId: shipperId ?? this.shipperId, @@ -230,7 +237,7 @@ class Order extends _Order { bool operator ==(other) { return other is _Order && other.id == id && - other.customerId == customerId && + other.customer == customer && other.employeeId == employeeId && other.orderDate == orderDate && other.shipperId == shipperId && @@ -240,15 +247,8 @@ class Order extends _Order { @override int get hashCode { - return hashObjects([ - id, - customerId, - employeeId, - orderDate, - shipperId, - createdAt, - updatedAt - ]); + return hashObjects( + [id, customer, employeeId, orderDate, shipperId, createdAt, updatedAt]); } Map toJson() { @@ -264,7 +264,9 @@ abstract class OrderSerializer { static Order fromMap(Map map) { return new Order( id: map['id'] as String, - customerId: map['customer_id'] as int, + customer: map['customer'] != null + ? CustomerSerializer.fromMap(map['customer'] as Map) + : null, employeeId: map['employee_id'] as int, orderDate: map['order_date'] != null ? (map['order_date'] is DateTime @@ -290,7 +292,7 @@ abstract class OrderSerializer { } return { 'id': model.id, - 'customer_id': model.customerId, + 'customer': CustomerSerializer.toMap(model.customer), 'employee_id': model.employeeId, 'order_date': model.orderDate?.toIso8601String(), 'shipper_id': model.shipperId, @@ -303,7 +305,7 @@ abstract class OrderSerializer { abstract class OrderFields { static const List allFields = const [ id, - customerId, + customer, employeeId, orderDate, shipperId, @@ -313,7 +315,7 @@ abstract class OrderFields { static const String id = 'id'; - static const String customerId = 'customer_id'; + static const String customer = 'customer'; static const String employeeId = 'employee_id';