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));