From 78cd1086c9c347e39b8857c8b537ca931647c893 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 2 Apr 2019 19:05:13 -0400 Subject: [PATCH] Got many to many --- angel_orm/lib/src/query.dart | 85 ++++-- angel_orm/lib/src/relations.dart | 8 +- .../lib/src/orm_build_context.dart | 23 +- .../lib/src/orm_generator.dart | 52 +++- angel_orm_generator/lib/src/readers.dart | 25 ++ angel_orm_generator/pubspec.yaml | 6 +- angel_orm_generator/test/common.dart | 13 +- .../test/many_to_many_test.dart | 65 +++- angel_orm_generator/test/models/book.g.dart | 6 +- angel_orm_generator/test/models/leg.g.dart | 16 +- angel_orm_generator/test/models/order.g.dart | 3 +- angel_orm_generator/test/models/tree.g.dart | 3 +- angel_orm_generator/test/models/user.dart | 6 +- angel_orm_generator/test/models/user.g.dart | 288 +++++++++++------- 14 files changed, 419 insertions(+), 180 deletions(-) diff --git a/angel_orm/lib/src/query.dart b/angel_orm/lib/src/query.dart index 5cbb041b..5f4dc7ff 100644 --- a/angel_orm/lib/src/query.dart +++ b/angel_orm/lib/src/query.dart @@ -146,6 +146,11 @@ abstract class Query extends QueryBase { 'This instance does not support creating new WHERE clauses.'); } + /// Determines whether this query can be compiled. + /// + /// Used to prevent ambiguities in joins. + bool canCompile(Set trampoline) => true; + /// Shorthand for calling [where].or with a new [Where] clause. void andWhere(void Function(Where) f) { var w = newWhereClause(); @@ -192,17 +197,55 @@ abstract class Query extends QueryBase { _crossJoin = tableName; } - String _joinAlias() => 'a${_joins.length}'; + String _joinAlias(Set trampoline) { + int i = _joins.length; + + while (true) { + var a = 'a$i'; + if (trampoline.add(a)) { + return a; + } else + i++; + } + } String _compileJoin(tableName, Set trampoline) { - if (tableName is String) return tableName; - if (tableName is Query) { + if (tableName is String) + return tableName; + else if (tableName is Query) { var c = tableName.compile(trampoline); if (c == null) return c; return '($c)'; + } else { + throw ArgumentError.value( + tableName, 'tableName', 'must be a String or Query'); + } + } + + void _makeJoin( + tableName, + Set trampoline, + JoinType type, + String localKey, + String foreignKey, + String op, + List additionalFields) { + trampoline ??= Set(); + + // Pivot tables guard against ambiguous fields by excluding tables + // that have already been queried in this scope. + if (trampoline.contains(tableName) && trampoline.contains(this.tableName)) { + // ex. if we have {roles, role_users}, then don't join "roles" again. + return; + } + + var to = _compileJoin(tableName, trampoline); + if (to != null) { + _joins.add(new JoinBuilder(type, this, to, localKey, foreignKey, + op: op, + alias: _joinAlias(trampoline), + additionalFields: additionalFields)); } - throw ArgumentError.value( - tableName, 'tableName', 'must be a String or Query'); } /// Execute an `INNER JOIN` against another table. @@ -210,9 +253,8 @@ abstract class Query extends QueryBase { {String op: '=', List additionalFields: const [], Set trampoline}) { - _joins.add(new JoinBuilder(JoinType.inner, this, - _compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, - op: op, alias: _joinAlias(), additionalFields: additionalFields)); + _makeJoin(tableName, trampoline, JoinType.inner, localKey, foreignKey, op, + additionalFields); } /// Execute a `LEFT JOIN` against another table. @@ -220,9 +262,8 @@ abstract class Query extends QueryBase { {String op: '=', List additionalFields: const [], Set trampoline}) { - _joins.add(new JoinBuilder(JoinType.left, this, - _compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, - op: op, alias: _joinAlias(), additionalFields: additionalFields)); + _makeJoin(tableName, trampoline, JoinType.left, localKey, foreignKey, op, + additionalFields); } /// Execute a `RIGHT JOIN` against another table. @@ -230,9 +271,8 @@ abstract class Query extends QueryBase { {String op: '=', List additionalFields: const [], Set trampoline}) { - _joins.add(new JoinBuilder(JoinType.right, this, - _compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, - op: op, alias: _joinAlias(), additionalFields: additionalFields)); + _makeJoin(tableName, trampoline, JoinType.right, localKey, foreignKey, op, + additionalFields); } /// Execute a `FULL OUTER JOIN` against another table. @@ -240,9 +280,8 @@ abstract class Query extends QueryBase { {String op: '=', List additionalFields: const [], Set trampoline}) { - _joins.add(new JoinBuilder(JoinType.full, this, - _compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, - op: op, alias: _joinAlias(), additionalFields: additionalFields)); + _makeJoin(tableName, trampoline, JoinType.full, localKey, foreignKey, op, + additionalFields); } /// Execute a `SELF JOIN`. @@ -250,9 +289,8 @@ abstract class Query extends QueryBase { {String op: '=', List additionalFields: const [], Set trampoline}) { - _joins.add(new JoinBuilder(JoinType.self, this, - _compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, - op: op, alias: _joinAlias(), additionalFields: additionalFields)); + _makeJoin(tableName, trampoline, JoinType.self, localKey, foreignKey, op, + additionalFields); } @override @@ -261,7 +299,8 @@ abstract class Query extends QueryBase { String preamble, bool withFields: true, String fromQuery}) { - if (!trampoline.add(tableName)) { + // One table MAY appear multiple times in a query. + if (!canCompile(trampoline)) { return null; } @@ -294,7 +333,7 @@ abstract class Query extends QueryBase { if (preamble == null) { if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin'); for (var join in _joins) { - var c = join.compile(); + var c = join.compile(trampoline); if (c != null) b.write(' $c'); } } @@ -565,7 +604,7 @@ class JoinBuilder { return right; } - String compile() { + String compile(Set trampoline) { if (to == null) return null; var b = new StringBuffer(); var left = '${from.tableName}.$key'; diff --git a/angel_orm/lib/src/relations.dart b/angel_orm/lib/src/relations.dart index 6f6be8f7..585018a5 100644 --- a/angel_orm/lib/src/relations.dart +++ b/angel_orm/lib/src/relations.dart @@ -60,16 +60,16 @@ class BelongsTo extends Relationship { const BelongsTo belongsTo = const BelongsTo(); class ManyToMany extends Relationship { - const ManyToMany( + final Type through; + + const ManyToMany(this.through, {String localKey: 'id', String foreignKey, String foreignTable, bool cascadeOnDelete: false}) - : super(RelationshipType.manyToMany, + : super(RelationshipType.hasMany, // Many-to-Many is actually just a hasMany localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable, cascadeOnDelete: cascadeOnDelete == true); } - -const ManyToMany manyToMany = const ManyToMany(); diff --git a/angel_orm_generator/lib/src/orm_build_context.dart b/angel_orm_generator/lib/src/orm_build_context.dart index aa9606f0..0aaf62cb 100644 --- a/angel_orm_generator/lib/src/orm_build_context.dart +++ b/angel_orm_generator/lib/src/orm_build_context.dart @@ -105,7 +105,8 @@ Future buildOrmContext( var foreignKey = cr.peek('foreignKey')?.stringValue; var foreignTable = cr.peek('foreignTable')?.stringValue; var cascadeOnDelete = cr.peek('cascadeOnDelete')?.boolValue == true; - OrmBuildContext foreign; + var through = cr.peek('through')?.typeValue; + OrmBuildContext foreign, throughContext; if (foreignTable == null) { // if (!isModelClass(field.type) && @@ -137,6 +138,17 @@ Future buildOrmContext( resolver, autoSnakeCaseNames); + // Resolve throughType as well + if (through != null && through is InterfaceType) { + throughContext = await buildOrmContext( + through.element, + new ConstantReader(const TypeChecker.fromRuntime(Serializable) + .firstAnnotationOf(modelType.element)), + buildStep, + resolver, + autoSnakeCaseNames); + } + var ormAnn = const TypeChecker.fromRuntime(Orm) .firstAnnotationOf(modelType.element); @@ -164,12 +176,15 @@ Future buildOrmContext( foreignKey ??= 'id'; } - var relation = new Relationship( + var relation = new RelationshipReader( type, localKey: localKey, foreignKey: foreignKey, foreignTable: foreignTable, cascadeOnDelete: cascadeOnDelete, + through: through, + foreign: foreign, + throughContext: throughContext, ); if (relation.type == RelationshipType.belongsTo) { @@ -189,7 +204,6 @@ Future buildOrmContext( } ctx.relations[field.name] = relation; - ctx.relationTypes[relation] = foreign; } else { if (column?.type == null) throw 'Cannot infer SQL column type for field "${ctx.buildContext.originalClassName}.${field.name}" with type "${field.type.displayName}".'; @@ -256,8 +270,7 @@ class OrmBuildContext { final Map columns = {}; final List effectiveFields = []; - final Map relations = {}; - final Map relationTypes = {}; + final Map relations = {}; OrmBuildContext(this.buildContext, this.ormAnnotation, this.tableName); } diff --git a/angel_orm_generator/lib/src/orm_generator.dart b/angel_orm_generator/lib/src/orm_generator.dart index 2ec8b95d..e294d9f5 100644 --- a/angel_orm_generator/lib/src/orm_generator.dart +++ b/angel_orm_generator/lib/src/orm_generator.dart @@ -211,7 +211,7 @@ class OrmGenerator extends GeneratorForAnnotation { RelationshipType.belongsTo, RelationshipType.hasMany ].contains(relation.type)) return; - var foreign = ctx.relationTypes[relation]; + var foreign = relation.foreign; var skipToList = refer('row') .property('skip') .call([literalNum(i)]) @@ -234,7 +234,7 @@ class OrmGenerator extends GeneratorForAnnotation { var blockStr = block.accept(new DartEmitter()); var ifStr = 'if (row.length > $i) { $blockStr }'; b.statements.add(new Code(ifStr)); - i += ctx.relationTypes[relation].effectiveFields.length; + i += relation.foreign.effectiveFields.length; }); b.addExpression(refer('model').returned); @@ -279,11 +279,14 @@ class OrmGenerator extends GeneratorForAnnotation { if (relation.type == RelationshipType.belongsTo || relation.type == RelationshipType.hasOne || relation.type == RelationshipType.hasMany) { - var foreign = ctx.relationTypes[relation]; - var additionalFields = foreign.effectiveFields + var foreign = relation.throughContext ?? relation.foreign; + + // If this is a many-to-many, add the fields from the other object. + var additionalFields = relation.foreign.effectiveFields // .where((f) => f.name != 'id' || !isSpecialId(ctx, f)) - .map((f) => literalString( - foreign.buildContext.resolveFieldName(f.name))); + .map((f) => literalString(relation.foreign.buildContext + .resolveFieldName(f.name))); + var joinArgs = [relation.localKey, relation.foreignKey] .map(literalString) .toList(); @@ -303,13 +306,43 @@ class OrmGenerator extends GeneratorForAnnotation { b.addExpression(refer('leftJoin').call(joinArgs, { 'additionalFields': - literalConstList(additionalFields.toList()) + literalConstList(additionalFields.toList()), + 'trampoline': refer('trampoline'), })); } }); }); })); + // If we have any many-to-many relations, we need to prevent + // fetching this table within their joins. + var manyToMany = ctx.relations.entries.where((e) => e.value.isManyToMany); + + if (manyToMany.isNotEmpty) { + var outExprs = manyToMany.map((e) { + var foreignTableName = e.value.throughContext.tableName; + return CodeExpression(Code(''' + (!( + trampoline.contains('${ctx.tableName}') + && trampoline.contains('$foreignTableName') + )) + ''')); + }); + var out = outExprs.reduce((a, b) => a.and(b)); + + clazz.methods.add(new Method((b) { + b + ..name = 'canCompile' + ..annotations.add(refer('override')) + ..requiredParameters + .add(new Parameter((b) => b..name = 'trampoline')) + ..returns = refer('bool') + ..body = Block((b) { + b.addExpression(out.returned); + }); + })); + } + // TODO: Ultimately remove the insert override if (false && ctx.relations.isNotEmpty) { clazz.methods.add(new Method((b) { @@ -391,10 +424,11 @@ class OrmGenerator extends GeneratorForAnnotation { var args = {}; ctx.relations.forEach((name, relation) { - if (false && relation.type == RelationshipType.hasMany) { + // TODO: Should this be entirely removed? + if (relation.type == RelationshipType.hasMany) { // For each hasMany, we need to create a query of // the corresponding type. - var foreign = ctx.relationTypes[relation]; + var foreign = relation.foreign; var queryType = refer( '${foreign.buildContext.modelClassNameRecase.pascalCase}Query'); var queryInstance = queryType.newInstance([]); diff --git a/angel_orm_generator/lib/src/readers.dart b/angel_orm_generator/lib/src/readers.dart index 00d5c18d..b27c1d74 100644 --- a/angel_orm_generator/lib/src/readers.dart +++ b/angel_orm_generator/lib/src/readers.dart @@ -1,6 +1,8 @@ import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:angel_orm/angel_orm.dart'; import 'package:source_gen/source_gen.dart'; +import 'orm_build_context.dart'; const TypeChecker columnTypeChecker = const TypeChecker.fromRuntime(Column); @@ -19,3 +21,26 @@ class ColumnReader { DartObject get defaultValue => reader.peek('defaultValue')?.objectValue; } + +class RelationshipReader { + final int type; + final String localKey; + final String foreignKey; + final String foreignTable; + final bool cascadeOnDelete; + final DartType through; + final OrmBuildContext foreign; + final OrmBuildContext throughContext; + + const RelationshipReader(this.type, + {this.localKey, + this.foreignKey, + this.foreignTable, + this.cascadeOnDelete, + this.through, + this.foreign, + this.throughContext}); + + bool get isManyToMany => + type == RelationshipType.hasMany && throughContext != null; +} diff --git a/angel_orm_generator/pubspec.yaml b/angel_orm_generator/pubspec.yaml index 136d5358..e0001369 100644 --- a/angel_orm_generator/pubspec.yaml +++ b/angel_orm_generator/pubspec.yaml @@ -35,6 +35,6 @@ dev_dependencies: collection: ^1.0.0 postgres: ^1.0.0 test: ^1.0.0 -# dependency_overrides: -# angel_orm: -# path: ../angel_orm +dependency_overrides: + angel_orm: + path: ../angel_orm diff --git a/angel_orm_generator/test/common.dart b/angel_orm_generator/test/common.dart index eea77a49..4f7843c3 100644 --- a/angel_orm_generator/test/common.dart +++ b/angel_orm_generator/test/common.dart @@ -25,7 +25,7 @@ class PostgresExecutor extends QueryExecutor { @override Future> query( String tableName, String query, Map substitutionValues, - [List returningFields]) { + [List returningFields]) async { if (returningFields != null) { var fields = returningFields.join(', '); var returning = 'RETURNING $fields'; @@ -37,7 +37,16 @@ class PostgresExecutor extends QueryExecutor { if (substitutionValues.isNotEmpty) print('Values: $substitutionValues'); print(substitutionValues.map((k, v) => MapEntry(k, v.runtimeType))); } - return connection.query(query, substitutionValues: substitutionValues); + + var rows = + await connection.query(query, substitutionValues: substitutionValues); + + if (!Platform.environment.containsKey('STFU')) { + print('Got ${rows.length} row(s):'); + rows.forEach(print); + } + + return rows; } @override diff --git a/angel_orm_generator/test/many_to_many_test.dart b/angel_orm_generator/test/many_to_many_test.dart index 0a82be59..32b4d665 100644 --- a/angel_orm_generator/test/many_to_many_test.dart +++ b/angel_orm_generator/test/many_to_many_test.dart @@ -1,45 +1,86 @@ library angel_orm_generator.test; import 'dart:async'; -import 'package:angel_orm/angel_orm.dart'; +import 'dart:io'; import 'package:test/test.dart'; import 'models/user.dart'; import 'common.dart'; main() { - QueryExecutor executor; + PostgresExecutor executor; Role canPub, canSub; User thosakwe; + Future dumpQuery(String query) async { + if (Platform.environment.containsKey('STFU')) return; + print('\n'); + print('=================================================='); + print(' DUMPING QUERY'); + print(query); + var rows = await executor.connection.query(query); + print('\n${rows.length} row(s):'); + rows.forEach((r) => print(' * $r')); + print('==================================================\n\n'); + } + setUp(() async { executor = await connectToPostgres(['user', 'role', 'user_role']); - var canPubQuery = new RoleQuery()..values.name = 'can_pub'; - var canSubQuery = new RoleQuery()..values.name = 'can_sub'; - canPub = await canPubQuery.insert(executor); - canSub = await canSubQuery.insert(executor); +// await dumpQuery(""" +// WITH roles as +// (INSERT INTO roles (name) +// VALUES ('pyt') +// RETURNING roles.id, roles.name, roles.created_at, roles.updated_at) +// SELECT +// roles.id, roles.name, roles.created_at, roles.updated_at +// FROM roles +// LEFT JOIN +// (SELECT +// role_users.role_id, role_users.user_id, +// a0.id, a0.username, a0.password, a0.email, a0.created_at, a0.updated_at +// FROM role_users +// LEFT JOIN +// users a0 ON role_users.user_id=a0.id) +// a1 ON roles.id=a1.role_id +// """); - var thosakweQuery = new UserQuery(); + var canPubQuery = RoleQuery()..values.name = 'can_pub'; + var canSubQuery = RoleQuery()..values.name = 'can_sub'; + canPub = await canPubQuery.insert(executor); + print('=== CANPUB: ${canPub?.toJson()}'); + // await dumpQuery(canPubQuery.compile(Set())); + canSub = await canSubQuery.insert(executor); + print('=== CANSUB: ${canSub?.toJson()}'); + + var thosakweQuery = UserQuery(); thosakweQuery.values ..username = 'thosakwe' ..password = 'Hahahahayoureallythoughtiwasstupidenoughtotypethishere' ..email = 'thosakwe AT gmail.com'; thosakwe = await thosakweQuery.insert(executor); + print('=== THOSAKWE: ${thosakwe?.toJson()}'); // Allow thosakwe to publish... - var thosakwePubQuery = new RoleUserQuery(); + var thosakwePubQuery = RoleUserQuery(); thosakwePubQuery.values ..userId = int.parse(thosakwe.id) ..roleId = int.parse(canPub.id); await thosakwePubQuery.insert(executor); // Allow thosakwe to subscribe... - var thosakweSubQuery = new RoleUserQuery(); + var thosakweSubQuery = RoleUserQuery(); thosakweSubQuery.values ..userId = int.parse(thosakwe.id) ..roleId = int.parse(canSub.id); await thosakweSubQuery.insert(executor); + // Print all users... + // await dumpQuery('select * from users;'); + // await dumpQuery('select * from roles;'); + // await dumpQuery('select * from role_users;'); + var query = RoleQuery()..where.id.equals(canPub.idAsInt); + await dumpQuery(query.compile(Set())); + print('\n'); print('=================================================='); print(' GOOD STUFF BEGINS HERE '); @@ -47,7 +88,7 @@ main() { }); Future fetchThosakwe() async { - var query = new UserQuery()..where.id.equals(int.parse(thosakwe.id)); + var query = UserQuery()..where.id.equals(int.parse(thosakwe.id)); return await query.getOne(executor); } @@ -60,9 +101,9 @@ main() { test('fetch users for role', () async { for (var role in [canPub, canSub]) { - var query = new RoleQuery()..where.id.equals(int.parse(role.id)); + var query = RoleQuery()..where.id.equals(role.idAsInt); var r = await query.getOne(executor); - expect(r.users, [thosakwe]); + expect(r.users.toList(), [thosakwe]); } }); } diff --git a/angel_orm_generator/test/models/book.g.dart b/angel_orm_generator/test/models/book.g.dart index b2136cef..aee9e4a0 100644 --- a/angel_orm_generator/test/models/book.g.dart +++ b/angel_orm_generator/test/models/book.g.dart @@ -35,9 +35,11 @@ class BookQuery extends Query { trampoline.add(tableName); _where = BookQueryWhere(this); leftJoin('authors', 'author_id', 'id', - additionalFields: const ['id', 'name', 'created_at', 'updated_at']); + additionalFields: const ['id', 'name', 'created_at', 'updated_at'], + trampoline: trampoline); leftJoin('authors', 'partner_author_id', 'id', - additionalFields: const ['id', 'name', 'created_at', 'updated_at']); + additionalFields: const ['id', 'name', 'created_at', 'updated_at'], + trampoline: trampoline); } @override diff --git a/angel_orm_generator/test/models/leg.g.dart b/angel_orm_generator/test/models/leg.g.dart index 14a91b01..734183b1 100644 --- a/angel_orm_generator/test/models/leg.g.dart +++ b/angel_orm_generator/test/models/leg.g.dart @@ -32,13 +32,15 @@ class LegQuery extends Query { trampoline ??= Set(); trampoline.add(tableName); _where = LegQueryWhere(this); - leftJoin('feet', 'id', 'leg_id', additionalFields: const [ - 'id', - 'leg_id', - 'n_toes', - 'created_at', - 'updated_at' - ]); + leftJoin('feet', 'id', 'leg_id', + additionalFields: const [ + 'id', + 'leg_id', + 'n_toes', + 'created_at', + 'updated_at' + ], + trampoline: trampoline); } @override diff --git a/angel_orm_generator/test/models/order.g.dart b/angel_orm_generator/test/models/order.g.dart index 3d7c2c9e..fe290ff7 100644 --- a/angel_orm_generator/test/models/order.g.dart +++ b/angel_orm_generator/test/models/order.g.dart @@ -36,7 +36,8 @@ class OrderQuery extends Query { trampoline.add(tableName); _where = OrderQueryWhere(this); leftJoin('customers', 'customer_id', 'id', - additionalFields: const ['id', 'created_at', 'updated_at']); + additionalFields: const ['id', 'created_at', 'updated_at'], + trampoline: trampoline); } @override diff --git a/angel_orm_generator/test/models/tree.g.dart b/angel_orm_generator/test/models/tree.g.dart index 3873006e..c672786d 100644 --- a/angel_orm_generator/test/models/tree.g.dart +++ b/angel_orm_generator/test/models/tree.g.dart @@ -39,7 +39,8 @@ class TreeQuery extends Query { 'common_name', 'created_at', 'updated_at' - ]); + ], + trampoline: trampoline); } @override diff --git a/angel_orm_generator/test/models/user.dart b/angel_orm_generator/test/models/user.dart index af714694..76cfa7e6 100644 --- a/angel_orm_generator/test/models/user.dart +++ b/angel_orm_generator/test/models/user.dart @@ -14,13 +14,13 @@ abstract class _User extends Model { String get password; String get email; - @manyToMany + @ManyToMany(_RoleUser) List<_Role> get roles; } @serializable @orm -abstract class _RoleUser extends Model { +abstract class _RoleUser { @belongsTo _Role get role; @@ -33,6 +33,6 @@ abstract class _RoleUser extends Model { abstract class _Role extends Model { String name; - @manyToMany + @ManyToMany(_RoleUser) List<_User> get users; } diff --git a/angel_orm_generator/test/models/user.g.dart b/angel_orm_generator/test/models/user.g.dart index ed008722..22c17b9f 100644 --- a/angel_orm_generator/test/models/user.g.dart +++ b/angel_orm_generator/test/models/user.g.dart @@ -29,9 +29,6 @@ class RoleUserMigration extends Migration { @override up(Schema schema) { schema.create('role_users', (table) { - table.serial('id')..primaryKey(); - table.timeStamp('created_at'); - table.timeStamp('updated_at'); table.integer('role_id').references('roles', 'id'); table.integer('user_id').references('users', 'id'); }); @@ -69,6 +66,9 @@ class UserQuery extends Query { trampoline ??= Set(); trampoline.add(tableName); _where = UserQueryWhere(this); + leftJoin(RoleUserQuery(trampoline: trampoline), 'id', 'user_id', + additionalFields: const ['id', 'name', 'created_at', 'updated_at'], + trampoline: trampoline); } @override @@ -117,6 +117,12 @@ class UserQuery extends Query { email: (row[3] as String), createdAt: (row[4] as DateTime), updatedAt: (row[5] as DateTime)); + if (row.length > 6) { + model = model.copyWith( + roles: [RoleQuery.parseRow(row.skip(6).toList())] + .where((x) => x != null) + .toList()); + } return model; } @@ -124,6 +130,69 @@ class UserQuery extends Query { deserialize(List row) { return parseRow(row); } + + @override + bool canCompile(trampoline) { + return (!(trampoline.contains('users') && + trampoline.contains('role_users'))); + } + + @override + get(QueryExecutor executor) { + return super.get(executor).then((result) { + return result.fold>([], (out, model) { + var idx = out.indexWhere((m) => m.id == model.id); + + if (idx == -1) { + return out..add(model); + } else { + var l = out[idx]; + return out + ..[idx] = l.copyWith( + roles: List<_Role>.from(l.roles ?? []) + ..addAll(model.roles ?? [])); + } + }); + }); + } + + @override + update(QueryExecutor executor) { + return super.update(executor).then((result) { + return result.fold>([], (out, model) { + var idx = out.indexWhere((m) => m.id == model.id); + + if (idx == -1) { + return out..add(model); + } else { + var l = out[idx]; + return out + ..[idx] = l.copyWith( + roles: List<_Role>.from(l.roles ?? []) + ..addAll(model.roles ?? [])); + } + }); + }); + } + + @override + delete(QueryExecutor executor) { + return super.delete(executor).then((result) { + return result.fold>([], (out, model) { + var idx = out.indexWhere((m) => m.id == model.id); + + if (idx == -1) { + return out..add(model); + } else { + var l = out[idx]; + return out + ..[idx] = l.copyWith( + roles: List<_Role>.from(l.roles ?? []) + ..addAll(model.roles ?? [])); + } + }); + }); + } } class UserQueryWhere extends QueryWhere { @@ -204,15 +273,18 @@ class RoleUserQuery extends Query { trampoline.add(tableName); _where = RoleUserQueryWhere(this); leftJoin('roles', 'role_id', 'id', - additionalFields: const ['id', 'name', 'created_at', 'updated_at']); - leftJoin('users', 'user_id', 'id', additionalFields: const [ - 'id', - 'username', - 'password', - 'email', - 'created_at', - 'updated_at' - ]); + additionalFields: const ['id', 'name', 'created_at', 'updated_at'], + trampoline: trampoline); + leftJoin('users', 'user_id', 'id', + additionalFields: const [ + 'id', + 'username', + 'password', + 'email', + 'created_at', + 'updated_at' + ], + trampoline: trampoline); } @override @@ -232,7 +304,7 @@ class RoleUserQuery extends Query { @override get fields { - return const ['id', 'role_id', 'user_id', 'created_at', 'updated_at']; + return const ['role_id', 'user_id']; } @override @@ -247,15 +319,12 @@ class RoleUserQuery extends Query { static RoleUser parseRow(List row) { if (row.every((x) => x == null)) return null; - var model = RoleUser( - id: row[0].toString(), - createdAt: (row[3] as DateTime), - updatedAt: (row[4] as DateTime)); - if (row.length > 5) { - model = model.copyWith(role: RoleQuery.parseRow(row.skip(5).toList())); + var model = RoleUser(); + if (row.length > 2) { + model = model.copyWith(role: RoleQuery.parseRow(row.skip(2).toList())); } - if (row.length > 9) { - model = model.copyWith(user: UserQuery.parseRow(row.skip(9).toList())); + if (row.length > 6) { + model = model.copyWith(user: UserQuery.parseRow(row.skip(6).toList())); } return model; } @@ -268,25 +337,16 @@ class RoleUserQuery extends Query { class RoleUserQueryWhere extends QueryWhere { RoleUserQueryWhere(RoleUserQuery query) - : id = NumericSqlExpressionBuilder(query, 'id'), - roleId = NumericSqlExpressionBuilder(query, 'role_id'), - userId = NumericSqlExpressionBuilder(query, 'user_id'), - createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'), - updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at'); - - final NumericSqlExpressionBuilder id; + : roleId = NumericSqlExpressionBuilder(query, 'role_id'), + userId = NumericSqlExpressionBuilder(query, 'user_id'); final NumericSqlExpressionBuilder roleId; final NumericSqlExpressionBuilder userId; - final DateTimeSqlExpressionBuilder createdAt; - - final DateTimeSqlExpressionBuilder updatedAt; - @override get expressionBuilders { - return [id, roleId, userId, createdAt, updatedAt]; + return [roleId, userId]; } } @@ -296,11 +356,6 @@ class RoleUserQueryValues extends MapQueryValues { return {}; } - int get id { - return (values['id'] as int); - } - - set id(int value) => values['id'] = value; int get roleId { return (values['role_id'] as int); } @@ -311,19 +366,7 @@ class RoleUserQueryValues extends MapQueryValues { } set userId(int value) => values['user_id'] = value; - 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(RoleUser model) { - createdAt = model.createdAt; - updatedAt = model.updatedAt; if (model.role != null) { values['role_id'] = int.parse(model.role.id); } @@ -338,6 +381,16 @@ class RoleQuery extends Query { trampoline ??= Set(); trampoline.add(tableName); _where = RoleQueryWhere(this); + leftJoin(RoleUserQuery(trampoline: trampoline), 'id', 'role_id', + additionalFields: const [ + 'id', + 'username', + 'password', + 'email', + 'created_at', + 'updated_at' + ], + trampoline: trampoline); } @override @@ -377,6 +430,12 @@ class RoleQuery extends Query { name: (row[1] as String), createdAt: (row[2] as DateTime), updatedAt: (row[3] as DateTime)); + if (row.length > 4) { + model = model.copyWith( + users: [UserQuery.parseRow(row.skip(4).toList())] + .where((x) => x != null) + .toList()); + } return model; } @@ -384,6 +443,69 @@ class RoleQuery extends Query { deserialize(List row) { return parseRow(row); } + + @override + bool canCompile(trampoline) { + return (!(trampoline.contains('roles') && + trampoline.contains('role_users'))); + } + + @override + get(QueryExecutor executor) { + return super.get(executor).then((result) { + return result.fold>([], (out, model) { + var idx = out.indexWhere((m) => m.id == model.id); + + if (idx == -1) { + return out..add(model); + } else { + var l = out[idx]; + return out + ..[idx] = l.copyWith( + users: List<_User>.from(l.users ?? []) + ..addAll(model.users ?? [])); + } + }); + }); + } + + @override + update(QueryExecutor executor) { + return super.update(executor).then((result) { + return result.fold>([], (out, model) { + var idx = out.indexWhere((m) => m.id == model.id); + + if (idx == -1) { + return out..add(model); + } else { + var l = out[idx]; + return out + ..[idx] = l.copyWith( + users: List<_User>.from(l.users ?? []) + ..addAll(model.users ?? [])); + } + }); + }); + } + + @override + delete(QueryExecutor executor) { + return super.delete(executor).then((result) { + return result.fold>([], (out, model) { + var idx = out.indexWhere((m) => m.id == model.id); + + if (idx == -1) { + return out..add(model); + } else { + var l = out[idx]; + return out + ..[idx] = l.copyWith( + users: List<_User>.from(l.users ?? []) + ..addAll(model.users ?? [])); + } + }); + }); + } } class RoleQueryWhere extends QueryWhere { @@ -519,11 +641,8 @@ class User extends _User { } @generatedSerializable -class RoleUser extends _RoleUser { - RoleUser({this.id, this.role, this.user, this.createdAt, this.updatedAt}); - - @override - final String id; +class RoleUser implements _RoleUser { + const RoleUser({this.role, this.user}); @override final _Role role; @@ -531,38 +650,17 @@ class RoleUser extends _RoleUser { @override final _User user; - @override - final DateTime createdAt; - - @override - final DateTime updatedAt; - - RoleUser copyWith( - {String id, - _Role role, - _User user, - DateTime createdAt, - DateTime updatedAt}) { - return new RoleUser( - id: id ?? this.id, - role: role ?? this.role, - user: user ?? this.user, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt); + RoleUser copyWith({_Role role, _User user}) { + return new RoleUser(role: role ?? this.role, user: user ?? this.user); } bool operator ==(other) { - return other is _RoleUser && - other.id == id && - other.role == role && - other.user == user && - other.createdAt == createdAt && - other.updatedAt == updatedAt; + return other is _RoleUser && other.role == role && other.user == user; } @override int get hashCode { - return hashObjects([id, role, user, createdAt, updatedAt]); + return hashObjects([role, user]); } Map toJson() { @@ -698,22 +796,11 @@ abstract class UserFields { abstract class RoleUserSerializer { static RoleUser fromMap(Map map) { return new RoleUser( - id: map['id'] as String, role: map['role'] != null ? RoleSerializer.fromMap(map['role'] as Map) : null, user: map['user'] != null ? UserSerializer.fromMap(map['user'] as Map) - : 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); } @@ -722,33 +809,18 @@ abstract class RoleUserSerializer { return null; } return { - 'id': model.id, 'role': RoleSerializer.toMap(model.role), - 'user': UserSerializer.toMap(model.user), - 'created_at': model.createdAt?.toIso8601String(), - 'updated_at': model.updatedAt?.toIso8601String() + 'user': UserSerializer.toMap(model.user) }; } } abstract class RoleUserFields { - static const List allFields = [ - id, - role, - user, - createdAt, - updatedAt - ]; - - static const String id = 'id'; + static const List allFields = [role, user]; static const String role = 'role'; static const String user = 'user'; - - static const String createdAt = 'created_at'; - - static const String updatedAt = 'updated_at'; } abstract class RoleSerializer {