all orm tests pass

This commit is contained in:
Tobe O 2018-12-07 23:13:49 -05:00
parent 643661cf86
commit d07ded57c4
10 changed files with 433 additions and 49 deletions

View file

@ -108,12 +108,20 @@ Future<OrmBuildContext> buildOrmContext(
OrmBuildContext foreign;
if (foreignTable == null) {
if (!isModelClass(field.type)) {
if (!isModelClass(field.type) &&
!(field.type is InterfaceType &&
isListOfModelType(field.type as InterfaceType))) {
throw new UnsupportedError(
'Cannot apply relationship to field "${field.name}" - ${field.type.name} is not assignable to Model.');
} else {
try {
var modelType = firstModelAncestor(field.type) ?? field.type;
var refType = field.type;
if (refType is InterfaceType && isListOfModelType(refType)) {
refType = (refType as InterfaceType).typeArguments[0];
}
var modelType = firstModelAncestor(refType) ?? refType;
foreign = await buildOrmContext(
modelType.element as ClassElement,
@ -162,8 +170,14 @@ Future<OrmBuildContext> buildOrmContext(
if (isBelongsRelation(relation)) {
var name = new ReCase(relation.localKey).camelCase;
ctx.buildContext.aliases[name] = relation.localKey;
ctx.effectiveFields.add(new RelationFieldImpl(
name, field.type.element.context.typeProvider.intType, field.name));
if (!ctx.effectiveFields.any((f) => f.name == field.name)) {
if (field.name != 'id' || !autoIdAndDateFields) {
var rf = new RelationFieldImpl(name,
field.type.element.context.typeProvider.intType, field.name);
ctx.effectiveFields.add(rf);
}
}
}
ctx.relations[field.name] = relation;
@ -172,7 +186,9 @@ Future<OrmBuildContext> buildOrmContext(
if (column?.type == null)
throw 'Cannot infer SQL column type for field "${field.name}" with type "${field.type.name}".';
ctx.columns[field.name] = column;
ctx.effectiveFields.add(field);
if (!ctx.effectiveFields.any((f) => f.name == field.name))
ctx.effectiveFields.add(field);
}
}

View file

@ -153,11 +153,14 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
args[field.name] = expr;
}
b.statements.add(new Code('if (row.every((x) => x == null)) return null;'));
b.statements
.add(new Code('if (row.every((x) => x == null)) return null;'));
b.addExpression(ctx.buildContext.modelClassType
.newInstance([], args).assignVar('model'));
ctx.relations.forEach((name, relation) {
if (!const [RelationshipType.hasOne, RelationshipType.belongsTo]
.contains(relation.type)) return;
var foreign = ctx.relationTypes[relation];
var skipToList = refer('row')
.property('skip')
@ -248,6 +251,16 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
b.addExpression(refer('result').assign(
refer('getOne').call([refer('executor')]).awaited));
// Fetch the results of @hasMany
ctx.relations.forEach((name, relation) {
if (relation.type == RelationshipType.hasMany) {
// Call fetchLinked();
var fetchLinked = refer('fetchLinked')
.call([refer('result'), refer('executor')]).awaited;
b.addExpression(refer('result').assign(fetchLinked));
}
});
b.addExpression(refer('result').returned);
});
});
@ -258,6 +271,108 @@ class OrmGenerator extends GeneratorForAnnotation<Orm> {
});
}));
}
// Create a Future<T> fetchLinked(T model, QueryExecutor), if necessary.
if (ctx.relations.values.any((r) => r.type == RelationshipType.hasMany)) {
clazz.methods.add(new Method((b) {
b
..name = 'fetchLinked'
..modifier = MethodModifier.async
..returns = new TypeReference((b) {
b
..symbol = 'Future'
..types.add(ctx.buildContext.modelClassType);
})
..requiredParameters.addAll([
new Parameter((b) => b
..name = 'model'
..type = ctx.buildContext.modelClassType),
new Parameter((b) => b
..name = 'executor'
..type = refer('QueryExecutor')),
])
..body = new Block((b) {
var args = <String, Expression>{};
ctx.relations.forEach((name, relation) {
if (relation.type == RelationshipType.hasMany) {
// For each hasMany, we need to create a query of
// the corresponding type.
var foreign = ctx.relationTypes[relation];
var queryType = refer(
'${foreign.buildContext.modelClassNameRecase.pascalCase}Query');
var queryInstance = queryType.newInstance([]);
// Next, we need to apply a cascade that sets the correct query value.
var localField = ctx.buildContext.fields.firstWhere(
(f) =>
ctx.buildContext.resolveFieldName(f.name) ==
relation.localKey, orElse: () {
throw '${ctx.buildContext.clazz.name} has no field that maps to the name "${relation.localKey}", '
'but it has a @HasMany() relation that expects such a field.';
});
var foreignField = foreign.buildContext.fields.firstWhere(
(f) =>
foreign.buildContext.resolveFieldName(f.name) ==
relation.foreignKey, orElse: () {
throw '${foreign.buildContext.clazz.name} has no field that maps to the name "${relation.foreignKey}", '
'but ${ctx.buildContext.clazz.name} has a @HasMany() relation that expects such a field.';
});
var queryValue =
(localField.name == 'id' && autoIdAndDateFields)
? 'int.parse(model.id)'
: 'model.${localField.name}';
var cascadeText =
'..where.${foreignField.name}.equals($queryValue)';
var queryText = queryInstance.accept(new DartEmitter());
var combinedExpr =
new CodeExpression(new Code('($queryText$cascadeText)'));
// Finally, just call get and await it.
var expr = combinedExpr
.property('get')
.call([refer('executor')]).awaited;
args[name] = expr;
}
});
// Just return a copyWith
b.addExpression(
refer('model').property('copyWith').call([], args).returned);
});
}));
}
// Also, if there is a @HasMany, generate overrides for query methods that
// execute in a transaction, and invoke fetchLinked.
if (ctx.relations.values.any((r) => r.type == RelationshipType.hasMany)) {
for (var methodName in const ['get', 'update', 'delete']) {
clazz.methods.add(new Method((b) {
b
..name = methodName
..annotations.add(refer('override'))
..requiredParameters.add(new Parameter((b) => b
..name = 'executor'
..type = refer('QueryExecutor')))
..body = new Block((b) {
var inTransaction = new Method((b) {
b..modifier = MethodModifier.async
..body = new Block((b) {
var mapped = new CodeExpression(new Code('await Future.wait(result.map((m) => fetchLinked(m, executor)))'));
b.addExpression(new CodeExpression(new Code('var result = await super.$methodName(executor)')));
b.addExpression(mapped.returned);
});
});
b.addExpression(refer('executor')
.property('transaction')
.call([inTransaction.closure]).returned);
});
}));
}
}
});
}

