From 421099ee9482ab361071797b9aa0be7288bc6bf6 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 18 Jun 2017 00:19:05 -0400 Subject: [PATCH] Work on queries... --- README.md | 84 +++-- lib/angel_orm.dart | 16 +- lib/builder.dart | 297 +----------------- lib/src/annotations.dart | 6 + lib/src/builder/find_annotation.dart | 8 + lib/src/builder/postgres/build_context.dart | 70 +++++ lib/src/builder/postgres/postgres.dart | 224 +++++++++++++ .../postgres/postgres_build_context.dart | 27 ++ lib/src/builder/repository.dart | 296 +++++++++++++++++ lib/src/migration.dart | 104 ++++++ lib/src/query.dart | 194 ++++++++++++ pubspec.yaml | 1 + test/car_test.dart | 22 ++ test/models/car.dart | 20 +- test/models/car.g.dart | 35 ++- test/models/car.orm.g.dart | 248 ++++----------- tool/phases.dart | 2 +- 17 files changed, 1105 insertions(+), 549 deletions(-) create mode 100644 lib/src/annotations.dart create mode 100644 lib/src/builder/find_annotation.dart create mode 100644 lib/src/builder/postgres/build_context.dart create mode 100644 lib/src/builder/postgres/postgres.dart create mode 100644 lib/src/builder/postgres/postgres_build_context.dart create mode 100644 lib/src/builder/repository.dart create mode 100644 lib/src/migration.dart create mode 100644 lib/src/query.dart create mode 100644 test/car_test.dart diff --git a/README.md b/README.md index f4aaf07b..c9d631af 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,23 @@ Your model, courtesy of `package:angel_serialize`: library angel_orm.test.models.car; import 'package:angel_framework/common.dart'; -import 'package:angel_orm/angel_orm.dart' as orm; +import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize/angel_serialize.dart'; part 'car.g.dart'; @serializable -@orm.model +@orm class _Car extends Model { - String manufacturer; - int year; + String make; + String description; + bool familyFriendly; + DateTime recalledAt; } ``` -After building, you'll have access to a `Repository` class with strongly-typed methods that +Models can still use the `@Alias()` annotation. `package:angel_orm` obeys it. + +After building, you'll have access to a `Query` class with strongly-typed methods that allow to run asynchronous queries without a headache. You can run complex queries like: @@ -46,33 +50,67 @@ import 'car.orm.g.dart'; /// Returns an Angel plug-in that connects to a PostgreSQL database, and sets up a controller connected to it... AngelConfigurer connectToCarsTable(PostgreSQLConnection connection) { return (Angel app) async { - // Instantiate a Car repository, which is auto-generated. This class helps us build fluent queries easily. - var cars = new CarRepository(connection); - - // Register it with Angel's dependency injection system. + // Register the connection with Angel's dependency injection system. // // This means that we can use it as a parameter in routes and controllers. - app.container.singleton(cars); + app.container.singleton(connection); // Attach the controller we create below - await app.configure(new CarService(cars)); + await app.configure(new CarService(connection)); }; } @Expose('/cars') class CarService extends Controller { - /// `manufacturerId` and `CarRepository` in this case would be dependency-injected. :) - @Expose('/:manufacturerId/years') - getAllYearsForManufacturer(String manufacturerId, CarRepository cars) { - return - cars - .whereManufacturer(manufacturerId) - .get() - .map((Car car) { - // Cars are deserialized automatically, woohoo! - return car.year; - }) - .toList(); + // The `connection` will be injected. + @Expose('/recalled_since_2008') + carsRecalledSince2008(PostgreSQLConnection connection) { + // Instantiate a Car query, which is auto-generated. This class helps us build fluent queries easily. + var cars = new CarQuery(connection); + cars.where + ..familyFriendly.equals(false) + ..recalledAt.year.greaterThanOrEqualTo(2008); + + // Shorter syntax we could use instead... + cars.where.recalledAt.year <= 2008; + + // `get()` returns a Stream. + // `get().toList()` returns a Future. + return cars.get().toList(); + } + + @Expose('/create', method: 'POST') + createCar(PostgreSQLConnection connection) async { + // `package:angel_orm` generates a strongly-typed `insert` function on the query class. + // Say goodbye to typos!!! + var car = await CarQuery.insert(connection, familyFriendly: true, make: 'Honda'); + + // Auto-serialized using code generated by `package:angel_serialize` + return car; } } +``` + +# Relations +**NOTE**: This is not yet implemented. + +* `@HasOne()` +* `@HasMany()` +* `@BelongsTo()` + +```dart +@serializable +@orm +abstract class _Author extends Model { + @hasMany // Use the defaults, and auto-compute `foreignKey` + List books; + + // Also supports parameters... + @HasMany(localKey: 'id', foreignKey: 'author_id', cascadeOnDelete: true) + List books; + + @Alias('writing_utensil') + @hasOne + Pen pen; +} ``` \ No newline at end of file diff --git a/lib/angel_orm.dart b/lib/angel_orm.dart index fb93ed14..0544ca4b 100644 --- a/lib/angel_orm.dart +++ b/lib/angel_orm.dart @@ -1,13 +1,3 @@ -class Model { - final String tableName; - const Model([this.tableName]); -} - -const Model model = const Model(); - -class Column { - final String name; - const Column([this.name]); -} - -const Column column = const Column(); \ No newline at end of file +export 'src/annotations.dart'; +export 'src/migration.dart'; +export 'src/query.dart'; \ No newline at end of file diff --git a/lib/builder.dart b/lib/builder.dart index 55003668..e18c1adf 100644 --- a/lib/builder.dart +++ b/lib/builder.dart @@ -1,296 +1 @@ -import 'dart:async'; -import 'dart:mirrors'; -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/source_gen.dart'; -import 'package:query_builder_sql/query_builder_sql.dart'; -import 'angel_orm.dart'; - -// TODO: whereXLessThan, greaterThan, etc. - -final RegExp _leadingDot = new RegExp(r'^\.+'); - -const List QUERY_DO_NOT_OVERRIDE = const ['when']; - -typedef Iterable SuperArgumentProvider( - Model model, ClassElement clazz); - -class AngelQueryBuilderGenerator extends GeneratorForAnnotation { - ClassMirror _baseRepositoryClassMirror; - final List _imports = [ - 'dart:async', - 'package:query_builder/query_builder.dart' - ]; - - final Map _constructorParams = {}; - SuperArgumentProvider _superArgProvider; - - AngelQueryBuilderGenerator(Type baseRepositoryQueryClass, - {Iterable additonalImports: const [], - Map constructorParams: const {}, - SuperArgumentProvider superArgProvider}) { - _baseRepositoryClassMirror = reflectClass(baseRepositoryQueryClass); - _imports.addAll(additonalImports ?? []); - _constructorParams.addAll(constructorParams ?? {}); - _superArgProvider = superArgProvider ?? - (annotation, clazz) => [ - literal(annotation.tableName?.isNotEmpty == true - ? annotation.tableName - : pluralize(new ReCase(clazz.name.substring(1)).snakeCase)) - ]; - } - - factory AngelQueryBuilderGenerator.postgresql() => - new AngelQueryBuilderGenerator(SqlRepositoryQuery, constructorParams: { - 'connection': new TypeBuilder('PostgreSQLConnection') - }, additonalImports: [ - 'package:postgres/postgres.dart', - 'package:query_builder_sql/query_builder_sql.dart' - ]); - - @override - Future generateForAnnotatedElement( - Element element, Model annotation, BuildStep buildStep) async { - if (element.kind != ElementKind.CLASS) - throw 'Only classes may be annotated with @model.'; - var lib = generatePostgresLibrary(element, annotation, buildStep.inputId); - return prettyToSource(lib.buildAst()); - } - - LibraryBuilder generatePostgresLibrary( - ClassElement clazz, Model annotation, AssetId inputId) { - if (!clazz.name.startsWith('_')) - throw 'Classes annotated with @model must have names starting with an underscore.'; - var lib = new LibraryBuilder(); - lib.addDirectives(_imports.map((p) => new ImportBuilder(p))); - lib.addDirective(new ImportBuilder(p.basename(inputId.path))); - - // Find all aliases... - Map aliases = {}; - clazz.fields.forEach((field) { - var aliasAnnotation = field.metadata - .firstWhere((ann) => matchAnnotation(Alias, ann), orElse: () => null); - if (aliasAnnotation != null) { - var alias = instantiateAnnotation(aliasAnnotation) as Alias; - aliases[field.name] = alias.name; - } - }); - - lib.addMember(generateRepositoryClass(clazz, aliases)); - lib.addMember(generateRepositoryQueryClass(clazz, annotation, aliases)); - return lib; - } - - ClassBuilder generateRepositoryClass( - ClassElement clazz, Map aliases) { - var genClassName = clazz.name.substring(1) + 'Repository'; - var genQueryClassName = genClassName + 'Query'; - var genClass = new ClassBuilder(genClassName); - var genQueryType = new TypeBuilder(genQueryClassName); - - // Add `connection` field + constructor - - var genConstructor = new ConstructorBuilder(); - _constructorParams.forEach((name, type) { - genClass.addField(varFinal(name, type: type)); - genConstructor.addPositional(parameter(name), asField: true); - }); - genClass.addConstructor(genConstructor); - - // Add an all method - genClass.addMethod(new MethodBuilder('all', - returnType: new TypeBuilder(genQueryClassName), - returns: new TypeBuilder(genQueryClassName) - .newInstance([reference('connection')]))); - - // For each field, add a whereX() method... - clazz.fields - .map((field) => generateWhereFieldMethod( - field, reference('all').call([]), genQueryType, aliases)) - .forEach(genClass.addMethod); - return genClass; - } - - ClassBuilder generateRepositoryQueryClass( - ClassElement clazz, Model annotation, Map aliases) { - var modelClassName = clazz.name.substring(1); - var genClassName = clazz.name.substring(1) + 'RepositoryQuery'; - var genClass = new ClassBuilder(genClassName, - asExtends: new TypeBuilder( - MirrorSystem.getName(_baseRepositoryClassMirror.simpleName), - genericTypes: [new TypeBuilder(modelClassName)])); - var genQueryType = new TypeBuilder(genClassName); - - // Add `connection` field + constructor - - var genConstructor = new ConstructorBuilder( - invokeSuper: _superArgProvider(annotation, clazz)); - _constructorParams.forEach((name, type) { - genClass.addField(varFinal(name, type: type)); - genConstructor.addPositional(parameter(name), asField: true); - }); - genClass.addConstructor(genConstructor); - - // For each field, add a whereX() method... - clazz.fields - .map((field) => generateWhereFieldMethod( - field, explicitThis, genQueryType, aliases)) - .forEach(genClass.addMethod); - - // Add orWhereX() - clazz.fields - .map((f) => generateOrWhereFieldMethod(genQueryType, f)) - .forEach(genClass.addMethod); - - // Override any query methods - _baseRepositoryClassMirror.instanceMembers.forEach((sym, method) { - // Skip setters, etc. - if (!method.isRegularMethod) return; - - // Only if return type contains 'RepositoryQuery' - var methodName = MirrorSystem.getName(sym); - - if (QUERY_DO_NOT_OVERRIDE.contains(methodName)) return; - - var returnTypeName = MirrorSystem.getName(method.returnType.simpleName); - - if (returnTypeName.contains('RepositoryQuery')) { - var overriddenMethod = - new MethodBuilder(methodName, returnType: genQueryType); - // Add @override - overriddenMethod.addAnnotation(lib$core.override); - - // Find all positional and named args - List args = []; - List named = []; - - method.parameters.forEach((param) { - var paramName = MirrorSystem.getName(param.simpleName); - var typeName = MirrorSystem.getName(param.type.simpleName); - var paramType = new TypeBuilder(typeName); - var genParam = parameter(paramName, [paramType]); - - if (!param.isNamed) { - args.add(paramName); - overriddenMethod.addPositional( - param.isOptional ? genParam.asOptional() : genParam); - } else { - overriddenMethod.addNamed(genParam); - named.add(paramName); - } - }); - - // Invoke super - overriddenMethod.addStatement(reference('super') - .invoke(methodName, args.map(reference), - namedArguments: named.fold>( - {}, (out, k) => out..[k] = reference(k))) - .asReturn()); - - genClass.addMethod(overriddenMethod); - } - }); - - // Override toSql to put keys in desired order - // TODO: Override toSql - - // Add get() - genClass.addMethod(generateGetMethod(clazz, modelClassName, aliases)); - - return genClass; - } - - MethodBuilder generateGetMethod( - ClassElement clazz, String modelClassName, Map aliases) { - var meth = new MethodBuilder('get')..addAnnotation(lib$core.override); - - // Map rows to model... - var mapRowsToModel = new MethodBuilder.closure() - ..addPositional(parameter('rows')); - - // First, figure out which rows we fetched... - // - // var requestedKeys = whereFields.keys.isNotEmpty ? whereFields.keys : []; - - var allModelFields = clazz.fields - .map((f) => aliases.containsKey(f.name) ? aliases[f.name] : f.name); - var whereFieldsKeys = reference('whereFields').property('keys'); - - // return new Stream<>.fromFuture(...) - meth.addStatement(lib$async.Stream.newInstance([ - reference('connection') - .invoke('query', [reference('toSql').call([])]).invoke( - 'then', [mapRowsToModel]) - ], constructor: 'fromFuture').asReturn()); - - return meth; - } - - MethodBuilder generateWhereFieldMethod( - FieldElement field, - ExpressionBuilder baseQuery, - TypeBuilder returnType, - Map aliases) { - var rc = new ReCase(field.name); - var whereMethod = - new MethodBuilder('where${rc.pascalCase}', returnType: returnType); - var columnName = - aliases.containsKey(field.name) ? aliases[field.name] : field.name; - whereMethod.addPositional( - parameter(field.name, [new TypeBuilder(field.type.displayName)])); - - if (field.type.name == 'DateTime') { - // Add named `{time: true}` - whereMethod.addNamed( - parameter('time', [lib$core.bool]).asOptional(literal(true))); - // return all().whereDate('x', x, time: time != false); - // return all().where('x', x); - whereMethod.addStatement(baseQuery.invoke('whereDate', [ - literal(columnName), - reference(field.name) - ], namedArguments: { - 'time': reference('time').notEquals(literal(false)) - }).asReturn()); - } else { - // return all().where('x', x); - whereMethod.addStatement(baseQuery.invoke( - 'where', [literal(columnName), reference(field.name)]).asReturn()); - } - - return whereMethod; - } - - MethodBuilder generateOrWhereFieldMethod( - TypeBuilder genQueryClassType, FieldElement field) { - var rc = new ReCase(field.name); - var orWhereMethod = new MethodBuilder('orWhere' + rc.pascalCase, - returnType: genQueryClassType); - orWhereMethod.addPositional( - parameter(field.name, [new TypeBuilder(field.type.displayName)])); - - if (field.type.name == 'DateTime') { - orWhereMethod.addNamed(parameter('time', [lib$core.bool])); - orWhereMethod.addStatement(reference('or').call([ - reference('where' + rc.pascalCase).call([ - reference(field.name) - ], namedArguments: { - 'time': reference('time').notEquals(literal(false)) - }) - ]).asReturn()); - } else { - orWhereMethod.addStatement(reference('or').call([ - reference('where' + rc.pascalCase).call([reference(field.name)]) - ]).asReturn()); - } - - return orWhereMethod; - } -} +export 'src/builder/postgres/postgres.dart'; \ No newline at end of file diff --git a/lib/src/annotations.dart b/lib/src/annotations.dart new file mode 100644 index 00000000..9a263e08 --- /dev/null +++ b/lib/src/annotations.dart @@ -0,0 +1,6 @@ +class ORM { + final String tableName; + const ORM([this.tableName]); +} + +const ORM orm = const ORM(); \ No newline at end of file diff --git a/lib/src/builder/find_annotation.dart b/lib/src/builder/find_annotation.dart new file mode 100644 index 00000000..c7b65d2a --- /dev/null +++ b/lib/src/builder/find_annotation.dart @@ -0,0 +1,8 @@ +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 new file mode 100644 index 00000000..dfe8a8b2 --- /dev/null +++ b/lib/src/builder/postgres/build_context.dart @@ -0,0 +1,70 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:angel_serialize/angel_serialize.dart'; +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 '../find_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, clazz.fields, + originalClassName: clazz.name, + tableName: annotation.tableName?.isNotEmpty == true + ? annotation.tableName + : pluralize(new ReCase(clazz.name).snakeCase), + sourceFilename: p.basename(buildStep.inputId.path)); + + for (var field in clazz.fields) { + if (field.getter != null && field.setter != null) { + // 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; + } + } + + return ctx; +} diff --git a/lib/src/builder/postgres/postgres.dart b/lib/src/builder/postgres/postgres.dart new file mode 100644 index 00000000..dc778815 --- /dev/null +++ b/lib/src/builder/postgres/postgres.dart @@ -0,0 +1,224 @@ +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/source_gen.dart'; +import '../../annotations.dart'; +import '../../migration.dart'; +import '../find_annotation.dart'; +import 'build_context.dart'; +import 'postgres_build_context.dart'; + +// TODO: HasOne, HasMany, BelongsTo +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}); + + @override + Future generateForAnnotatedElement( + Element element, ORM annotation, BuildStep buildStep) { + 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())); + } + + LibraryBuilder generateOrmLibrary(PostgresBuildContext ctx) { + 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)); + return lib; + } + + ClassBuilder buildQueryClass(PostgresBuildContext ctx) { + var clazz = new ClassBuilder(ctx.queryClassName); + + // Add or + not + for (var relation in ['and', 'or', 'not']) { + clazz.addField(varFinal('_$relation', + type: new TypeBuilder('List', genericTypes: [lib$core.String]), + value: list([]))); + var relationMethod = + new MethodBuilder(relation, returnType: lib$core.$void); + relationMethod.addPositional( + parameter('other', [new TypeBuilder(ctx.queryClassName)])); + var otherWhere = reference('other').property('where'); + var compiled = reference('compiled'); + relationMethod.addStatement( + varField('compiled', value: otherWhere.invoke('toWhereClause', []))); + relationMethod.addStatement(ifThen(compiled.notEquals(literal(null)), [ + reference('_$relation').invoke('add', [compiled]) + ])); + clazz.addMethod(relationMethod); + } + + // Add _buildSelectQuery() + + // Add where... + clazz.addField(varFinal('where', + type: new TypeBuilder(ctx.whereClassName), + value: new TypeBuilder(ctx.whereClassName).newInstance([]))); + + // Add get()... + clazz.addMethod(buildGetMethod(ctx)); + + // Add getOne()... + clazz.addMethod(buildGetOneMethod(ctx)); + + // Add update()... + clazz.addMethod(buildUpdateMethod(ctx)); + + // Add remove()... + clazz.addMethod(buildDeleteMethod(ctx)); + + // Add insert()... + clazz.addMethod(buildInsertMethod(ctx), asStatic: true); + + // Add getAll() => new TodoQuery().get(); + clazz.addMethod( + new MethodBuilder('getAll', + returnType: new TypeBuilder('Stream', + genericTypes: [new TypeBuilder(ctx.modelClassName)]), + returns: new TypeBuilder(ctx.queryClassName) + .newInstance([]).invoke('get', [])), + asStatic: true); + + return clazz; + } + + MethodBuilder buildGetMethod(PostgresBuildContext ctx) { + var meth = new MethodBuilder('get', + returnType: new TypeBuilder('Stream', + genericTypes: [new TypeBuilder(ctx.modelClassName)])); + return meth; + } + + MethodBuilder buildGetOneMethod(PostgresBuildContext ctx) { + var meth = new MethodBuilder('getOne', + returnType: new TypeBuilder('Future', + genericTypes: [new TypeBuilder(ctx.modelClassName)])); + return meth; + } + + MethodBuilder buildUpdateMethod(PostgresBuildContext ctx) { + var meth = new MethodBuilder('update', + returnType: new TypeBuilder('Future', + genericTypes: [new TypeBuilder(ctx.modelClassName)])); + return meth; + } + + MethodBuilder buildDeleteMethod(PostgresBuildContext ctx) { + var meth = new MethodBuilder('delete', + returnType: new TypeBuilder('Future', + genericTypes: [new TypeBuilder(ctx.modelClassName)])); + return meth; + } + + MethodBuilder buildInsertMethod(PostgresBuildContext ctx) { + var meth = new MethodBuilder('insert', + returnType: new TypeBuilder('Future', + genericTypes: [new TypeBuilder(ctx.modelClassName)])); + meth.addPositional( + parameter('connection', [new TypeBuilder('PostgreSQLConnection')])); + + // Add all named params + ctx.fields.forEach((field) { + var p = new ParameterBuilder(field.name, + type: new TypeBuilder(field.type.name)); + var column = ctx.columnInfo[field.name]; + if (column?.defaultValue != null) + p = p.asOptional(literal(column.defaultValue)); + meth.addNamed(p); + }); + + return meth; + } + + ClassBuilder buildWhereClass(PostgresBuildContext ctx) { + var clazz = new ClassBuilder(ctx.whereClassName); + + ctx.fields.forEach((field) { + TypeBuilder queryBuilderType; + List args = []; + + switch (field.type.name) { + case 'String': + queryBuilderType = new TypeBuilder('StringSqlExpressionBuilder'); + break; + case 'int': + queryBuilderType = new TypeBuilder('NumericSqlExpressionBuilder', + genericTypes: [lib$core.int]); + break; + case 'double': + queryBuilderType = new TypeBuilder('NumericSqlExpressionBuilder', + genericTypes: [new TypeBuilder('double')]); + break; + case 'num': + queryBuilderType = new TypeBuilder('NumericSqlExpressionBuilder'); + break; + case 'bool': + queryBuilderType = new TypeBuilder('BooleanSqlExpressionBuilder'); + break; + case 'DateTime': + queryBuilderType = new TypeBuilder('DateTimeSqlExpressionBuilder'); + args.add(literal(ctx.resolveFieldName(field.name))); + break; + } + + if (queryBuilderType == null) + throw 'Could not resolve query builder type for field "${field.name}" of type "${field.type.name}".'; + clazz.addField(varFinal(field.name, + type: queryBuilderType, value: queryBuilderType.newInstance(args))); + }); + + // Create `toWhereClause()` + var toWhereClause = + new MethodBuilder('toWhereClause', returnType: lib$core.String); + + // List expressions = []; + toWhereClause.addStatement(varFinal('expressions', + type: new TypeBuilder('List', genericTypes: [lib$core.String]), + value: list([]))); + var expressions = reference('expressions'); + + // Add all expressions... + ctx.fields.forEach((field) { + var name = ctx.resolveFieldName(field.name); + var queryBuilder = reference(field.name); + var toAdd = field.type.name == 'DateTime' + ? queryBuilder.invoke('compile', []) + : (literal('`$name` ') + queryBuilder.invoke('compile', [])); + + toWhereClause.addStatement(ifThen(queryBuilder.property('hasValue'), [ + expressions.invoke('add', [toAdd]) + ])); + }); + + // return expressions.isEmpty ? null : ('WHERE ' + expressions.join(' AND ')); + toWhereClause.addStatement(expressions + .property('isEmpty') + .ternary( + literal(null), + (literal('WHERE ') + expressions.invoke('join', [literal(' AND ')])) + .parentheses()) + .asReturn()); + + clazz.addMethod(toWhereClause); + + return clazz; + } +} diff --git a/lib/src/builder/postgres/postgres_build_context.dart b/lib/src/builder/postgres/postgres_build_context.dart new file mode 100644 index 00000000..fa84000d --- /dev/null +++ b/lib/src/builder/postgres/postgres_build_context.dart @@ -0,0 +1,27 @@ +import 'package:analyzer/dart/element/element.dart'; +import '../../annotations.dart'; +import '../../migration.dart'; + +class PostgresBuildContext { + final Map aliases = {}; + final Map columnInfo = {}; + final Map indices = {}; + final String originalClassName, tableName, sourceFilename; + final ORM annotation; + // Todo: We can use analyzer to copy straight from Model class + final List fields; + String primaryKeyName = 'id'; + + PostgresBuildContext(this.annotation, this.fields, + {this.originalClassName, this.tableName, this.sourceFilename}); + + String get modelClassName => originalClassName.startsWith('_') + ? originalClassName.substring(1) + : originalClassName; + + String get queryClassName => modelClassName + 'Query'; + String get whereClassName => queryClassName + 'Where'; + + String resolveFieldName(String name) => + aliases.containsKey(name) ? aliases[name] : name; +} diff --git a/lib/src/builder/repository.dart b/lib/src/builder/repository.dart new file mode 100644 index 00000000..249ac61f --- /dev/null +++ b/lib/src/builder/repository.dart @@ -0,0 +1,296 @@ +import 'dart:async'; +import 'dart:mirrors'; +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/source_gen.dart'; +import 'package:query_builder_sql/query_builder_sql.dart'; +import '../annotations.dart'; + +// TODO: whereXLessThan, greaterThan, etc. + +final RegExp _leadingDot = new RegExp(r'^\.+'); + +const List QUERY_DO_NOT_OVERRIDE = const ['when']; + +typedef Iterable SuperArgumentProvider( + ORM model, ClassElement clazz); + +class AngelQueryBuilderGenerator extends GeneratorForAnnotation { + ClassMirror _baseRepositoryClassMirror; + final List _imports = [ + 'dart:async', + 'package:query_builder/query_builder.dart' + ]; + + final Map _constructorParams = {}; + SuperArgumentProvider _superArgProvider; + + AngelQueryBuilderGenerator(Type baseRepositoryQueryClass, + {Iterable additonalImports: const [], + Map constructorParams: const {}, + SuperArgumentProvider superArgProvider}) { + _baseRepositoryClassMirror = reflectClass(baseRepositoryQueryClass); + _imports.addAll(additonalImports ?? []); + _constructorParams.addAll(constructorParams ?? {}); + _superArgProvider = superArgProvider ?? + (annotation, clazz) => [ + literal(annotation.tableName?.isNotEmpty == true + ? annotation.tableName + : pluralize(new ReCase(clazz.name.substring(1)).snakeCase)) + ]; + } + + factory AngelQueryBuilderGenerator.postgresql() => + new AngelQueryBuilderGenerator(SqlRepositoryQuery, constructorParams: { + 'connection': new TypeBuilder('PostgreSQLConnection') + }, additonalImports: [ + 'package:postgres/postgres.dart', + 'package:query_builder_sql/query_builder_sql.dart' + ]); + + @override + Future generateForAnnotatedElement( + Element element, ORM annotation, BuildStep buildStep) async { + if (element.kind != ElementKind.CLASS) + throw 'Only classes may be annotated with @model.'; + var lib = generatePostgresLibrary(element, annotation, buildStep.inputId); + return prettyToSource(lib.buildAst()); + } + + LibraryBuilder generatePostgresLibrary( + ClassElement clazz, ORM annotation, AssetId inputId) { + if (!clazz.name.startsWith('_')) + throw 'Classes annotated with @model must have names starting with an underscore.'; + var lib = new LibraryBuilder(); + lib.addDirectives(_imports.map((p) => new ImportBuilder(p))); + lib.addDirective(new ImportBuilder(p.basename(inputId.path))); + + // Find all aliases... + Map aliases = {}; + clazz.fields.forEach((field) { + var aliasAnnotation = field.metadata + .firstWhere((ann) => matchAnnotation(Alias, ann), orElse: () => null); + if (aliasAnnotation != null) { + var alias = instantiateAnnotation(aliasAnnotation) as Alias; + aliases[field.name] = alias.name; + } + }); + + lib.addMember(generateRepositoryClass(clazz, aliases)); + lib.addMember(generateRepositoryQueryClass(clazz, annotation, aliases)); + return lib; + } + + ClassBuilder generateRepositoryClass( + ClassElement clazz, Map aliases) { + var genClassName = clazz.name.substring(1) + 'Repository'; + var genQueryClassName = genClassName + 'Query'; + var genClass = new ClassBuilder(genClassName); + var genQueryType = new TypeBuilder(genQueryClassName); + + // Add `connection` field + constructor + + var genConstructor = new ConstructorBuilder(); + _constructorParams.forEach((name, type) { + genClass.addField(varFinal(name, type: type)); + genConstructor.addPositional(parameter(name), asField: true); + }); + genClass.addConstructor(genConstructor); + + // Add an all method + genClass.addMethod(new MethodBuilder('all', + returnType: new TypeBuilder(genQueryClassName), + returns: new TypeBuilder(genQueryClassName) + .newInstance([reference('connection')]))); + + // For each field, add a whereX() method... + clazz.fields + .map((field) => generateWhereFieldMethod( + field, reference('all').call([]), genQueryType, aliases)) + .forEach(genClass.addMethod); + return genClass; + } + + ClassBuilder generateRepositoryQueryClass( + ClassElement clazz, ORM annotation, Map aliases) { + var modelClassName = clazz.name.substring(1); + var genClassName = clazz.name.substring(1) + 'RepositoryQuery'; + var genClass = new ClassBuilder(genClassName, + asExtends: new TypeBuilder( + MirrorSystem.getName(_baseRepositoryClassMirror.simpleName), + genericTypes: [new TypeBuilder(modelClassName)])); + var genQueryType = new TypeBuilder(genClassName); + + // Add `connection` field + constructor + + var genConstructor = new ConstructorBuilder( + invokeSuper: _superArgProvider(annotation, clazz)); + _constructorParams.forEach((name, type) { + genClass.addField(varFinal(name, type: type)); + genConstructor.addPositional(parameter(name), asField: true); + }); + genClass.addConstructor(genConstructor); + + // For each field, add a whereX() method... + clazz.fields + .map((field) => generateWhereFieldMethod( + field, explicitThis, genQueryType, aliases)) + .forEach(genClass.addMethod); + + // Add orWhereX() + clazz.fields + .map((f) => generateOrWhereFieldMethod(genQueryType, f)) + .forEach(genClass.addMethod); + + // Override any query methods + _baseRepositoryClassMirror.instanceMembers.forEach((sym, method) { + // Skip setters, etc. + if (!method.isRegularMethod) return; + + // Only if return type contains 'RepositoryQuery' + var methodName = MirrorSystem.getName(sym); + + if (QUERY_DO_NOT_OVERRIDE.contains(methodName)) return; + + var returnTypeName = MirrorSystem.getName(method.returnType.simpleName); + + if (returnTypeName.contains('RepositoryQuery')) { + var overriddenMethod = + new MethodBuilder(methodName, returnType: genQueryType); + // Add @override + overriddenMethod.addAnnotation(lib$core.override); + + // Find all positional and named args + List args = []; + List named = []; + + method.parameters.forEach((param) { + var paramName = MirrorSystem.getName(param.simpleName); + var typeName = MirrorSystem.getName(param.type.simpleName); + var paramType = new TypeBuilder(typeName); + var genParam = parameter(paramName, [paramType]); + + if (!param.isNamed) { + args.add(paramName); + overriddenMethod.addPositional( + param.isOptional ? genParam.asOptional() : genParam); + } else { + overriddenMethod.addNamed(genParam); + named.add(paramName); + } + }); + + // Invoke super + overriddenMethod.addStatement(reference('super') + .invoke(methodName, args.map(reference), + namedArguments: named.fold>( + {}, (out, k) => out..[k] = reference(k))) + .asReturn()); + + genClass.addMethod(overriddenMethod); + } + }); + + // Override toSql to put keys in desired order + // TODO: Override toSql + + // Add get() + genClass.addMethod(generateGetMethod(clazz, modelClassName, aliases)); + + return genClass; + } + + MethodBuilder generateGetMethod( + ClassElement clazz, String modelClassName, Map aliases) { + var meth = new MethodBuilder('get')..addAnnotation(lib$core.override); + + // Map rows to model... + var mapRowsToModel = new MethodBuilder.closure() + ..addPositional(parameter('rows')); + + // First, figure out which rows we fetched... + // + // var requestedKeys = whereFields.keys.isNotEmpty ? whereFields.keys : []; + + var allModelFields = clazz.fields + .map((f) => aliases.containsKey(f.name) ? aliases[f.name] : f.name); + var whereFieldsKeys = reference('whereFields').property('keys'); + + // return new Stream<>.fromFuture(...) + meth.addStatement(lib$async.Stream.newInstance([ + reference('connection') + .invoke('query', [reference('toSql').call([])]).invoke( + 'then', [mapRowsToModel]) + ], constructor: 'fromFuture').asReturn()); + + return meth; + } + + MethodBuilder generateWhereFieldMethod( + FieldElement field, + ExpressionBuilder baseQuery, + TypeBuilder returnType, + Map aliases) { + var rc = new ReCase(field.name); + var whereMethod = + new MethodBuilder('where${rc.pascalCase}', returnType: returnType); + var columnName = + aliases.containsKey(field.name) ? aliases[field.name] : field.name; + whereMethod.addPositional( + parameter(field.name, [new TypeBuilder(field.type.displayName)])); + + if (field.type.name == 'DateTime') { + // Add named `{time: true}` + whereMethod.addNamed( + parameter('time', [lib$core.bool]).asOptional(literal(true))); + // return all().whereDate('x', x, time: time != false); + // return all().where('x', x); + whereMethod.addStatement(baseQuery.invoke('whereDate', [ + literal(columnName), + reference(field.name) + ], namedArguments: { + 'time': reference('time').notEquals(literal(false)) + }).asReturn()); + } else { + // return all().where('x', x); + whereMethod.addStatement(baseQuery.invoke( + 'where', [literal(columnName), reference(field.name)]).asReturn()); + } + + return whereMethod; + } + + MethodBuilder generateOrWhereFieldMethod( + TypeBuilder genQueryClassType, FieldElement field) { + var rc = new ReCase(field.name); + var orWhereMethod = new MethodBuilder('orWhere' + rc.pascalCase, + returnType: genQueryClassType); + orWhereMethod.addPositional( + parameter(field.name, [new TypeBuilder(field.type.displayName)])); + + if (field.type.name == 'DateTime') { + orWhereMethod.addNamed(parameter('time', [lib$core.bool])); + orWhereMethod.addStatement(reference('or').call([ + reference('where' + rc.pascalCase).call([ + reference(field.name) + ], namedArguments: { + 'time': reference('time').notEquals(literal(false)) + }) + ]).asReturn()); + } else { + orWhereMethod.addStatement(reference('or').call([ + reference('where' + rc.pascalCase).call([reference(field.name)]) + ]).asReturn()); + } + + return orWhereMethod; + } +} diff --git a/lib/src/migration.dart b/lib/src/migration.dart new file mode 100644 index 00000000..512a25c6 --- /dev/null +++ b/lib/src/migration.dart @@ -0,0 +1,104 @@ +/// Applies additional attributes to a database column. +class Column { + /// If `true`, a SQL field will be auto-incremented. + final bool autoIncrement; + + /// If `true`, a SQL field will be nullable. + final bool nullable; + + /// Specifies the length of a `VARCHAR`. + final int length; + + /// Explicitly defines a SQL type for this column. + final ColumnType type; + + /// Specifies what kind of index this column is, if any. + final IndexType index; + + /// The default value of this field. + final defaultValue; + + const Column( + {this.autoIncrement: false, + this.nullable: true, + this.length, + this.type, + this.index: IndexType.NONE, + this.defaultValue}); +} + +class PrimaryKey extends Column { + const PrimaryKey({bool autoIncrement: true}) + : super(autoIncrement: autoIncrement, index: IndexType.PRIMARY_KEY); +} + +const Column primaryKey = const PrimaryKey(); + +/// Maps to SQL index types. +enum IndexType { + NONE, + + /// Standard index. + INDEX, + + /// A primary key. + PRIMARY_KEY, + + /// A *unique* index. + UNIQUE +} + +/// Maps to SQL data types. +/// +/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm +class ColumnType { + /// The name of this data type. + final String name; + const ColumnType._(this.name); + + // Numbers + static const ColumnType BIG_INT = const ColumnType._('bigint'); + static const ColumnType INT = const ColumnType._('int'); + static const ColumnType SMALL_INT = const ColumnType._('smallint'); + static const ColumnType TINY_INT = const ColumnType._('tinyint'); + static const ColumnType BIT = const ColumnType._('bit'); + static const ColumnType DECIMAL = const ColumnType._('decimal'); + static const ColumnType NUMERIC = const ColumnType._('numeric'); + static const ColumnType MONEY = const ColumnType._('money'); + static const ColumnType SMALL_MONEY = const ColumnType._('smallmoney'); + static const ColumnType FLOAT = const ColumnType._('float'); + static const ColumnType REAL = const ColumnType._('real'); + + // Dates and times + static const ColumnType DATE_TIME = const ColumnType._('datetime'); + static const ColumnType SMALL_DATE_TIME = const ColumnType._('smalldatetime'); + static const ColumnType DATE = const ColumnType._('date'); + static const ColumnType TIME = const ColumnType._('time'); + static const ColumnType TIME_STAMP = const ColumnType._('timestamp'); + + // Strings + static const ColumnType CHAR = const ColumnType._('char'); + static const ColumnType VAR_CHAR = const ColumnType._('varchar'); + static const ColumnType VAR_CHAR_MAX = const ColumnType._('varchar(max)'); + static const ColumnType TEXT = const ColumnType._('text'); + + // Unicode strings + static const ColumnType NCHAR = const ColumnType._('nchar'); + static const ColumnType NVAR_CHAR = const ColumnType._('nvarchar'); + static const ColumnType NVAR_CHAR_MAX = const ColumnType._('nvarchar(max)'); + static const ColumnType NTEXT = const ColumnType._('ntext'); + + // Binary + static const ColumnType BINARY = const ColumnType._('binary'); + static const ColumnType VAR_BINARY = const ColumnType._('varbinary'); + static const ColumnType VAR_BINARY_MAX = const ColumnType._('varbinary(max)'); + static const ColumnType IMAGE = const ColumnType._('image'); + + // Misc. + static const ColumnType SQL_VARIANT = const ColumnType._('sql_variant'); + static const ColumnType UNIQUE_IDENTIFIER = + const ColumnType._('uniqueidentifier'); + static const ColumnType XML = const ColumnType._('xml'); + static const ColumnType CURSOR = const ColumnType._('cursor'); + static const ColumnType TABLE = const ColumnType._('table'); +} diff --git a/lib/src/query.dart b/lib/src/query.dart new file mode 100644 index 00000000..db34d555 --- /dev/null +++ b/lib/src/query.dart @@ -0,0 +1,194 @@ +import 'package:intl/intl.dart'; + +abstract class SqlExpressionBuilder { + bool get hasValue; + String compile(); +} + +class NumericSqlExpressionBuilder + implements SqlExpressionBuilder { + bool _hasValue = false; + String _op = '='; + T _value; + + @override + bool get hasValue => _hasValue; + + bool _change(String op, T value) { + _op = op; + _value = value; + return _hasValue = true; + } + + @override + String compile() { + if (_value == null) return null; + return '$_op $_value'; + } + + operator <(T value) => _change('<', value); + operator >(T value) => _change('>', value); + operator <=(T value) => _change('<=', value); + operator >=(T value) => _change('>=', value); + + void lessThan(T value) { + _change('<', value); + } + + void lessThanOrEqualTo(T value) { + _change('<=', value); + } + + void greaterThan(T value) { + _change('>', value); + } + + void greaterThanOrEqualTo(T value) { + _change('>=', value); + } + + void equals(T value) { + _change('=', value); + } + + void notEquals(T value) { + _change('!=', value); + } +} + +// TODO: Escape SQL Strings +class StringSqlExpressionBuilder implements SqlExpressionBuilder { + bool _hasValue = false; + String _op = '='; + String _value; + + @override + bool get hasValue => _hasValue; + + bool _change(String op, String value) { + _op = op; + _value = value; + return _hasValue = true; + } + + @override + String compile() { + if (_value == null) return null; + return '$_op `$_value`'; + } + + void isEmpty() => equals(''); + + void equals(String value) { + _change('=', value); + } + + void notEquals(String value) { + _change('!=', value); + } + + void like(String value) { + _change('LIKE', value); + } +} + +class BooleanSqlExpressionBuilder implements SqlExpressionBuilder { + bool _hasValue = false; + String _op = '='; + bool _value; + + @override + bool get hasValue => _hasValue; + + bool _change(String op, bool value) { + _op = op; + _value = value; + return _hasValue = true; + } + + @override + String compile() { + if (_value == null) return null; + var v = _value ? 1 : 0; + return '$_op $v'; + } + + void equals(bool value) { + _change('=', value); + } + + void notEquals(bool value) { + _change('!=', value); + } +} + +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(), + day = new NumericSqlExpressionBuilder(), + hour = new NumericSqlExpressionBuilder(), + minute = new NumericSqlExpressionBuilder(), + second = new NumericSqlExpressionBuilder(); + final String columnName; + String _raw; + + DateTimeSqlExpressionBuilder(this.columnName); + + @override + bool get hasValue => + _raw?.isNotEmpty == true || + year.hasValue || + month.hasValue || + day.hasValue || + hour.hasValue || + minute.hasValue || + second.hasValue; + + bool _change(String _op, DateTime dt, bool time) { + var dateString = time ? _ymdHms.format(dt) : _ymd.format(dt); + _raw = '`$columnName` $_op \'$dateString\''; + return true; + } + + operator <(DateTime value) => _change('<', value, true); + operator <=(DateTime value) => _change('<=', value, true); + operator >(DateTime value) => _change('>', value, true); + operator >=(DateTime value) => _change('>=', value, true); + + void equals(DateTime value, {bool includeTime: true}) { + _change('=', value, includeTime != false); + } + + void lessThan(DateTime value, {bool includeTime: true}) { + _change('<', value, includeTime != false); + } + + void lessThanOrEqualTo(DateTime value, {bool includeTime: true}) { + _change('<=', value, includeTime != false); + } + + void greaterThan(DateTime value, {bool includeTime: true}) { + _change('>', value, includeTime != false); + } + + void greaterThanOrEqualTo(DateTime value, {bool includeTime: true}) { + _change('>=', value, includeTime != false); + } + + @override + String compile() { + if (_raw?.isNotEmpty == true) return _raw; + List parts = []; + if (year.hasValue) parts.add('YEAR(`$columnName`) ${year.compile()}'); + if (month.hasValue) parts.add('MONTH(`$columnName`) ${month.compile()}'); + if (day.hasValue) parts.add('DAY(`$columnName`) ${day.compile()}'); + if (hour.hasValue) parts.add('HOUR(`$columnName`) ${hour.compile()}'); + if (minute.hasValue) parts.add('MINUTE(`$columnName`) ${minute.compile()}'); + if (second.hasValue) parts.add('SECOND(`$columnName`) ${second.compile()}'); + + return parts.isEmpty ? null : parts.join(' AND '); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d7f72f44..83219309 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ dependencies: code_builder: ^1.0.0 id: ^1.0.0 inflection: ^0.4.1 + intl: ^0.15.1 query_builder_sql: ^1.0.0-alpha recase: ^1.0.0 source_gen: ^0.5.8 diff --git a/test/car_test.dart b/test/car_test.dart new file mode 100644 index 00000000..7c63f2a7 --- /dev/null +++ b/test/car_test.dart @@ -0,0 +1,22 @@ +import 'package:test/test.dart'; +import 'models/car.dart'; +import 'models/car.orm.g.dart'; + +final DateTime MILENNIUM = new DateTime.utc(2000, 1, 1); + +main() { + test('to where', () { + var query = new CarQuery(); + query.where + ..familyFriendly.equals(true) + ..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'"); + }); + + test('insert', () async { + var car = await CarQuery.insert(make: 'Mazda', familyFriendly: false); + print(car.toJson()); + }, skip: 'Insert not yet implemented'); +} diff --git a/test/models/car.dart b/test/models/car.dart index 6ab67c1b..bfbb536d 100644 --- a/test/models/car.dart +++ b/test/models/car.dart @@ -1,21 +1,15 @@ library angel_orm.test.models.car; import 'package:angel_framework/common.dart'; -import 'package:angel_orm/angel_orm.dart' as orm; +import 'package:angel_orm/angel_orm.dart'; import 'package:angel_serialize/angel_serialize.dart'; part 'car.g.dart'; @serializable -@orm.model +@orm class _Car extends Model { - @override - String id; - - @override - @Alias('created_at') - DateTime createdAt; - - @override - @Alias('updated_at') - DateTime updatedAt; -} \ No newline at end of file + String make; + String description; + bool familyFriendly; + DateTime recalledAt; +} diff --git a/test/models/car.g.dart b/test/models/car.g.dart index 95be582d..70865fb0 100644 --- a/test/models/car.g.dart +++ b/test/models/car.g.dart @@ -9,35 +9,36 @@ part of angel_orm.test.models.car; class Car extends _Car { @override - String id; + String make; @override - DateTime createdAt; + String description; @override - DateTime updatedAt; + bool familyFriendly; - Car({this.id, this.createdAt, this.updatedAt}); + @override + DateTime recalledAt; + + Car({this.make, this.description, this.familyFriendly, this.recalledAt}); factory Car.fromJson(Map data) { return new Car( - id: data['id'], - 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']) + make: data['make'], + description: data['description'], + familyFriendly: data['familyFriendly'], + recalledAt: data['recalledAt'] is DateTime + ? data['recalledAt'] + : (data['recalledAt'] is String + ? DateTime.parse(data['recalledAt']) : null)); } Map toJson() => { - 'id': id, - 'created_at': createdAt == null ? null : createdAt.toIso8601String(), - 'updated_at': updatedAt == null ? null : updatedAt.toIso8601String() + 'make': make, + 'description': description, + 'familyFriendly': familyFriendly, + 'recalledAt': recalledAt == null ? null : recalledAt.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 160beacb..466af1a5 100644 --- a/test/models/car.orm.g.dart +++ b/test/models/car.orm.g.dart @@ -1,212 +1,88 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** -// Generator: AngelQueryBuilderGenerator +// Generator: PostgresORMGenerator // Target: class _Car // ************************************************************************** import 'dart:async'; -import 'package:query_builder/query_builder.dart'; +import 'package:angel_orm/angel_orm.dart'; import 'package:postgres/postgres.dart'; -import 'package:query_builder_sql/query_builder_sql.dart'; import 'car.dart'; -class CarRepository { - final PostgreSQLConnection connection; +class CarQuery { + final List _and = []; - CarRepository(this.connection); + final List _or = []; - CarRepositoryQuery all() => new CarRepositoryQuery(connection); + final List _not = []; - CarRepositoryQuery whereId(String id) { - return all().where('id', id); + final CarQueryWhere where = new CarQueryWhere(); + + void and(CarQuery other) { + var compiled = other.where.toWhereClause(); + if (compiled != null) { + _and.add(compiled); + } } - CarRepositoryQuery whereCreatedAt(DateTime createdAt, {bool time: true}) { - return all().whereDate('created_at', createdAt, time: time != false); + void or(CarQuery other) { + var compiled = other.where.toWhereClause(); + if (compiled != null) { + _or.add(compiled); + } } - CarRepositoryQuery whereUpdatedAt(DateTime updatedAt, {bool time: true}) { - return all().whereDate('updated_at', updatedAt, time: time != false); + void not(CarQuery other) { + var compiled = other.where.toWhereClause(); + if (compiled != null) { + _not.add(compiled); + } } + + Stream get() {} + + Future getOne() {} + + Future update() {} + + Future delete() {} + + static Future insert(PostgreSQLConnection connection, + {String make, + String description, + bool familyFriendly, + DateTime recalledAt}) {} + + static Stream getAll() => new CarQuery().get(); } -class CarRepositoryQuery extends SqlRepositoryQuery { - final PostgreSQLConnection connection; +class CarQueryWhere { + final StringSqlExpressionBuilder make = new StringSqlExpressionBuilder(); - CarRepositoryQuery(this.connection) : super('cars'); + final StringSqlExpressionBuilder description = + new StringSqlExpressionBuilder(); - CarRepositoryQuery whereId(String id) { - return this.where('id', id); - } + final BooleanSqlExpressionBuilder familyFriendly = + new BooleanSqlExpressionBuilder(); - CarRepositoryQuery whereCreatedAt(DateTime createdAt, {bool time: true}) { - return this.whereDate('created_at', createdAt, time: time != false); - } + final DateTimeSqlExpressionBuilder recalledAt = + new DateTimeSqlExpressionBuilder('recalled_at'); - CarRepositoryQuery whereUpdatedAt(DateTime updatedAt, {bool time: true}) { - return this.whereDate('updated_at', updatedAt, time: time != false); - } - - CarRepositoryQuery orWhereId(String id) { - return or(whereId(id)); - } - - CarRepositoryQuery orWhereCreatedAt(DateTime createdAt, {bool time}) { - return or(whereCreatedAt(createdAt, time: time != false)); - } - - CarRepositoryQuery orWhereUpdatedAt(DateTime updatedAt, {bool time}) { - return or(whereUpdatedAt(updatedAt, time: time != false)); - } - - @override - CarRepositoryQuery latest([String fieldName]) { - return super.latest(fieldName); - } - - @override - CarRepositoryQuery oldest([String fieldName]) { - return super.oldest(fieldName); - } - - @override - CarRepositoryQuery where(String fieldName, dynamic value) { - return super.where(fieldName, value); - } - - @override - CarRepositoryQuery whereNot(String fieldName, dynamic value) { - return super.whereNot(fieldName, value); - } - - @override - CarRepositoryQuery whereNull(String fieldName) { - return super.whereNull(fieldName); - } - - @override - CarRepositoryQuery whereNotNull(String fieldName) { - return super.whereNotNull(fieldName); - } - - @override - CarRepositoryQuery distinct(Iterable fieldNames) { - return super.distinct(fieldNames); - } - - @override - CarRepositoryQuery groupBy(String fieldName) { - return super.groupBy(fieldName); - } - - @override - CarRepositoryQuery inRandomOrder() { - return super.inRandomOrder(); - } - - @override - CarRepositoryQuery orderBy(String fieldName, [OrderBy orderBy]) { - return super.orderBy(fieldName, orderBy); - } - - @override - CarRepositoryQuery select(Iterable selectors) { - return super.select(selectors); - } - - @override - CarRepositoryQuery skip(int count) { - return super.skip(count); - } - - @override - CarRepositoryQuery take(int count) { - return super.take(count); - } - - @override - CarRepositoryQuery join( - String otherTable, String nearColumn, String farColumn, - [JoinType joinType]) { - return super.join(otherTable, nearColumn, farColumn, joinType); - } - - @override - CarRepositoryQuery union(RepositoryQuery other, [UnionType type]) { - return super.union(other, type); - } - - @override - CarRepositoryQuery whereBetween( - String fieldName, dynamic lower, dynamic upper) { - return super.whereBetween(fieldName, lower, upper); - } - - @override - CarRepositoryQuery whereDate(String fieldName, DateTime date, {bool time}) { - return super.whereDate(fieldName, date, time: time); - } - - @override - CarRepositoryQuery whereDay(String fieldName, int day) { - return super.whereDay(fieldName, day); - } - - @override - CarRepositoryQuery whereEquality( - String fieldName, dynamic value, Equality equality) { - return super.whereEquality(fieldName, value, equality); - } - - @override - CarRepositoryQuery whereIn(String fieldName, Iterable values) { - return super.whereIn(fieldName, values); - } - - @override - CarRepositoryQuery whereLike(String fieldName, dynamic value) { - return super.whereLike(fieldName, value); - } - - @override - CarRepositoryQuery whereMonth(String fieldName, int month) { - return super.whereMonth(fieldName, month); - } - - @override - CarRepositoryQuery whereNotBetween( - String fieldName, dynamic lower, dynamic upper) { - return super.whereNotBetween(fieldName, lower, upper); - } - - @override - CarRepositoryQuery whereNotIn(String fieldName, Iterable values) { - return super.whereNotIn(fieldName, values); - } - - @override - CarRepositoryQuery whereYear(String fieldName, int year) { - return super.whereYear(fieldName, year); - } - - @override - CarRepositoryQuery selfJoin(String t1, String t2) { - return super.selfJoin(t1, t2); - } - - @override - CarRepositoryQuery or(RepositoryQuery other) { - return super.or(other); - } - - @override - CarRepositoryQuery not(RepositoryQuery other) { - return super.not(other); - } - - @override - get() { - return new Stream.fromFuture(connection.query(toSql()).then((rows) {})); + String toWhereClause() { + final List expressions = []; + 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()); + } + return expressions.isEmpty ? null : ('WHERE ' + expressions.join(' AND ')); } } diff --git a/tool/phases.dart b/tool/phases.dart index e2412fcf..22633c0c 100644 --- a/tool/phases.dart +++ b/tool/phases.dart @@ -9,6 +9,6 @@ final PhaseGroup PHASES = new PhaseGroup() new InputSet('angel_orm', const ['test/models/*.dart']))) ..addPhase(new Phase() ..addAction( - new GeneratorBuilder([new AngelQueryBuilderGenerator.postgresql()], + new GeneratorBuilder([new PostgresORMGenerator()], isStandalone: true, generatedExtension: '.orm.g.dart'), new InputSet('angel_orm', const ['test/models/*.dart'])));