Got many to many

This commit is contained in:
Tobe O 2019-04-02 19:05:13 -04:00
parent 261013aa7e
commit 78cd1086c9
14 changed files with 419 additions and 180 deletions

View file

@ -146,6 +146,11 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
'This instance does not support creating new WHERE clauses.'); '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<String> trampoline) => true;
/// Shorthand for calling [where].or with a new [Where] clause. /// Shorthand for calling [where].or with a new [Where] clause.
void andWhere(void Function(Where) f) { void andWhere(void Function(Where) f) {
var w = newWhereClause(); var w = newWhereClause();
@ -192,27 +197,64 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
_crossJoin = tableName; _crossJoin = tableName;
} }
String _joinAlias() => 'a${_joins.length}'; String _joinAlias(Set<String> trampoline) {
int i = _joins.length;
while (true) {
var a = 'a$i';
if (trampoline.add(a)) {
return a;
} else
i++;
}
}
String _compileJoin(tableName, Set<String> trampoline) { String _compileJoin(tableName, Set<String> trampoline) {
if (tableName is String) return tableName; if (tableName is String)
if (tableName is Query) { return tableName;
else if (tableName is Query) {
var c = tableName.compile(trampoline); var c = tableName.compile(trampoline);
if (c == null) return c; if (c == null) return c;
return '($c)'; return '($c)';
} } else {
throw ArgumentError.value( throw ArgumentError.value(
tableName, 'tableName', 'must be a String or Query'); tableName, 'tableName', 'must be a String or Query');
} }
}
void _makeJoin(
tableName,
Set<String> trampoline,
JoinType type,
String localKey,
String foreignKey,
String op,
List<String> 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));
}
}
/// Execute an `INNER JOIN` against another table. /// Execute an `INNER JOIN` against another table.
void join(tableName, String localKey, String foreignKey, void join(tableName, String localKey, String foreignKey,
{String op: '=', {String op: '=',
List<String> additionalFields: const [], List<String> additionalFields: const [],
Set<String> trampoline}) { Set<String> trampoline}) {
_joins.add(new JoinBuilder(JoinType.inner, this, _makeJoin(tableName, trampoline, JoinType.inner, localKey, foreignKey, op,
_compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, additionalFields);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
} }
/// Execute a `LEFT JOIN` against another table. /// Execute a `LEFT JOIN` against another table.
@ -220,9 +262,8 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
{String op: '=', {String op: '=',
List<String> additionalFields: const [], List<String> additionalFields: const [],
Set<String> trampoline}) { Set<String> trampoline}) {
_joins.add(new JoinBuilder(JoinType.left, this, _makeJoin(tableName, trampoline, JoinType.left, localKey, foreignKey, op,
_compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, additionalFields);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
} }
/// Execute a `RIGHT JOIN` against another table. /// Execute a `RIGHT JOIN` against another table.
@ -230,9 +271,8 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
{String op: '=', {String op: '=',
List<String> additionalFields: const [], List<String> additionalFields: const [],
Set<String> trampoline}) { Set<String> trampoline}) {
_joins.add(new JoinBuilder(JoinType.right, this, _makeJoin(tableName, trampoline, JoinType.right, localKey, foreignKey, op,
_compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, additionalFields);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
} }
/// Execute a `FULL OUTER JOIN` against another table. /// Execute a `FULL OUTER JOIN` against another table.
@ -240,9 +280,8 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
{String op: '=', {String op: '=',
List<String> additionalFields: const [], List<String> additionalFields: const [],
Set<String> trampoline}) { Set<String> trampoline}) {
_joins.add(new JoinBuilder(JoinType.full, this, _makeJoin(tableName, trampoline, JoinType.full, localKey, foreignKey, op,
_compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, additionalFields);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
} }
/// Execute a `SELF JOIN`. /// Execute a `SELF JOIN`.
@ -250,9 +289,8 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
{String op: '=', {String op: '=',
List<String> additionalFields: const [], List<String> additionalFields: const [],
Set<String> trampoline}) { Set<String> trampoline}) {
_joins.add(new JoinBuilder(JoinType.self, this, _makeJoin(tableName, trampoline, JoinType.self, localKey, foreignKey, op,
_compileJoin(tableName, trampoline ?? Set()), localKey, foreignKey, additionalFields);
op: op, alias: _joinAlias(), additionalFields: additionalFields));
} }
@override @override
@ -261,7 +299,8 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
String preamble, String preamble,
bool withFields: true, bool withFields: true,
String fromQuery}) { String fromQuery}) {
if (!trampoline.add(tableName)) { // One table MAY appear multiple times in a query.
if (!canCompile(trampoline)) {
return null; return null;
} }
@ -294,7 +333,7 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
if (preamble == null) { if (preamble == null) {
if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin'); if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin');
for (var join in _joins) { for (var join in _joins) {
var c = join.compile(); var c = join.compile(trampoline);
if (c != null) b.write(' $c'); if (c != null) b.write(' $c');
} }
} }
@ -565,7 +604,7 @@ class JoinBuilder {
return right; return right;
} }
String compile() { String compile(Set<String> trampoline) {
if (to == null) return null; if (to == null) return null;
var b = new StringBuffer(); var b = new StringBuffer();
var left = '${from.tableName}.$key'; var left = '${from.tableName}.$key';

View file

@ -60,16 +60,16 @@ class BelongsTo extends Relationship {
const BelongsTo belongsTo = const BelongsTo(); const BelongsTo belongsTo = const BelongsTo();
class ManyToMany extends Relationship { class ManyToMany extends Relationship {
const ManyToMany( final Type through;
const ManyToMany(this.through,
{String localKey: 'id', {String localKey: 'id',
String foreignKey, String foreignKey,
String foreignTable, String foreignTable,
bool cascadeOnDelete: false}) bool cascadeOnDelete: false})
: super(RelationshipType.manyToMany, : super(RelationshipType.hasMany, // Many-to-Many is actually just a hasMany
localKey: localKey, localKey: localKey,
foreignKey: foreignKey, foreignKey: foreignKey,
foreignTable: foreignTable, foreignTable: foreignTable,
cascadeOnDelete: cascadeOnDelete == true); cascadeOnDelete: cascadeOnDelete == true);
} }
const ManyToMany manyToMany = const ManyToMany();

View file

@ -105,7 +105,8 @@ Future<OrmBuildContext> buildOrmContext(
var foreignKey = cr.peek('foreignKey')?.stringValue; var foreignKey = cr.peek('foreignKey')?.stringValue;
var foreignTable = cr.peek('foreignTable')?.stringValue; var foreignTable = cr.peek('foreignTable')?.stringValue;
var cascadeOnDelete = cr.peek('cascadeOnDelete')?.boolValue == true; var cascadeOnDelete = cr.peek('cascadeOnDelete')?.boolValue == true;
OrmBuildContext foreign; var through = cr.peek('through')?.typeValue;
OrmBuildContext foreign, throughContext;
if (foreignTable == null) { if (foreignTable == null) {
// if (!isModelClass(field.type) && // if (!isModelClass(field.type) &&
@ -137,6 +138,17 @@ Future<OrmBuildContext> buildOrmContext(
resolver, resolver,
autoSnakeCaseNames); 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) var ormAnn = const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(modelType.element); .firstAnnotationOf(modelType.element);
@ -164,12 +176,15 @@ Future<OrmBuildContext> buildOrmContext(
foreignKey ??= 'id'; foreignKey ??= 'id';
} }
var relation = new Relationship( var relation = new RelationshipReader(
type, type,
localKey: localKey, localKey: localKey,
foreignKey: foreignKey, foreignKey: foreignKey,
foreignTable: foreignTable, foreignTable: foreignTable,
cascadeOnDelete: cascadeOnDelete, cascadeOnDelete: cascadeOnDelete,
through: through,
foreign: foreign,
throughContext: throughContext,
); );
if (relation.type == RelationshipType.belongsTo) { if (relation.type == RelationshipType.belongsTo) {
@ -189,7 +204,6 @@ Future<OrmBuildContext> buildOrmContext(
} }
ctx.relations[field.name] = relation; ctx.relations[field.name] = relation;
ctx.relationTypes[relation] = foreign;
} else { } else {
if (column?.type == null) if (column?.type == null)
throw 'Cannot infer SQL column type for field "${ctx.buildContext.originalClassName}.${field.name}" with type "${field.type.displayName}".'; 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<String, Column> columns = {}; final Map<String, Column> columns = {};
final List<FieldElement> effectiveFields = []; final List<FieldElement> effectiveFields = [];
final Map<String, Relationship> relations = {}; final Map<String, RelationshipReader> relations = {};
final Map<Relationship, OrmBuildContext> relationTypes = {};
OrmBuildContext(this.buildContext, this.ormAnnotation, this.tableName); OrmBuildContext(this.buildContext, this.ormAnnotation, this.tableName);
} }

View file

@ -211,7 +211,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
RelationshipType.belongsTo, RelationshipType.belongsTo,
RelationshipType.hasMany RelationshipType.hasMany
].contains(relation.type)) return; ].contains(relation.type)) return;
var foreign = ctx.relationTypes[relation]; var foreign = relation.foreign;
var skipToList = refer('row') var skipToList = refer('row')
.property('skip') .property('skip')
.call([literalNum(i)]) .call([literalNum(i)])
@ -234,7 +234,7 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
var blockStr = block.accept(new DartEmitter()); var blockStr = block.accept(new DartEmitter());
var ifStr = 'if (row.length > $i) { $blockStr }'; var ifStr = 'if (row.length > $i) { $blockStr }';
b.statements.add(new Code(ifStr)); b.statements.add(new Code(ifStr));
i += ctx.relationTypes[relation].effectiveFields.length; i += relation.foreign.effectiveFields.length;
}); });
b.addExpression(refer('model').returned); b.addExpression(refer('model').returned);
@ -279,11 +279,14 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
if (relation.type == RelationshipType.belongsTo || if (relation.type == RelationshipType.belongsTo ||
relation.type == RelationshipType.hasOne || relation.type == RelationshipType.hasOne ||
relation.type == RelationshipType.hasMany) { relation.type == RelationshipType.hasMany) {
var foreign = ctx.relationTypes[relation]; var foreign = relation.throughContext ?? relation.foreign;
var additionalFields = foreign.effectiveFields
// 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)) // .where((f) => f.name != 'id' || !isSpecialId(ctx, f))
.map((f) => literalString( .map((f) => literalString(relation.foreign.buildContext
foreign.buildContext.resolveFieldName(f.name))); .resolveFieldName(f.name)));
var joinArgs = [relation.localKey, relation.foreignKey] var joinArgs = [relation.localKey, relation.foreignKey]
.map(literalString) .map(literalString)
.toList(); .toList();
@ -303,13 +306,43 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
b.addExpression(refer('leftJoin').call(joinArgs, { b.addExpression(refer('leftJoin').call(joinArgs, {
'additionalFields': '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<Expression>((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 // TODO: Ultimately remove the insert override
if (false && ctx.relations.isNotEmpty) { if (false && ctx.relations.isNotEmpty) {
clazz.methods.add(new Method((b) { clazz.methods.add(new Method((b) {
@ -391,10 +424,11 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
var args = <String, Expression>{}; var args = <String, Expression>{};
ctx.relations.forEach((name, relation) { 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 // For each hasMany, we need to create a query of
// the corresponding type. // the corresponding type.
var foreign = ctx.relationTypes[relation]; var foreign = relation.foreign;
var queryType = refer( var queryType = refer(
'${foreign.buildContext.modelClassNameRecase.pascalCase}Query'); '${foreign.buildContext.modelClassNameRecase.pascalCase}Query');
var queryInstance = queryType.newInstance([]); var queryInstance = queryType.newInstance([]);

View file

@ -1,6 +1,8 @@
import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_orm/angel_orm.dart'; import 'package:angel_orm/angel_orm.dart';
import 'package:source_gen/source_gen.dart'; import 'package:source_gen/source_gen.dart';
import 'orm_build_context.dart';
const TypeChecker columnTypeChecker = const TypeChecker.fromRuntime(Column); const TypeChecker columnTypeChecker = const TypeChecker.fromRuntime(Column);
@ -19,3 +21,26 @@ class ColumnReader {
DartObject get defaultValue => reader.peek('defaultValue')?.objectValue; 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;
}

View file

@ -35,6 +35,6 @@ dev_dependencies:
collection: ^1.0.0 collection: ^1.0.0
postgres: ^1.0.0 postgres: ^1.0.0
test: ^1.0.0 test: ^1.0.0
# dependency_overrides: dependency_overrides:
# angel_orm: angel_orm:
# path: ../angel_orm path: ../angel_orm

View file

@ -25,7 +25,7 @@ class PostgresExecutor extends QueryExecutor {
@override @override
Future<List<List>> query( Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues, String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String> returningFields]) { [List<String> returningFields]) async {
if (returningFields != null) { if (returningFields != null) {
var fields = returningFields.join(', '); var fields = returningFields.join(', ');
var returning = 'RETURNING $fields'; var returning = 'RETURNING $fields';
@ -37,7 +37,16 @@ class PostgresExecutor extends QueryExecutor {
if (substitutionValues.isNotEmpty) print('Values: $substitutionValues'); if (substitutionValues.isNotEmpty) print('Values: $substitutionValues');
print(substitutionValues.map((k, v) => MapEntry(k, v.runtimeType))); 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 @override

View file

@ -1,45 +1,86 @@
library angel_orm_generator.test; library angel_orm_generator.test;
import 'dart:async'; import 'dart:async';
import 'package:angel_orm/angel_orm.dart'; import 'dart:io';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'models/user.dart'; import 'models/user.dart';
import 'common.dart'; import 'common.dart';
main() { main() {
QueryExecutor executor; PostgresExecutor executor;
Role canPub, canSub; Role canPub, canSub;
User thosakwe; User thosakwe;
Future<void> 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 { setUp(() async {
executor = await connectToPostgres(['user', 'role', 'user_role']); executor = await connectToPostgres(['user', 'role', 'user_role']);
var canPubQuery = new RoleQuery()..values.name = 'can_pub'; // await dumpQuery("""
var canSubQuery = new RoleQuery()..values.name = 'can_sub'; // WITH roles as
canPub = await canPubQuery.insert(executor); // (INSERT INTO roles (name)
canSub = await canSubQuery.insert(executor); // 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 thosakweQuery.values
..username = 'thosakwe' ..username = 'thosakwe'
..password = 'Hahahahayoureallythoughtiwasstupidenoughtotypethishere' ..password = 'Hahahahayoureallythoughtiwasstupidenoughtotypethishere'
..email = 'thosakwe AT gmail.com'; ..email = 'thosakwe AT gmail.com';
thosakwe = await thosakweQuery.insert(executor); thosakwe = await thosakweQuery.insert(executor);
print('=== THOSAKWE: ${thosakwe?.toJson()}');
// Allow thosakwe to publish... // Allow thosakwe to publish...
var thosakwePubQuery = new RoleUserQuery(); var thosakwePubQuery = RoleUserQuery();
thosakwePubQuery.values thosakwePubQuery.values
..userId = int.parse(thosakwe.id) ..userId = int.parse(thosakwe.id)
..roleId = int.parse(canPub.id); ..roleId = int.parse(canPub.id);
await thosakwePubQuery.insert(executor); await thosakwePubQuery.insert(executor);
// Allow thosakwe to subscribe... // Allow thosakwe to subscribe...
var thosakweSubQuery = new RoleUserQuery(); var thosakweSubQuery = RoleUserQuery();
thosakweSubQuery.values thosakweSubQuery.values
..userId = int.parse(thosakwe.id) ..userId = int.parse(thosakwe.id)
..roleId = int.parse(canSub.id); ..roleId = int.parse(canSub.id);
await thosakweSubQuery.insert(executor); 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('\n');
print('=================================================='); print('==================================================');
print(' GOOD STUFF BEGINS HERE '); print(' GOOD STUFF BEGINS HERE ');
@ -47,7 +88,7 @@ main() {
}); });
Future<User> fetchThosakwe() async { Future<User> 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); return await query.getOne(executor);
} }
@ -60,9 +101,9 @@ main() {
test('fetch users for role', () async { test('fetch users for role', () async {
for (var role in [canPub, canSub]) { 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); var r = await query.getOne(executor);
expect(r.users, [thosakwe]); expect(r.users.toList(), [thosakwe]);
} }
}); });
} }

View file

@ -35,9 +35,11 @@ class BookQuery extends Query<Book, BookQueryWhere> {
trampoline.add(tableName); trampoline.add(tableName);
_where = BookQueryWhere(this); _where = BookQueryWhere(this);
leftJoin('authors', 'author_id', 'id', 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', 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 @override

View file

@ -32,13 +32,15 @@ class LegQuery extends Query<Leg, LegQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = LegQueryWhere(this); _where = LegQueryWhere(this);
leftJoin('feet', 'id', 'leg_id', additionalFields: const [ leftJoin('feet', 'id', 'leg_id',
additionalFields: const [
'id', 'id',
'leg_id', 'leg_id',
'n_toes', 'n_toes',
'created_at', 'created_at',
'updated_at' 'updated_at'
]); ],
trampoline: trampoline);
} }
@override @override

View file

@ -36,7 +36,8 @@ class OrderQuery extends Query<Order, OrderQueryWhere> {
trampoline.add(tableName); trampoline.add(tableName);
_where = OrderQueryWhere(this); _where = OrderQueryWhere(this);
leftJoin('customers', 'customer_id', 'id', leftJoin('customers', 'customer_id', 'id',
additionalFields: const ['id', 'created_at', 'updated_at']); additionalFields: const ['id', 'created_at', 'updated_at'],
trampoline: trampoline);
} }
@override @override

View file

@ -39,7 +39,8 @@ class TreeQuery extends Query<Tree, TreeQueryWhere> {
'common_name', 'common_name',
'created_at', 'created_at',
'updated_at' 'updated_at'
]); ],
trampoline: trampoline);
} }
@override @override

View file

@ -14,13 +14,13 @@ abstract class _User extends Model {
String get password; String get password;
String get email; String get email;
@manyToMany @ManyToMany(_RoleUser)
List<_Role> get roles; List<_Role> get roles;
} }
@serializable @serializable
@orm @orm
abstract class _RoleUser extends Model { abstract class _RoleUser {
@belongsTo @belongsTo
_Role get role; _Role get role;
@ -33,6 +33,6 @@ abstract class _RoleUser extends Model {
abstract class _Role extends Model { abstract class _Role extends Model {
String name; String name;
@manyToMany @ManyToMany(_RoleUser)
List<_User> get users; List<_User> get users;
} }

View file

@ -29,9 +29,6 @@ class RoleUserMigration extends Migration {
@override @override
up(Schema schema) { up(Schema schema) {
schema.create('role_users', (table) { 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('role_id').references('roles', 'id');
table.integer('user_id').references('users', 'id'); table.integer('user_id').references('users', 'id');
}); });
@ -69,6 +66,9 @@ class UserQuery extends Query<User, UserQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = UserQueryWhere(this); _where = UserQueryWhere(this);
leftJoin(RoleUserQuery(trampoline: trampoline), 'id', 'user_id',
additionalFields: const ['id', 'name', 'created_at', 'updated_at'],
trampoline: trampoline);
} }
@override @override
@ -117,6 +117,12 @@ class UserQuery extends Query<User, UserQueryWhere> {
email: (row[3] as String), email: (row[3] as String),
createdAt: (row[4] as DateTime), createdAt: (row[4] as DateTime),
updatedAt: (row[5] 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; return model;
} }
@ -124,6 +130,69 @@ class UserQuery extends Query<User, UserQueryWhere> {
deserialize(List row) { deserialize(List row) {
return parseRow(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<List<User>>([], (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<List<User>>([], (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<List<User>>([], (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 { class UserQueryWhere extends QueryWhere {
@ -204,15 +273,18 @@ class RoleUserQuery extends Query<RoleUser, RoleUserQueryWhere> {
trampoline.add(tableName); trampoline.add(tableName);
_where = RoleUserQueryWhere(this); _where = RoleUserQueryWhere(this);
leftJoin('roles', 'role_id', 'id', leftJoin('roles', 'role_id', 'id',
additionalFields: const ['id', 'name', 'created_at', 'updated_at']); additionalFields: const ['id', 'name', 'created_at', 'updated_at'],
leftJoin('users', 'user_id', 'id', additionalFields: const [ trampoline: trampoline);
leftJoin('users', 'user_id', 'id',
additionalFields: const [
'id', 'id',
'username', 'username',
'password', 'password',
'email', 'email',
'created_at', 'created_at',
'updated_at' 'updated_at'
]); ],
trampoline: trampoline);
} }
@override @override
@ -232,7 +304,7 @@ class RoleUserQuery extends Query<RoleUser, RoleUserQueryWhere> {
@override @override
get fields { get fields {
return const ['id', 'role_id', 'user_id', 'created_at', 'updated_at']; return const ['role_id', 'user_id'];
} }
@override @override
@ -247,15 +319,12 @@ class RoleUserQuery extends Query<RoleUser, RoleUserQueryWhere> {
static RoleUser parseRow(List row) { static RoleUser parseRow(List row) {
if (row.every((x) => x == null)) return null; if (row.every((x) => x == null)) return null;
var model = RoleUser( var model = RoleUser();
id: row[0].toString(), if (row.length > 2) {
createdAt: (row[3] as DateTime), model = model.copyWith(role: RoleQuery.parseRow(row.skip(2).toList()));
updatedAt: (row[4] as DateTime));
if (row.length > 5) {
model = model.copyWith(role: RoleQuery.parseRow(row.skip(5).toList()));
} }
if (row.length > 9) { if (row.length > 6) {
model = model.copyWith(user: UserQuery.parseRow(row.skip(9).toList())); model = model.copyWith(user: UserQuery.parseRow(row.skip(6).toList()));
} }
return model; return model;
} }
@ -268,25 +337,16 @@ class RoleUserQuery extends Query<RoleUser, RoleUserQueryWhere> {
class RoleUserQueryWhere extends QueryWhere { class RoleUserQueryWhere extends QueryWhere {
RoleUserQueryWhere(RoleUserQuery query) RoleUserQueryWhere(RoleUserQuery query)
: id = NumericSqlExpressionBuilder<int>(query, 'id'), : roleId = NumericSqlExpressionBuilder<int>(query, 'role_id'),
roleId = NumericSqlExpressionBuilder<int>(query, 'role_id'), userId = NumericSqlExpressionBuilder<int>(query, 'user_id');
userId = NumericSqlExpressionBuilder<int>(query, 'user_id'),
createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'),
updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at');
final NumericSqlExpressionBuilder<int> id;
final NumericSqlExpressionBuilder<int> roleId; final NumericSqlExpressionBuilder<int> roleId;
final NumericSqlExpressionBuilder<int> userId; final NumericSqlExpressionBuilder<int> userId;
final DateTimeSqlExpressionBuilder createdAt;
final DateTimeSqlExpressionBuilder updatedAt;
@override @override
get expressionBuilders { get expressionBuilders {
return [id, roleId, userId, createdAt, updatedAt]; return [roleId, userId];
} }
} }
@ -296,11 +356,6 @@ class RoleUserQueryValues extends MapQueryValues {
return {}; return {};
} }
int get id {
return (values['id'] as int);
}
set id(int value) => values['id'] = value;
int get roleId { int get roleId {
return (values['role_id'] as int); return (values['role_id'] as int);
} }
@ -311,19 +366,7 @@ class RoleUserQueryValues extends MapQueryValues {
} }
set userId(int value) => values['user_id'] = value; 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) { void copyFrom(RoleUser model) {
createdAt = model.createdAt;
updatedAt = model.updatedAt;
if (model.role != null) { if (model.role != null) {
values['role_id'] = int.parse(model.role.id); values['role_id'] = int.parse(model.role.id);
} }
@ -338,6 +381,16 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
trampoline ??= Set(); trampoline ??= Set();
trampoline.add(tableName); trampoline.add(tableName);
_where = RoleQueryWhere(this); _where = RoleQueryWhere(this);
leftJoin(RoleUserQuery(trampoline: trampoline), 'id', 'role_id',
additionalFields: const [
'id',
'username',
'password',
'email',
'created_at',
'updated_at'
],
trampoline: trampoline);
} }
@override @override
@ -377,6 +430,12 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
name: (row[1] as String), name: (row[1] as String),
createdAt: (row[2] as DateTime), createdAt: (row[2] as DateTime),
updatedAt: (row[3] 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; return model;
} }
@ -384,6 +443,69 @@ class RoleQuery extends Query<Role, RoleQueryWhere> {
deserialize(List row) { deserialize(List row) {
return parseRow(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<List<Role>>([], (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<List<Role>>([], (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<List<Role>>([], (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 { class RoleQueryWhere extends QueryWhere {
@ -519,11 +641,8 @@ class User extends _User {
} }
@generatedSerializable @generatedSerializable
class RoleUser extends _RoleUser { class RoleUser implements _RoleUser {
RoleUser({this.id, this.role, this.user, this.createdAt, this.updatedAt}); const RoleUser({this.role, this.user});
@override
final String id;
@override @override
final _Role role; final _Role role;
@ -531,38 +650,17 @@ class RoleUser extends _RoleUser {
@override @override
final _User user; final _User user;
@override RoleUser copyWith({_Role role, _User user}) {
final DateTime createdAt; return new RoleUser(role: role ?? this.role, user: user ?? this.user);
@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);
} }
bool operator ==(other) { bool operator ==(other) {
return other is _RoleUser && return other is _RoleUser && other.role == role && other.user == user;
other.id == id &&
other.role == role &&
other.user == user &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
} }
@override @override
int get hashCode { int get hashCode {
return hashObjects([id, role, user, createdAt, updatedAt]); return hashObjects([role, user]);
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -698,22 +796,11 @@ abstract class UserFields {
abstract class RoleUserSerializer { abstract class RoleUserSerializer {
static RoleUser fromMap(Map map) { static RoleUser fromMap(Map map) {
return new RoleUser( return new RoleUser(
id: map['id'] as String,
role: map['role'] != null role: map['role'] != null
? RoleSerializer.fromMap(map['role'] as Map) ? RoleSerializer.fromMap(map['role'] as Map)
: null, : null,
user: map['user'] != null user: map['user'] != null
? UserSerializer.fromMap(map['user'] as Map) ? 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); : null);
} }
@ -722,33 +809,18 @@ abstract class RoleUserSerializer {
return null; return null;
} }
return { return {
'id': model.id,
'role': RoleSerializer.toMap(model.role), 'role': RoleSerializer.toMap(model.role),
'user': UserSerializer.toMap(model.user), 'user': UserSerializer.toMap(model.user)
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
}; };
} }
} }
abstract class RoleUserFields { abstract class RoleUserFields {
static const List<String> allFields = <String>[ static const List<String> allFields = <String>[role, user];
id,
role,
user,
createdAt,
updatedAt
];
static const String id = 'id';
static const String role = 'role'; static const String role = 'role';
static const String user = 'user'; static const String user = 'user';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
} }
abstract class RoleSerializer { abstract class RoleSerializer {