diff --git a/.idea/modules.xml b/.idea/modules.xml
index 1d73e682..0d8a8dd2 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/postgres.iml b/.idea/orm.iml
similarity index 64%
rename from .idea/postgres.iml
rename to .idea/orm.iml
index ab4131d2..6f2622c5 100644
--- a/.idea/postgres.iml
+++ b/.idea/orm.iml
@@ -12,14 +12,6 @@
-
-
-
-
-
-
-
-
diff --git a/.idea/runConfigurations/serialize__build_dart.xml b/.idea/runConfigurations/serialize__build_dart.xml
deleted file mode 100644
index e50cadde..00000000
--- a/.idea/runConfigurations/serialize__build_dart.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/lib/builder.dart b/lib/builder.dart
index e18c1adf..be464374 100644
--- a/lib/builder.dart
+++ b/lib/builder.dart
@@ -1 +1,2 @@
+export 'src/builder/postgres/migration.dart';
export 'src/builder/postgres/postgres.dart';
\ No newline at end of file
diff --git a/lib/src/builder/find_annotation.dart b/lib/src/builder/find_annotation.dart
deleted file mode 100644
index c7b65d2a..00000000
--- a/lib/src/builder/find_annotation.dart
+++ /dev/null
@@ -1,8 +0,0 @@
-import 'package:analyzer/dart/element/element.dart';
-import 'package:source_gen/src/annotation.dart';
-
-T findAnnotation(FieldElement field, Type outType) {
- var first = field.metadata
- .firstWhere((ann) => matchAnnotation(outType, ann), orElse: () => null);
- return first == null ? null : instantiateAnnotation(first);
-}
diff --git a/lib/src/builder/postgres/build_context.dart b/lib/src/builder/postgres/build_context.dart
index 645159a0..5a678305 100644
--- a/lib/src/builder/postgres/build_context.dart
+++ b/lib/src/builder/postgres/build_context.dart
@@ -1,80 +1,87 @@
import 'package:analyzer/dart/element/element.dart';
-import 'package:angel_serialize/angel_serialize.dart';
+import 'package:angel_serialize/build_context.dart' as serialize;
+import 'package:angel_serialize/context.dart' as serialize;
import 'package:build/build.dart';
import 'package:inflection/inflection.dart';
-import 'package:path/path.dart' as p;
import 'package:recase/recase.dart';
import '../../annotations.dart';
import '../../migration.dart';
import '../../relations.dart';
-import '../find_annotation.dart';
+import 'package:angel_serialize/src/find_annotation.dart';
+import 'package:source_gen/src/annotation.dart';
import 'postgres_build_context.dart';
// TODO: Should add id, createdAt, updatedAt...
-PostgresBuildContext buildContext(ClassElement clazz, ORM annotation,
- BuildStep buildStep, bool autoSnakeCaseNames) {
- var ctx = new PostgresBuildContext(annotation,
- originalClassName: clazz.name,
+PostgresBuildContext buildContext(
+ ClassElement clazz,
+ ORM annotation,
+ BuildStep buildStep,
+ Resolver resolver,
+ bool autoSnakeCaseNames,
+ bool autoIdAndDateFields) {
+ var raw = serialize.buildContext(clazz, null, buildStep, resolver,
+ autoSnakeCaseNames != false, autoIdAndDateFields != false);
+ var ctx = new PostgresBuildContext(raw, annotation, resolver, buildStep,
tableName: annotation.tableName?.isNotEmpty == true
? annotation.tableName
- : pluralize(new ReCase(clazz.name).snakeCase),
- sourceFilename: p.basename(buildStep.inputId.path));
+ : pluralize(new ReCase(clazz.name).snakeCase));
+ List fieldNames = [];
- for (var field in clazz.fields) {
- if (field.getter != null && field.setter != null) {
- // Check for relationship. If so, skip.
- Relationship relationship = findAnnotation(field, HasOne) ??
+ for (var field in raw.fields) {
+ fieldNames.add(field.name);
+ // Check for relationship. If so, skip.
+ Relationship relationship = null;
+ /* findAnnotation(field, HasOne) ??
findAnnotation(field, HasMany) ??
- findAnnotation(field, BelongsTo);
+ findAnnotation(field, BelongsTo);*/
+ bool isRelationship = field.metadata.any((ann) {
+ return matchAnnotation(Relationship, ann) ||
+ matchAnnotation(HasMany, ann) ||
+ matchAnnotation(HasOne, ann) ||
+ matchAnnotation(BelongsTo, ann);
+ });
- if (relationship != null) {
- ctx.relationships[field.name] = relationship;
- continue;
- } else print('Hm: ${field.name}');
- // Check for alias
- var alias = findAnnotation(field, Alias);
-
- if (alias?.name?.isNotEmpty == true) {
- ctx.aliases[field.name] = alias.name;
- } else if (autoSnakeCaseNames != false) {
- ctx.aliases[field.name] = new ReCase(field.name).snakeCase;
- }
-
- // Check for column annotation...
- var column = findAnnotation(field, Column);
-
- if (column == null) {
- // Guess what kind of column this is...
- switch (field.type.name) {
- case 'String':
- column = const Column(type: ColumnType.VAR_CHAR);
- break;
- case 'int':
- column = const Column(type: ColumnType.INT);
- break;
- case 'double':
- column = const Column(type: ColumnType.DECIMAL);
- break;
- case 'num':
- column = const Column(type: ColumnType.NUMERIC);
- break;
- case 'num':
- column = const Column(type: ColumnType.NUMERIC);
- break;
- case 'bool':
- column = const Column(type: ColumnType.BIT);
- break;
- case 'DateTime':
- column = const Column(type: ColumnType.DATE_TIME);
- break;
- }
- }
-
- if (column == null)
- throw 'Cannot infer SQL column type for field "${field.name}" with type "${field.type.name}".';
- ctx.columnInfo[field.name] = column;
- ctx.fields.add(field);
+ if (relationship != null) {
+ ctx.relationships[field.name] = relationship;
+ continue;
+ } else if (isRelationship) {
+ ctx.relationships[field.name] = null;
+ continue;
}
+
+ // Check for column annotation...
+ var column = findAnnotation(field, Column);
+
+ if (column == null) {
+ // Guess what kind of column this is...
+ switch (field.type.name) {
+ case 'String':
+ column = const Column(type: ColumnType.VAR_CHAR);
+ break;
+ case 'int':
+ column = const Column(type: ColumnType.INT);
+ break;
+ case 'double':
+ column = const Column(type: ColumnType.DECIMAL);
+ break;
+ case 'num':
+ column = const Column(type: ColumnType.NUMERIC);
+ break;
+ case 'num':
+ column = const Column(type: ColumnType.NUMERIC);
+ break;
+ case 'bool':
+ column = const Column(type: ColumnType.BIT);
+ break;
+ case 'DateTime':
+ column = const Column(type: ColumnType.TIME_STAMP);
+ break;
+ }
+ }
+
+ if (column == null)
+ throw 'Cannot infer SQL column type for field "${field.name}" with type "${field.type.name}".';
+ ctx.columnInfo[field.name] = column;
}
return ctx;
diff --git a/lib/src/builder/postgres/migration.dart b/lib/src/builder/postgres/migration.dart
new file mode 100644
index 00000000..d880d29d
--- /dev/null
+++ b/lib/src/builder/postgres/migration.dart
@@ -0,0 +1,105 @@
+import 'dart:async';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:angel_serialize/angel_serialize.dart';
+import 'package:build/build.dart';
+import 'package:code_builder/dart/async.dart';
+import 'package:code_builder/dart/core.dart';
+import 'package:code_builder/code_builder.dart';
+import 'package:inflection/inflection.dart';
+import 'package:path/path.dart' as p;
+import 'package:recase/recase.dart';
+import 'package:source_gen/src/annotation.dart';
+import 'package:source_gen/src/utils.dart';
+import 'package:source_gen/source_gen.dart';
+import '../../annotations.dart';
+import '../../migration.dart';
+import 'package:angel_serialize/src/find_annotation.dart';
+import 'build_context.dart';
+import 'postgres_build_context.dart';
+
+// TODO: HasOne, HasMany, BelongsTo
+class SQLMigrationGenerator implements Builder {
+ /// If "true" (default), then field names will automatically be (de)serialized as snake_case.
+ final bool autoSnakeCaseNames;
+
+ /// If "true" (default), then
+ final bool autoIdAndDateFields;
+
+ const SQLMigrationGenerator(
+ {this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
+
+ @override
+ Map> get buildExtensions => {
+ '.dart': ['.up.g.sql', '.down.g.sql']
+ };
+
+ @override
+ Future build(BuildStep buildStep) async {
+ var resolver = await buildStep.resolver;
+ var up = new StringBuffer();
+ var down = new StringBuffer();
+
+ if (!await resolver.isLibrary(buildStep.inputId)) {
+ return;
+ }
+
+ var lib = await resolver.getLibrary(buildStep.inputId);
+ var elements = getElementsFromLibraryElement(lib);
+
+ if (!elements.any(
+ (el) => el.metadata.any((ann) => matchAnnotation(ORM, ann)))) return;
+
+ generateSqlMigrations(lib, resolver, buildStep, up, down);
+ buildStep.writeAsString(
+ buildStep.inputId.changeExtension('.up.g.sql'), up.toString());
+ buildStep.writeAsString(
+ buildStep.inputId.changeExtension('.down.g.sql'), down.toString());
+ }
+
+ void generateSqlMigrations(LibraryElement libraryElement, Resolver resolver,
+ BuildStep buildStep, StringBuffer up, StringBuffer down) {
+ List done = [];
+ for (var element in getElementsFromLibraryElement(libraryElement)) {
+ if (element is ClassElement && !done.contains(element.name)) {
+ var ann = element.metadata
+ .firstWhere((a) => matchAnnotation(ORM, a), orElse: () => null);
+ if (ann != null) {
+ var ctx = buildContext(
+ element,
+ instantiateAnnotation(ann),
+ buildStep,
+ resolver,
+ autoSnakeCaseNames != false,
+ autoIdAndDateFields != false);
+ buildUpMigration(ctx, up);
+ buildDownMigration(ctx, down);
+ done.add(element.name);
+ }
+ }
+ }
+ }
+
+ void buildUpMigration(PostgresBuildContext ctx, StringBuffer buf) {
+ buf.writeln('CREATE TABLE "${ctx.tableName}" (');
+
+ int i = 0;
+ ctx.columnInfo.forEach((name, col) {
+ if (i++ > 0) buf.writeln(',');
+ var key = ctx.resolveFieldName(name);
+ buf.write(' "$key" ${col.type.name}');
+
+ if (col.index == IndexType.PRIMARY_KEY)
+ buf.write(' PRIMARY KEY');
+ else if (col.index == IndexType.UNIQUE) buf.write(' UNIQUE');
+
+ if (col.nullable != true) buf.write(' NOT NULLABLE');
+ });
+
+ buf.writeln();
+ buf.writeln(');');
+ }
+
+ void buildDownMigration(PostgresBuildContext ctx, StringBuffer buf) {
+ buf.writeln('DROP TABLE "${ctx.tableName}";');
+ }
+}
diff --git a/lib/src/builder/postgres/postgres.dart b/lib/src/builder/postgres/postgres.dart
index dc778815..d33c9171 100644
--- a/lib/src/builder/postgres/postgres.dart
+++ b/lib/src/builder/postgres/postgres.dart
@@ -8,10 +8,12 @@ import 'package:code_builder/code_builder.dart';
import 'package:inflection/inflection.dart';
import 'package:path/path.dart' as p;
import 'package:recase/recase.dart';
+import 'package:source_gen/src/annotation.dart';
+import 'package:source_gen/src/utils.dart';
import 'package:source_gen/source_gen.dart';
import '../../annotations.dart';
import '../../migration.dart';
-import '../find_annotation.dart';
+import 'package:angel_serialize/src/find_annotation.dart';
import 'build_context.dart';
import 'postgres_build_context.dart';
@@ -20,33 +22,59 @@ class PostgresORMGenerator extends GeneratorForAnnotation {
/// If `true` (default), then field names will automatically be (de)serialized as snake_case.
final bool autoSnakeCaseNames;
- const PostgresORMGenerator({this.autoSnakeCaseNames: true});
+ /// If `true` (default), then
+ final bool autoIdAndDateFields;
+
+ const PostgresORMGenerator(
+ {this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
@override
Future generateForAnnotatedElement(
- Element element, ORM annotation, BuildStep buildStep) {
+ Element element, ORM annotation, BuildStep buildStep) async {
if (element is! ClassElement)
- throw 'Only classes can be annotated with @model.';
- var context =
- buildContext(element, annotation, buildStep, autoSnakeCaseNames);
- return new Future.value(
- prettyToSource(generateOrmLibrary(context).buildAst()));
+ throw 'Only classes can be annotated with @serializable.';
+ var resolver = await buildStep.resolver;
+ return prettyToSource(
+ generateOrmLibrary(element.library, resolver, buildStep).buildAst());
}
- LibraryBuilder generateOrmLibrary(PostgresBuildContext ctx) {
+ LibraryBuilder generateOrmLibrary(
+ LibraryElement libraryElement, Resolver resolver, BuildStep buildStep) {
var lib = new LibraryBuilder();
lib.addDirective(new ImportBuilder('dart:async'));
lib.addDirective(new ImportBuilder('package:angel_orm/angel_orm.dart'));
lib.addDirective(new ImportBuilder('package:postgres/postgres.dart'));
- lib.addDirective(new ImportBuilder(ctx.sourceFilename));
- lib.addMember(buildQueryClass(ctx));
- lib.addMember(buildWhereClass(ctx));
+ lib.addDirective(new ImportBuilder(p.basename(buildStep.inputId.path)));
+
+ List done = [];
+ for (var element in getElementsFromLibraryElement(libraryElement)) {
+ if (element is ClassElement && !done.contains(element.name)) {
+ var ann = element.metadata
+ .firstWhere((a) => matchAnnotation(ORM, a), orElse: () => null);
+ if (ann != null) {
+ var ctx = buildContext(
+ element,
+ instantiateAnnotation(ann),
+ buildStep,
+ resolver,
+ autoSnakeCaseNames != false,
+ autoIdAndDateFields != false);
+ lib.addMember(buildQueryClass(ctx));
+ lib.addMember(buildWhereClass(ctx));
+ done.add(element.name);
+ }
+ }
+ }
return lib;
}
ClassBuilder buildQueryClass(PostgresBuildContext ctx) {
var clazz = new ClassBuilder(ctx.queryClassName);
+ // Add constructor + field
+ var PostgreSQLConnection = new TypeBuilder('PostgreSQLConnection');
+ var connection = reference('connection');
+
// Add or + not
for (var relation in ['and', 'or', 'not']) {
clazz.addField(varFinal('_$relation',
@@ -73,6 +101,12 @@ class PostgresORMGenerator extends GeneratorForAnnotation {
type: new TypeBuilder(ctx.whereClassName),
value: new TypeBuilder(ctx.whereClassName).newInstance([])));
+ // Add toSql()...
+ clazz.addMethod(buildToSqlMethod(ctx));
+
+ // Add parseRow()...
+ clazz.addMethod(buildParseRowMethod(ctx), asStatic: true);
+
// Add get()...
clazz.addMethod(buildGetMethod(ctx));
@@ -94,16 +128,77 @@ class PostgresORMGenerator extends GeneratorForAnnotation {
returnType: new TypeBuilder('Stream',
genericTypes: [new TypeBuilder(ctx.modelClassName)]),
returns: new TypeBuilder(ctx.queryClassName)
- .newInstance([]).invoke('get', [])),
+ .newInstance([]).invoke('get', [connection]))
+ ..addPositional(parameter('connection', [PostgreSQLConnection])),
asStatic: true);
return clazz;
}
+ MethodBuilder buildToSqlMethod(PostgresBuildContext ctx) {
+ // TODO: Bake relations into SQL queries
+ var meth = new MethodBuilder('toSql', returnType: lib$core.String);
+ return meth;
+ }
+
+ MethodBuilder buildParseRowMethod(PostgresBuildContext ctx) {
+ var meth = new MethodBuilder('parseRow',
+ returnType: new TypeBuilder(ctx.modelClassName));
+ meth.addPositional(parameter('row', [lib$core.List]));
+ var row = reference('row');
+ var DATE_YMD_HMS = reference('DATE_YMD_HMS');
+
+ // We want to create a Map using the SQL row.
+ Map data = {};
+
+ int i = 0;
+
+ // TODO: Support relations...
+ ctx.fields.forEach((field) {
+ var name = ctx.resolveFieldName(field.name);
+ var rowKey = row[literal(i++)];
+
+ if (field.type.name == 'DateTime') {
+ // TODO: Handle DATE and not just DATETIME
+ data[name] = DATE_YMD_HMS.invoke('parse', [rowKey]);
+ } else if (field.name == 'id' && ctx.shimmed.containsKey('id')) {
+ data[name] = rowKey.invoke('toString', []);
+ } else if (field.type.isAssignableTo(ctx.typeProvider.boolType)) {
+ data[name] = rowKey.equals(literal(1));
+ } else
+ data[name] = rowKey;
+ });
+
+ // Then, call a .fromJson() constructor
+ meth.addStatement(new TypeBuilder(ctx.modelClassName)
+ .newInstance([map(data)], constructor: 'fromJson').asReturn());
+
+ return meth;
+ }
+
MethodBuilder buildGetMethod(PostgresBuildContext ctx) {
var meth = new MethodBuilder('get',
returnType: new TypeBuilder('Stream',
genericTypes: [new TypeBuilder(ctx.modelClassName)]));
+ meth.addPositional(
+ parameter('connection', [new TypeBuilder('PostgreSQLConnection')]));
+ var streamController = new TypeBuilder('StreamController',
+ genericTypes: [new TypeBuilder(ctx.modelClassName)]);
+ var ctrl = reference('ctrl'), connection = reference('connection');
+ meth.addStatement(varField('ctrl',
+ type: streamController, value: streamController.newInstance([])));
+
+ // Invoke query...
+ var future = connection.invoke('query', [reference('toSql').call([])]);
+ var catchError = ctrl.property('addError');
+ var then = new MethodBuilder.closure()..addPositional(parameter('rows'));
+ then.addStatement(reference('rows')
+ .invoke('map', [reference('parseRow')]).invoke(
+ 'forEach', [ctrl.property('add')]));
+ then.addStatement(ctrl.invoke('close', []));
+ meth.addStatement(
+ future.invoke('then', [then]).invoke('catchError', [catchError]));
+ meth.addStatement(ctrl.property('stream').asReturn());
return meth;
}
@@ -111,6 +206,19 @@ class PostgresORMGenerator extends GeneratorForAnnotation {
var meth = new MethodBuilder('getOne',
returnType: new TypeBuilder('Future',
genericTypes: [new TypeBuilder(ctx.modelClassName)]));
+ meth.addPositional(parameter('id', [lib$core.int]));
+ meth.addPositional(
+ parameter('connection', [new TypeBuilder('PostgreSQLConnection')]));
+ meth.addStatement(reference('connection').invoke('query', [
+ literal('SELECT * FROM `${ctx.tableName}` WHERE `id` = @id;')
+ ], namedArguments: {
+ 'substitutionValues': map({'id': reference('id')})
+ }).invoke('then', [
+ new MethodBuilder.closure(
+ returns:
+ reference('parseRow').call([reference('rows').property('first')]))
+ ..addPositional(parameter('rows'))
+ ]).asReturn());
return meth;
}
@@ -129,7 +237,9 @@ class PostgresORMGenerator extends GeneratorForAnnotation {
}
MethodBuilder buildInsertMethod(PostgresBuildContext ctx) {
+ // TODO: Auto-set createdAt, updatedAt...
var meth = new MethodBuilder('insert',
+ modifier: MethodModifier.asAsync,
returnType: new TypeBuilder('Future',
genericTypes: [new TypeBuilder(ctx.modelClassName)]));
meth.addPositional(
@@ -145,6 +255,96 @@ class PostgresORMGenerator extends GeneratorForAnnotation {
meth.addNamed(p);
});
+ var buf = new StringBuffer('INSERT INTO `${ctx.tableName}` (');
+ for (int i = 0; i < ctx.fields.length; i++) {
+ if (i > 0) buf.write(', ');
+ var key = ctx.resolveFieldName(ctx.fields[i].name);
+ buf.write('`$key`');
+ }
+
+ buf.write(' VALUES (');
+ for (int i = 0; i < ctx.fields.length; i++) {
+ if (i > 0) buf.write(', ');
+ buf.write('@${ctx.fields[i].name}');
+ }
+
+ buf.write(');');
+
+ Map substitutionValues = {};
+ ctx.fields.forEach((field) {
+ substitutionValues[field.name] = reference(field.name);
+ });
+
+ /*
+ // Create StringBuffer
+ meth.addStatement(varField('buf',
+ type: lib$core.StringBuffer,
+ value: lib$core.StringBuffer.newInstance([])));
+ var buf = reference('buf');
+
+ // Create "INSERT INTO segment"
+ var fieldNames = ctx.fields
+ .map((f) => ctx.resolveFieldName(f.name))
+ .map((k) => '`$k`')
+ .join(', ');
+ var insertInto =
+ literal('INSERT INTO `${ctx.tableName}` ($fieldNames) VALUES (');
+ meth.addStatement(buf.invoke('write', [insertInto]));
+
+ // Write all fields
+ int i = 0;
+ var backtick = literal('`');
+ var numType = ctx.typeProvider.numType;
+ var boolType = ctx.typeProvider.boolType;
+ ctx.fields.forEach((field) {
+ var ref = reference(field.name);
+ ExpressionBuilder value;
+
+ // Handle numbers
+ if (field.type.isAssignableTo(numType)) {
+ value = ref;
+ }
+
+ // Handle boolean
+ else if (field.type.isAssignableTo(numType)) {
+ value = ref.equals(literal(true)).ternary(literal(1), literal(0));
+ }
+
+ // Handle DateTime
+ else if (field.type.isAssignableTo(ctx.dateTimeType)) {
+ // TODO: DATE and not just DATETIME
+ value = reference('DATE_YMD_HMS').invoke('format', [ref]);
+ }
+
+ // Handle anything else...
+ // TODO: Escape SQL strings???
+ else {
+ value = backtick + (ref.invoke('toString', [])) + backtick;
+ }
+
+ if (i++ > 0) meth.addStatement(buf.invoke('write', [literal(', ')]));
+ meth.addStatement(ifThen(ref.equals(literal(null)), [
+ buf.invoke('write', [literal('NULL')]),
+ elseThen([
+ buf.invoke('write', [value])
+ ])
+ ]));
+ });
+
+ // Finalize buffer
+ meth.addStatement(buf.invoke('write', [literal(');')]));
+ meth.addStatement(varField('query', value: buf.invoke('toString', [])));*/
+
+ var connection = reference('connection');
+ var query = literal(buf.toString());
+ var result = reference('result');
+ meth.addStatement(varField('result',
+ value: connection.invoke('query', [
+ query
+ ], namedArguments: {
+ 'substitutionValues': map(substitutionValues)
+ }).asAwait()));
+ meth.addStatement(reference('parseRow').call([result]).asReturn());
return meth;
}
diff --git a/lib/src/builder/postgres/postgres_build_context.dart b/lib/src/builder/postgres/postgres_build_context.dart
index c18b86b0..74a3da44 100644
--- a/lib/src/builder/postgres/postgres_build_context.dart
+++ b/lib/src/builder/postgres/postgres_build_context.dart
@@ -1,29 +1,56 @@
import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/src/generated/resolver.dart';
+import 'package:build/build.dart';
+import 'package:angel_serialize/context.dart';
import '../../annotations.dart';
import '../../migration.dart';
import '../../relations.dart';
-class PostgresBuildContext {
- final Map aliases = {};
+class PostgresBuildContext extends BuildContext {
+ DartType _dateTimeTypeCache;
+ LibraryElement _libraryCache;
+ TypeProvider _typeProviderCache;
final Map columnInfo = {};
final Map indices = {};
final Map relationships = {};
- final String originalClassName, tableName, sourceFilename;
- final ORM annotation;
- // Todo: We can use analyzer to copy straight from Model class
- final List fields = [];
+ final String tableName;
+ final ORM ormAnnotation;
+ final BuildContext raw;
+ final Resolver resolver;
+ final BuildStep buildStep;
String primaryKeyName = 'id';
- PostgresBuildContext(this.annotation,
- {this.originalClassName, this.tableName, this.sourceFilename});
+ PostgresBuildContext(
+ this.raw, this.ormAnnotation, this.resolver, this.buildStep,
+ {this.tableName})
+ : super(raw.annotation,
+ originalClassName: raw.originalClassName,
+ sourceFilename: raw.sourceFilename);
- String get modelClassName => originalClassName.startsWith('_')
- ? originalClassName.substring(1)
- : originalClassName;
+ List get fields => raw.fields;
+
+ Map get aliases => raw.aliases;
+
+ Map get shimmed => raw.shimmed;
+
+ String get sourceFilename => raw.sourceFilename;
+
+ String get modelClassName => raw.modelClassName;
+
+ String get originalClassName => raw.originalClassName;
String get queryClassName => modelClassName + 'Query';
String get whereClassName => queryClassName + 'Where';
- String resolveFieldName(String name) =>
- aliases.containsKey(name) ? aliases[name] : name;
+ LibraryElement get library =>
+ _libraryCache ??= resolver.getLibrary(buildStep.inputId);
+
+ DartType get dateTimeType => _dateTimeTypeCache ??= (resolver.libraries
+ .firstWhere((lib) => lib.isDartCore)
+ .getType('DateTime')
+ .type);
+
+ TypeProvider get typeProvider =>
+ _typeProviderCache ??= library.context.typeProvider;
}
diff --git a/lib/src/query.dart b/lib/src/query.dart
index db34d555..8fc6ac04 100644
--- a/lib/src/query.dart
+++ b/lib/src/query.dart
@@ -1,5 +1,9 @@
import 'package:intl/intl.dart';
+
+final DateFormat DATE_YMD = new DateFormat('yyyy-MM-dd');
+final DateFormat DATE_YMD_HMS = new DateFormat('yyyy-MM-dd HH:mm:ss');
+
abstract class SqlExpressionBuilder {
bool get hasValue;
String compile();
@@ -123,8 +127,6 @@ class BooleanSqlExpressionBuilder implements SqlExpressionBuilder {
}
class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder {
- static final DateFormat _ymd = new DateFormat('yy-MM-dd');
- static final DateFormat _ymdHms = new DateFormat('yy-MM-dd HH:mm:ss');
final NumericSqlExpressionBuilder year =
new NumericSqlExpressionBuilder(),
month = new NumericSqlExpressionBuilder(),
@@ -148,7 +150,7 @@ class DateTimeSqlExpressionBuilder implements SqlExpressionBuilder {
second.hasValue;
bool _change(String _op, DateTime dt, bool time) {
- var dateString = time ? _ymdHms.format(dt) : _ymd.format(dt);
+ var dateString = time ? DATE_YMD_HMS.format(dt) : DATE_YMD.format(dt);
_raw = '`$columnName` $_op \'$dateString\'';
return true;
}
diff --git a/lib/src/relations.dart b/lib/src/relations.dart
index 87395c11..38f0ec7c 100644
--- a/lib/src/relations.dart
+++ b/lib/src/relations.dart
@@ -13,7 +13,7 @@ class Relationship {
class HasMany extends Relationship {
const HasMany(
- {String localKey,
+ {String localKey: 'id',
String foreignKey,
String foreignTable,
bool cascadeOnDelete: false})
@@ -28,7 +28,7 @@ const HasMany hasMany = const HasMany();
class HasOne extends Relationship {
const HasOne(
- {String localKey,
+ {String localKey: 'id',
String foreignKey,
String foreignTable,
bool cascadeOnDelete: false})
@@ -42,7 +42,8 @@ class HasOne extends Relationship {
const HasOne hasOne = const HasOne();
class BelongsTo extends Relationship {
- const BelongsTo({String localKey, String foreignKey, String foreignTable})
+ const BelongsTo(
+ {String localKey: 'id', String foreignKey, String foreignTable})
: super._(
localKey: localKey,
foreignKey: foreignKey,
diff --git a/pubspec.yaml b/pubspec.yaml
index 83219309..c1b09fa9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -20,4 +20,7 @@ dev_dependencies:
build_runner: ^0.3.0
http: ">= 0.11.3 < 0.12.0"
postgres: ">=0.9.5 <1.0.0"
- test: ">= 0.12.13 < 0.13.0"
\ No newline at end of file
+ test: ">= 0.12.13 < 0.13.0"
+dependency_overrides:
+ source_gen:
+ path: ../../Dart/source_gen
\ No newline at end of file
diff --git a/test/car_test.dart b/test/car_test.dart
index 6f722201..753baa2d 100644
--- a/test/car_test.dart
+++ b/test/car_test.dart
@@ -1,3 +1,6 @@
+import 'dart:io';
+import 'package:angel_orm/angel_orm.dart';
+import 'package:postgres/postgres.dart';
import 'package:test/test.dart';
import 'models/car.dart';
import 'models/car.orm.g.dart';
@@ -5,6 +8,23 @@ import 'models/car.orm.g.dart';
final DateTime MILENNIUM = new DateTime.utc(2000, 1, 1);
main() {
+ PostgreSQLConnection connection;
+
+ setUp(() async {
+ connection = new PostgreSQLConnection('127.0.0.1', 0, '');
+ await connection.open();
+
+ // Create temp table
+ var query = await new File('test/models/car.sql').readAsString();
+ await connection.execute(query);
+ });
+
+ tearDown(() async {
+ // Drop `cars`
+ await connection.execute('DROP TABLE `cars`;');
+ await connection.close();
+ });
+
test('to where', () {
var query = new CarQuery();
query.where
@@ -12,11 +32,34 @@ main() {
..recalledAt.lessThanOrEqualTo(MILENNIUM, includeTime: false);
var whereClause = query.where.toWhereClause();
print('Where clause: $whereClause');
- expect(whereClause, "WHERE `family_friendly` = 1 AND `recalled_at` <= '00-01-01'");
+ expect(whereClause,
+ "WHERE `family_friendly` = 1 AND `recalled_at` <= '00-01-01'");
});
- test('insert', () async {
- var car = await CarQuery.insert(null, make: 'Mazda', familyFriendly: false);
+ test('parseRow', () {
+ var row = [
+ 0,
+ 'Mazda',
+ 'CX9',
+ 1,
+ DATE_YMD_HMS.format(MILENNIUM),
+ DATE_YMD_HMS.format(MILENNIUM),
+ DATE_YMD_HMS.format(MILENNIUM)
+ ];
+ print(row);
+ var car = CarQuery.parseRow(row);
print(car.toJson());
- }, skip: 'Insert not yet implemented');
+ expect(car.id, '0');
+ expect(car.make, 'Mazda');
+ expect(car.description, 'CX9');
+ expect(car.familyFriendly, true);
+ expect(MILENNIUM.toIso8601String(),
+ startsWith(car.recalledAt.toIso8601String()));
+ expect(MILENNIUM.toIso8601String(),
+ startsWith(car.createdAt.toIso8601String()));
+ expect(MILENNIUM.toIso8601String(),
+ startsWith(car.updatedAt.toIso8601String()));
+ });
+
+ test('insert', () async {});
}
diff --git a/test/models/car.dart b/test/models/car.dart
index 88755d34..bfbb536d 100644
--- a/test/models/car.dart
+++ b/test/models/car.dart
@@ -3,7 +3,6 @@ library angel_orm.test.models.car;
import 'package:angel_framework/common.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_serialize/angel_serialize.dart';
-import 'tire.dart';
part 'car.g.dart';
@serializable
@@ -13,6 +12,4 @@ class _Car extends Model {
String description;
bool familyFriendly;
DateTime recalledAt;
- @hasMany
- List tires;
}
diff --git a/test/models/car.down.g.sql b/test/models/car.down.g.sql
new file mode 100644
index 00000000..b6acf5bc
--- /dev/null
+++ b/test/models/car.down.g.sql
@@ -0,0 +1 @@
+DROP TABLE "cars";
diff --git a/test/models/car.g.dart b/test/models/car.g.dart
index 79f3eb6b..2425d12e 100644
--- a/test/models/car.g.dart
+++ b/test/models/car.g.dart
@@ -8,6 +8,9 @@ part of angel_orm.test.models.car;
// **************************************************************************
class Car extends _Car {
+ @override
+ String id;
+
@override
String make;
@@ -21,34 +24,51 @@ class Car extends _Car {
DateTime recalledAt;
@override
- List tires;
+ DateTime createdAt;
+
+ @override
+ DateTime updatedAt;
Car(
- {this.make,
+ {this.id,
+ this.make,
this.description,
this.familyFriendly,
this.recalledAt,
- this.tires});
+ this.createdAt,
+ this.updatedAt});
factory Car.fromJson(Map data) {
return new Car(
+ id: data['id'],
make: data['make'],
description: data['description'],
- familyFriendly: data['familyFriendly'],
- recalledAt: data['recalledAt'] is DateTime
- ? data['recalledAt']
- : (data['recalledAt'] is String
- ? DateTime.parse(data['recalledAt'])
+ familyFriendly: data['family_friendly'],
+ recalledAt: data['recalled_at'] is DateTime
+ ? data['recalled_at']
+ : (data['recalled_at'] is String
+ ? DateTime.parse(data['recalled_at'])
: null),
- tires: data['tires']);
+ createdAt: data['created_at'] is DateTime
+ ? data['created_at']
+ : (data['created_at'] is String
+ ? DateTime.parse(data['created_at'])
+ : null),
+ updatedAt: data['updated_at'] is DateTime
+ ? data['updated_at']
+ : (data['updated_at'] is String
+ ? DateTime.parse(data['updated_at'])
+ : null));
}
Map toJson() => {
+ 'id': id,
'make': make,
'description': description,
- 'familyFriendly': familyFriendly,
- 'recalledAt': recalledAt == null ? null : recalledAt.toIso8601String(),
- 'tires': tires
+ 'family_friendly': familyFriendly,
+ 'recalled_at': recalledAt == null ? null : recalledAt.toIso8601String(),
+ 'created_at': createdAt == null ? null : createdAt.toIso8601String(),
+ 'updated_at': updatedAt == null ? null : updatedAt.toIso8601String()
};
static Car parse(Map map) => new Car.fromJson(map);
diff --git a/test/models/car.orm.g.dart b/test/models/car.orm.g.dart
index 932f73b0..4b9945b4 100644
--- a/test/models/car.orm.g.dart
+++ b/test/models/car.orm.g.dart
@@ -5,6 +5,142 @@
// Target: class _Car
// **************************************************************************
-// Error: type 'SuperConstructorInvocationImpl' is not a subtype of type 'ConstructorFieldInitializer' in type cast where
-// SuperConstructorInvocationImpl is from package:analyzer/src/dart/ast/ast.dart
-// ConstructorFieldInitializer is from package:analyzer/dart/ast/ast.dart
+import 'dart:async';
+import 'package:angel_orm/angel_orm.dart';
+import 'package:postgres/postgres.dart';
+import 'car.dart';
+
+class CarQuery {
+ final List _and = [];
+
+ final List _or = [];
+
+ final List _not = [];
+
+ final CarQueryWhere where = new CarQueryWhere();
+
+ void and(CarQuery other) {
+ var compiled = other.where.toWhereClause();
+ if (compiled != null) {
+ _and.add(compiled);
+ }
+ }
+
+ void or(CarQuery other) {
+ var compiled = other.where.toWhereClause();
+ if (compiled != null) {
+ _or.add(compiled);
+ }
+ }
+
+ void not(CarQuery other) {
+ var compiled = other.where.toWhereClause();
+ if (compiled != null) {
+ _not.add(compiled);
+ }
+ }
+
+ String toSql() {}
+
+ static Car parseRow(List row) {
+ return new Car.fromJson({
+ 'id': row[0].toString(),
+ 'make': row[1],
+ 'description': row[2],
+ 'family_friendly': row[3] == 1,
+ 'recalled_at': DATE_YMD_HMS.parse(row[4]),
+ 'created_at': DATE_YMD_HMS.parse(row[5]),
+ 'updated_at': DATE_YMD_HMS.parse(row[6])
+ });
+ }
+
+ Stream get(PostgreSQLConnection connection) {
+ StreamController ctrl = new StreamController();
+ connection.query(toSql()).then((rows) {
+ rows.map(parseRow).forEach(ctrl.add);
+ ctrl.close();
+ }).catchError(ctrl.addError);
+ return ctrl.stream;
+ }
+
+ Future getOne(int id, PostgreSQLConnection connection) {
+ return connection.query('SELECT * FROM `cars` WHERE `id` = @id;',
+ substitutionValues: {'id': id}).then((rows) => parseRow(rows.first));
+ }
+
+ Future update() {}
+
+ Future delete() {}
+
+ static Future insert(PostgreSQLConnection connection,
+ {String id,
+ String make,
+ String description,
+ bool familyFriendly,
+ DateTime recalledAt,
+ DateTime createdAt,
+ DateTime updatedAt}) async {
+ var result = await connection.query(
+ 'INSERT INTO `cars` (`id`, `make`, `description`, `family_friendly`, `recalled_at`, `created_at`, `updated_at` VALUES (@id, @make, @description, @familyFriendly, @recalledAt, @createdAt, @updatedAt);',
+ substitutionValues: {
+ 'id': id,
+ 'make': make,
+ 'description': description,
+ 'familyFriendly': familyFriendly,
+ 'recalledAt': recalledAt,
+ 'createdAt': createdAt,
+ 'updatedAt': updatedAt
+ });
+ return parseRow(result);
+ }
+
+ static Stream getAll(PostgreSQLConnection connection) =>
+ new CarQuery().get(connection);
+}
+
+class CarQueryWhere {
+ final StringSqlExpressionBuilder id = new StringSqlExpressionBuilder();
+
+ final StringSqlExpressionBuilder make = new StringSqlExpressionBuilder();
+
+ final StringSqlExpressionBuilder description =
+ new StringSqlExpressionBuilder();
+
+ final BooleanSqlExpressionBuilder familyFriendly =
+ new BooleanSqlExpressionBuilder();
+
+ final DateTimeSqlExpressionBuilder recalledAt =
+ new DateTimeSqlExpressionBuilder('recalled_at');
+
+ final DateTimeSqlExpressionBuilder createdAt =
+ new DateTimeSqlExpressionBuilder('created_at');
+
+ final DateTimeSqlExpressionBuilder updatedAt =
+ new DateTimeSqlExpressionBuilder('updated_at');
+
+ String toWhereClause() {
+ final List expressions = [];
+ if (id.hasValue) {
+ expressions.add('`id` ' + id.compile());
+ }
+ if (make.hasValue) {
+ expressions.add('`make` ' + make.compile());
+ }
+ if (description.hasValue) {
+ expressions.add('`description` ' + description.compile());
+ }
+ if (familyFriendly.hasValue) {
+ expressions.add('`family_friendly` ' + familyFriendly.compile());
+ }
+ if (recalledAt.hasValue) {
+ expressions.add(recalledAt.compile());
+ }
+ if (createdAt.hasValue) {
+ expressions.add(createdAt.compile());
+ }
+ if (updatedAt.hasValue) {
+ expressions.add(updatedAt.compile());
+ }
+ return expressions.isEmpty ? null : ('WHERE ' + expressions.join(' AND '));
+ }
+}
diff --git a/test/models/car.up.g.sql b/test/models/car.up.g.sql
new file mode 100644
index 00000000..b801d466
--- /dev/null
+++ b/test/models/car.up.g.sql
@@ -0,0 +1,9 @@
+CREATE TABLE "cars" (
+ "id" varchar,
+ "make" varchar,
+ "description" varchar,
+ "family_friendly" bit,
+ "recalled_at" timestamp,
+ "created_at" timestamp,
+ "updated_at" timestamp
+);
diff --git a/test/models/tire.dart b/test/models/tire.dart
deleted file mode 100644
index 9c7825a5..00000000
--- a/test/models/tire.dart
+++ /dev/null
@@ -1,10 +0,0 @@
-library angel_test.test.models.tire;
-
-import 'package:angel_framework/common.dart';
-import 'package:angel_serialize/angel_serialize.dart';
-part 'tire.g.dart';
-
-@serializable
-class _Tire extends Model {
- int size;
-}
diff --git a/test/models/tire.g.dart b/test/models/tire.g.dart
deleted file mode 100644
index bea4fd6a..00000000
--- a/test/models/tire.g.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of angel_test.test.models.tire;
-
-// **************************************************************************
-// Generator: JsonModelGenerator
-// Target: class _Tire
-// **************************************************************************
-
-class Tire extends _Tire {
- @override
- int size;
-
- Tire({this.size});
-
- factory Tire.fromJson(Map data) {
- return new Tire(size: data['size']);
- }
-
- Map toJson() => {'size': size};
-
- static Tire parse(Map map) => new Tire.fromJson(map);
-}
diff --git a/tool/phases.dart b/tool/phases.dart
index 22633c0c..42cec33c 100644
--- a/tool/phases.dart
+++ b/tool/phases.dart
@@ -3,12 +3,14 @@ import 'package:source_gen/source_gen.dart';
import 'package:angel_orm/builder.dart';
import 'package:angel_serialize/builder.dart';
+final InputSet MODELS = new InputSet('angel_orm', const ['test/models/*.dart']);
+
final PhaseGroup PHASES = new PhaseGroup()
..addPhase(new Phase()
- ..addAction(new GeneratorBuilder([const JsonModelGenerator()]),
- new InputSet('angel_orm', const ['test/models/*.dart'])))
+ ..addAction(new GeneratorBuilder([const JsonModelGenerator()]), MODELS))
..addPhase(new Phase()
..addAction(
new GeneratorBuilder([new PostgresORMGenerator()],
isStandalone: true, generatedExtension: '.orm.g.dart'),
- new InputSet('angel_orm', const ['test/models/*.dart'])));
+ MODELS))
+ ..addPhase(new Phase()..addAction(new SQLMigrationGenerator(), MODELS));