View file

@ -36,6 +36,7 @@ class PostgresExecutor extends QueryExecutor {
@override
Future<T> transaction<T>(FutureOr<T> Function() f) async {
if (connection is! PostgreSQLConnection) return await f();
var old = connection;
T result;
try {

View file

@ -1,19 +1,18 @@
import 'package:postgres/postgres.dart';
import 'package:test/test.dart';
import 'models/fruit.dart';
import 'models/fruit.orm.g.dart';
import 'models/tree.dart';
import 'models/tree.orm.g.dart';
import 'common.dart';
main() {
PostgreSQLConnection connection;
PostgresExecutor executor;
Tree appleTree;
int treeId;
setUp(() async {
connection = await connectToPostgres(['tree', 'fruit']);
appleTree = await TreeQuery.insert(connection, rings: 10);
var query = new TreeQuery()..values.rings = 10;
executor = await connectToPostgres(['tree', 'fruit']);
appleTree = await query.insert(executor);
treeId = int.parse(appleTree.id);
});
@ -33,34 +32,36 @@ main() {
}
setUp(() async {
apple = await FruitQuery.insert(
connection,
treeId: treeId,
commonName: 'Apple',
);
var appleQuery = new FruitQuery()
..values.treeId = treeId
..values.commonName = 'Apple';
banana = await FruitQuery.insert(
connection,
treeId: treeId,
commonName: 'Banana',
);
var bananaQuery = new FruitQuery()
..values.treeId = treeId
..values.commonName = 'Banana';
apple = await appleQuery.insert(executor);
banana = await bananaQuery.insert(executor);
});
test('can fetch any children', () async {
var tree = await TreeQuery.getOne(treeId, connection);
var query = new TreeQuery()..where.id.equals(treeId);
var tree = await query.getOne(executor);
verify(tree);
});
test('sets on update', () async {
var tq = new TreeQuery()..where.id.equals(treeId);
var tree = await tq.update(connection, rings: 24).first;
var tq = new TreeQuery()
..where.id.equals(treeId)
..values.rings = 24;
var tree = await tq.updateOne(executor);
verify(tree);
expect(tree.rings, 24);
});
test('sets on delete', () async {
var tq = new TreeQuery()..where.id.equals(treeId);
var tree = await tq.delete(connection).first;
var tree = await tq.deleteOne(executor);
verify(tree);
});
});

View file

@ -0,0 +1,8 @@
CREATE TEMPORARY TABLE "fruits" (
"id" serial,
"tree_id" int,
"common_name" varchar,
"created_at" timestamp,
"updated_at" timestamp,
PRIMARY KEY(id)
);

View file

@ -0,0 +1,8 @@
CREATE TEMPORARY TABLE "trees" (
"id" serial,
"rings" smallint UNIQUE,
"created_at" timestamp,
"updated_at" timestamp,
UNIQUE(rings),
PRIMARY KEY(id)
);

View file

@ -2,6 +2,141 @@
part of angel_orm_generator.test.models.tree;
// **************************************************************************
// OrmGenerator
// **************************************************************************
class TreeQuery extends Query<Tree, TreeQueryWhere> {
TreeQuery() {}
@override
final TreeQueryValues values = new TreeQueryValues();
@override
final TreeQueryWhere where = new TreeQueryWhere();
@override
get tableName {
return 'trees';
}
@override
get fields {
return const ['id', 'rings', 'created_at', 'updated_at'];
}
@override
TreeQueryWhere newWhereClause() {
return new TreeQueryWhere();
}
static Tree parseRow(List row) {
if (row.every((x) => x == null)) return null;
var model = new Tree(
id: row[0].toString(),
rings: (row[1] as int),
createdAt: (row[2] as DateTime),
updatedAt: (row[3] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
@override
insert(executor) {
return executor.transaction(() async {
var result = await super.insert(executor);
where.id.equals(int.parse(result.id));
result = await getOne(executor);
result = await fetchLinked(result, executor);
return result;
});
}
Future<Tree> fetchLinked(Tree model, QueryExecutor executor) async {
return model.copyWith(
fruits: await (new FruitQuery()
..where.treeId.equals(int.parse(model.id)))
.get(executor));
}
@override
get(QueryExecutor executor) {
return executor.transaction(() async {
var result = await super.get(executor);
return await Future.wait(result.map((m) => fetchLinked(m, executor)));
});
}
@override
update(QueryExecutor executor) {
return executor.transaction(() async {
var result = await super.update(executor);
return await Future.wait(result.map((m) => fetchLinked(m, executor)));
});
}
@override
delete(QueryExecutor executor) {
return executor.transaction(() async {
var result = await super.delete(executor);
return await Future.wait(result.map((m) => fetchLinked(m, executor)));
});
}
}
class TreeQueryWhere extends QueryWhere {
final NumericSqlExpressionBuilder<int> id =
new NumericSqlExpressionBuilder<int>('id');
final NumericSqlExpressionBuilder<int> rings =
new NumericSqlExpressionBuilder<int>('rings');
final DateTimeSqlExpressionBuilder createdAt =
new DateTimeSqlExpressionBuilder('created_at');
final DateTimeSqlExpressionBuilder updatedAt =
new DateTimeSqlExpressionBuilder('updated_at');
@override
get expressionBuilders {
return [id, rings, createdAt, updatedAt];
}
}
class TreeQueryValues extends MapQueryValues {
int get id {
return (values['id'] as int);
}
void set id(int value) => values['id'] = value;
int get rings {
return (values['rings'] as int);
}
void set rings(int value) => values['rings'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
void set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
void set updatedAt(DateTime value) => values['updated_at'] = value;
void copyFrom(Tree model) {
values.addAll({
'rings': model.rings,
'created_at': model.createdAt,
'updated_at': model.updatedAt
});
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************

View file

@ -13,6 +13,7 @@ part 'user.serializer.g.dart';
class _User extends Model {
String username, password, email;
@belongsToMany
List<Role> roles;
// TODO: Belongs to many
//@belongsToMany
//List<Role> roles;
}

View file

@ -2,6 +2,124 @@
part of angel_orm_generator.test.models.user;
// **************************************************************************
// OrmGenerator
// **************************************************************************
class UserQuery extends Query<User, UserQueryWhere> {
@override
final UserQueryValues values = new UserQueryValues();
@override
final UserQueryWhere where = new UserQueryWhere();
@override
get tableName {
return 'users';
}
@override
get fields {
return const [
'id',
'username',
'password',
'email',
'created_at',
'updated_at'
];
}
@override
UserQueryWhere newWhereClause() {
return new UserQueryWhere();
}
static User parseRow(List row) {
if (row.every((x) => x == null)) return null;
var model = new User(
id: row[0].toString(),
username: (row[1] as String),
password: (row[2] as String),
email: (row[3] as String),
createdAt: (row[4] as DateTime),
updatedAt: (row[5] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}
class UserQueryWhere extends QueryWhere {
final NumericSqlExpressionBuilder<int> id =
new NumericSqlExpressionBuilder<int>('id');
final StringSqlExpressionBuilder username =
new StringSqlExpressionBuilder('username');
final StringSqlExpressionBuilder password =
new StringSqlExpressionBuilder('password');
final StringSqlExpressionBuilder email =
new StringSqlExpressionBuilder('email');
final DateTimeSqlExpressionBuilder createdAt =
new DateTimeSqlExpressionBuilder('created_at');
final DateTimeSqlExpressionBuilder updatedAt =
new DateTimeSqlExpressionBuilder('updated_at');
@override
get expressionBuilders {
return [id, username, password, email, createdAt, updatedAt];
}
}
class UserQueryValues extends MapQueryValues {
int get id {
return (values['id'] as int);
}
void set id(int value) => values['id'] = value;
String get username {
return (values['username'] as String);
}
void set username(String value) => values['username'] = value;
String get password {
return (values['password'] as String);
}
void set password(String value) => values['password'] = value;
String get email {
return (values['email'] as String);
}
void set email(String value) => values['email'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
void set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
void set updatedAt(DateTime value) => values['updated_at'] = value;
void copyFrom(User model) {
values.addAll({
'username': model.username,
'password': model.password,
'email': model.email,
'created_at': model.createdAt,
'updated_at': model.updatedAt
});
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@ -13,10 +131,8 @@ class User extends _User {
this.username,
this.password,
this.email,
List<Role> roles,
this.createdAt,
this.updatedAt})
: this.roles = new List.unmodifiable(roles ?? []);
this.updatedAt});
@override
final String id;
@ -30,9 +146,6 @@ class User extends _User {
@override
final String email;
@override
final List<Role> roles;
@override
final DateTime createdAt;
@ -44,7 +157,6 @@ class User extends _User {
String username,
String password,
String email,
List<Role> roles,
DateTime createdAt,
DateTime updatedAt}) {
return new User(
@ -52,7 +164,6 @@ class User extends _User {
username: username ?? this.username,
password: password ?? this.password,
email: email ?? this.email,
roles: roles ?? this.roles,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
@ -63,16 +174,13 @@ class User extends _User {
other.username == username &&
other.password == password &&
other.email == email &&
const ListEquality<Role>(const DefaultEquality<Role>())
.equals(other.roles, roles) &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects(
[id, username, password, email, roles, createdAt, updatedAt]);
return hashObjects([id, username, password, email, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {

View file

@ -13,11 +13,6 @@ abstract class UserSerializer {
username: map['username'] as String,
password: map['password'] as String,
email: map['email'] as String,
roles: map['roles'] is Iterable
? new List.unmodifiable(((map['roles'] as Iterable)
.where((x) => x is Map) as Iterable<Map>)
.map(RoleSerializer.fromMap))
: null,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
@ -39,7 +34,6 @@ abstract class UserSerializer {
'username': model.username,
'password': model.password,
'email': model.email,
'roles': model.roles?.map((m) => m.toJson())?.toList(),
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
@ -52,7 +46,6 @@ abstract class UserFields {
username,
password,
email,
roles,
createdAt,
updatedAt
];
@ -65,8 +58,6 @@ abstract class UserFields {
static const String email = 'email';
static const String roles = 'roles';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';