Start service gen
This commit is contained in:
parent
f9de817e40
commit
abf438196e
7 changed files with 186 additions and 1484 deletions
|
@ -1,2 +1,3 @@
|
||||||
export 'src/builder/postgres/migration.dart';
|
export 'src/builder/orm/migration.dart';
|
||||||
export 'src/builder/postgres/postgres.dart';
|
export 'src/builder/orm/postgres.dart';
|
||||||
|
export 'src/builder/orm/service.dart';
|
178
angel_orm_generator/lib/src/builder/orm/service.dart
Normal file
178
angel_orm_generator/lib/src/builder/orm/service.dart
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:analyzer/dart/element/element.dart';
|
||||||
|
import 'package:angel_orm/angel_orm.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 'build_context.dart';
|
||||||
|
import 'postgres_build_context.dart';
|
||||||
|
|
||||||
|
class PostgresServiceGenerator extends GeneratorForAnnotation<ORM> {
|
||||||
|
final bool autoSnakeCaseNames;
|
||||||
|
|
||||||
|
final bool autoIdAndDateFields;
|
||||||
|
|
||||||
|
const PostgresServiceGenerator(
|
||||||
|
{this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> generateForAnnotatedElement(
|
||||||
|
Element element, ORM annotation, BuildStep buildStep) async {
|
||||||
|
if (buildStep.inputId.path.contains('.service.g.dart')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element is! ClassElement)
|
||||||
|
throw 'Only classes can be annotated with @ORM().';
|
||||||
|
var resolver = await buildStep.resolver;
|
||||||
|
var lib =
|
||||||
|
generateOrmLibrary(element.library, resolver, buildStep).buildAst();
|
||||||
|
if (lib == null) return null;
|
||||||
|
return prettyToSource(lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryBuilder generateOrmLibrary(
|
||||||
|
LibraryElement libraryElement, Resolver resolver, BuildStep buildStep) {
|
||||||
|
var lib = new LibraryBuilder();
|
||||||
|
lib.addDirective(new ImportBuilder('dart:async'));
|
||||||
|
lib.addDirective(
|
||||||
|
new ImportBuilder('package:angel_framework/angel_framework.dart'));
|
||||||
|
lib.addDirective(new ImportBuilder('package:postgres/postgres.dart'));
|
||||||
|
lib.addDirective(new ImportBuilder(p.basename(buildStep.inputId.path)));
|
||||||
|
|
||||||
|
var pathName = p.basenameWithoutExtension(
|
||||||
|
p.basenameWithoutExtension(buildStep.inputId.path));
|
||||||
|
lib.addDirective(new ImportBuilder('$pathName.orm.g.dart'));
|
||||||
|
|
||||||
|
var elements = getElementsFromLibraryElement(libraryElement)
|
||||||
|
.where((el) => el is ClassElement);
|
||||||
|
Map<ClassElement, PostgresBuildContext> contexts = {};
|
||||||
|
List<String> done = [];
|
||||||
|
|
||||||
|
for (var element in elements) {
|
||||||
|
if (!done.contains(element.name)) {
|
||||||
|
var ann = element.metadata
|
||||||
|
.firstWhere((a) => matchAnnotation(ORM, a), orElse: () => null);
|
||||||
|
if (ann != null) {
|
||||||
|
contexts[element] = buildContext(
|
||||||
|
element,
|
||||||
|
instantiateAnnotation(ann),
|
||||||
|
buildStep,
|
||||||
|
resolver,
|
||||||
|
autoSnakeCaseNames != false,
|
||||||
|
autoIdAndDateFields != false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contexts.isEmpty) return null;
|
||||||
|
|
||||||
|
done.clear();
|
||||||
|
for (var element in contexts.keys) {
|
||||||
|
if (!done.contains(element.name)) {
|
||||||
|
var ctx = contexts[element];
|
||||||
|
lib.addMember(buildServiceClass(ctx));
|
||||||
|
done.add(element.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassBuilder buildServiceClass(PostgresBuildContext ctx) {
|
||||||
|
var rc = new ReCase(ctx.modelClassName);
|
||||||
|
var clazz = new ClassBuilder('${rc.pascalCase}Service',
|
||||||
|
asExtends: new TypeBuilder('Service'));
|
||||||
|
|
||||||
|
// Add fields
|
||||||
|
// connection, allowRemoveAll, allowQuery
|
||||||
|
|
||||||
|
clazz
|
||||||
|
..addField(varFinal('connection', type: ctx.postgreSQLConnectionBuilder))
|
||||||
|
..addField(varFinal('allowRemoveAll', type: lib$core.bool))
|
||||||
|
..addField(varFinal('allowQuery', type: lib$core.bool));
|
||||||
|
|
||||||
|
clazz.addConstructor(constructor([
|
||||||
|
thisField(parameter('connection')),
|
||||||
|
thisField(named(parameter('allowRemoveAll', [literal(false)]))),
|
||||||
|
thisField(named(parameter('allowQuery', [literal(false)])))
|
||||||
|
]));
|
||||||
|
|
||||||
|
clazz.addMethod(buildQueryMethod(ctx));
|
||||||
|
clazz.addMethod(buildToIdMethod(ctx));
|
||||||
|
|
||||||
|
var params = reference('params'),
|
||||||
|
buildQuery = reference('buildQuery'),
|
||||||
|
connection = reference('connection'),
|
||||||
|
query = reference('query');
|
||||||
|
|
||||||
|
// Future<List<T>> index([p]) => buildQuery(p).get(connection).toList();
|
||||||
|
clazz.addMethod(lambda(
|
||||||
|
'index',
|
||||||
|
buildQuery
|
||||||
|
.call([params]).invoke('get', [connection]).invoke('toList', []),
|
||||||
|
returnType: new TypeBuilder('Future', genericTypes: [
|
||||||
|
new TypeBuilder('List', genericTypes: [ctx.modelClassBuilder])
|
||||||
|
]))
|
||||||
|
..addPositional(parameter('params', [lib$core.Map]))
|
||||||
|
..addAnnotation(lib$core.override));
|
||||||
|
|
||||||
|
var read = new MethodBuilder('read',
|
||||||
|
returnType:
|
||||||
|
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
||||||
|
parseParams(read, ctx, id: true);
|
||||||
|
read.addStatement(query.invoke('get', [connection]).property('first'));
|
||||||
|
clazz.addMethod(read);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return clazz;
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodBuilder buildQueryMethod(PostgresBuildContext ctx) {
|
||||||
|
var meth =
|
||||||
|
new MethodBuilder('buildQuery', returnType: ctx.queryClassBuilder);
|
||||||
|
return meth;
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodBuilder buildToIdMethod(PostgresBuildContext ctx) {
|
||||||
|
var meth = new MethodBuilder('toId', returnType: lib$core.int);
|
||||||
|
var id = reference('id');
|
||||||
|
|
||||||
|
meth.addStatement(ifThen(id.isInstanceOf(lib$core.int), [
|
||||||
|
id.asReturn(),
|
||||||
|
elseThen([
|
||||||
|
ifThen(id.equals(literal('null')).or(id.equals(literal(null))), [
|
||||||
|
literal(null).asReturn(),
|
||||||
|
elseThen([
|
||||||
|
lib$core.int.invoke('parse', [id.invoke('toString', [])]).asReturn()
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
|
||||||
|
return meth;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseParams(MethodBuilder meth, PostgresBuildContext ctx, {bool id}) {
|
||||||
|
meth.addStatement(varField('query',
|
||||||
|
value: reference('buildQuery').call([
|
||||||
|
reference('params')
|
||||||
|
.notEquals(literal(null))
|
||||||
|
.ternary(reference('params'), map({}))
|
||||||
|
])));
|
||||||
|
|
||||||
|
if (id == true) {
|
||||||
|
meth.addStatement(
|
||||||
|
reference('query').property('where').property('id').invoke('equals', [
|
||||||
|
reference('toId').call([reference('id')])
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,141 +0,0 @@
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
|
||||||
import 'package:analyzer/src/dart/element/element.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:angel_serialize_generator/src/find_annotation.dart';
|
|
||||||
import 'package:angel_serialize_generator/build_context.dart' as serialize;
|
|
||||||
import 'package:angel_serialize_generator/context.dart' as serialize;
|
|
||||||
import 'package:build/build.dart';
|
|
||||||
import 'package:inflection/inflection.dart';
|
|
||||||
import 'package:recase/recase.dart';
|
|
||||||
import 'package:source_gen/source_gen.dart';
|
|
||||||
import 'postgres_build_context.dart';
|
|
||||||
|
|
||||||
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),
|
|
||||||
autoSnakeCaseNames: autoSnakeCaseNames != false,
|
|
||||||
autoIdAndDateFields: autoIdAndDateFields != false);
|
|
||||||
var relations = new TypeChecker.fromRuntime(Relationship);
|
|
||||||
List<String> fieldNames = [];
|
|
||||||
List<FieldElement> fields = [];
|
|
||||||
|
|
||||||
for (var field in raw.fields) {
|
|
||||||
fieldNames.add(field.name);
|
|
||||||
// Check for relationship. If so, skip.
|
|
||||||
var relationshipAnnotation = relations.firstAnnotationOf(field);
|
|
||||||
/* findAnnotation<HasOne>(field, HasOne) ??
|
|
||||||
findAnnotation<HasMany>(field, HasMany) ??
|
|
||||||
findAnnotation<BelongsTo>(field, BelongsTo);*/
|
|
||||||
|
|
||||||
if (relationshipAnnotation != null) {
|
|
||||||
int type = -1;
|
|
||||||
|
|
||||||
switch (relationshipAnnotation.type.name) {
|
|
||||||
case 'HasMany':
|
|
||||||
type = RelationshipType.HAS_MANY;
|
|
||||||
break;
|
|
||||||
case 'HasOne':
|
|
||||||
type = RelationshipType.HAS_ONE;
|
|
||||||
break;
|
|
||||||
case 'BelongsTo':
|
|
||||||
type = RelationshipType.BELONGS_TO;
|
|
||||||
break;
|
|
||||||
case 'BelongsToMany':
|
|
||||||
type = RelationshipType.BELONGS_TO_MANY;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedError(
|
|
||||||
'Unsupported relationship type "${relationshipAnnotation.type.name}".');
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.relationshipFields.add(field);
|
|
||||||
ctx.relationships[field.name] = new Relationship(type,
|
|
||||||
localKey:
|
|
||||||
relationshipAnnotation.getField('localKey')?.toStringValue(),
|
|
||||||
foreignKey:
|
|
||||||
relationshipAnnotation.getField('foreignKey')?.toStringValue(),
|
|
||||||
foreignTable:
|
|
||||||
relationshipAnnotation.getField('foreignTable')?.toStringValue(),
|
|
||||||
cascadeOnDelete: relationshipAnnotation
|
|
||||||
.getField('cascadeOnDelete')
|
|
||||||
?.toBoolValue());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for column annotation...
|
|
||||||
var column = findAnnotation<Column>(field, Column);
|
|
||||||
|
|
||||||
if (column == null && field.name == 'id' && ctx.shimmed['id'] == true) {
|
|
||||||
column = const Column(type: ColumnType.SERIAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.BOOLEAN);
|
|
||||||
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;
|
|
||||||
fields.add(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fields.addAll(fields);
|
|
||||||
|
|
||||||
// Add belongs to fields
|
|
||||||
// TODO: Do this for belongs to many as well
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
var rc = new ReCase(relationship.localKey);
|
|
||||||
|
|
||||||
if (relationship.type == RelationshipType.BELONGS_TO) {
|
|
||||||
var field = new RelationshipConstraintField(
|
|
||||||
rc.camelCase, ctx.typeProvider.intType, name);
|
|
||||||
ctx.fields.add(field);
|
|
||||||
ctx.aliases[field.name] = relationship.localKey;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RelationshipConstraintField extends FieldElementImpl {
|
|
||||||
@override
|
|
||||||
final DartType type;
|
|
||||||
final String originalName;
|
|
||||||
RelationshipConstraintField(String name, this.type, this.originalName)
|
|
||||||
: super(name, -1);
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:build/build.dart';
|
|
||||||
import 'package:inflection/inflection.dart';
|
|
||||||
import 'package:recase/recase.dart';
|
|
||||||
import 'package:source_gen/src/annotation.dart';
|
|
||||||
import 'package:source_gen/src/utils.dart';
|
|
||||||
import 'build_context.dart';
|
|
||||||
import 'postgres_build_context.dart';
|
|
||||||
|
|
||||||
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 the schema will automatically add id, created_at and updated_at fields.
|
|
||||||
final bool autoIdAndDateFields;
|
|
||||||
|
|
||||||
/// If `true` (default: `false`), then the resulting schema will generate a `TEMPORARY` table.
|
|
||||||
final bool temporary;
|
|
||||||
|
|
||||||
const SQLMigrationGenerator(
|
|
||||||
{this.autoSnakeCaseNames: true,
|
|
||||||
this.autoIdAndDateFields: true,
|
|
||||||
this.temporary: false});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, List<String>> 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<String> 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) {
|
|
||||||
if (temporary == true)
|
|
||||||
buf.writeln('CREATE TEMPORARY TABLE "${ctx.tableName}" (');
|
|
||||||
else
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
|
|
||||||
if (relationship.isBelongsTo) {
|
|
||||||
if (i++ > 0) buf.writeln(',');
|
|
||||||
buf.write(
|
|
||||||
' "${relationship.localKey}" int REFERENCES ${relationship.foreignTable}(${relationship.foreignKey})');
|
|
||||||
if (relationship.cascadeOnDelete != false && relationship.isSingular)
|
|
||||||
buf.write(' ON DELETE CASCADE');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Primary keys, unique
|
|
||||||
bool hasPrimary = false;
|
|
||||||
ctx.fields.forEach((f) {
|
|
||||||
var col = ctx.columnInfo[f.name];
|
|
||||||
if (col != null) {
|
|
||||||
var name = ctx.resolveFieldName(f.name);
|
|
||||||
if (col.index == IndexType.UNIQUE) {
|
|
||||||
if (i++ > 0) buf.writeln(',');
|
|
||||||
buf.write(' UNIQUE($name(');
|
|
||||||
} else if (col.index == IndexType.PRIMARY_KEY) {
|
|
||||||
if (i++ > 0) buf.writeln(',');
|
|
||||||
hasPrimary = true;
|
|
||||||
buf.write(' PRIMARY KEY($name)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasPrimary) {
|
|
||||||
var idField =
|
|
||||||
ctx.fields.firstWhere((f) => f.name == 'id', orElse: () => null);
|
|
||||||
if (idField != null) {
|
|
||||||
if (i++ > 0) buf.writeln(',');
|
|
||||||
buf.write(' PRIMARY KEY(id)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.writeln();
|
|
||||||
buf.writeln(');');
|
|
||||||
}
|
|
||||||
|
|
||||||
void buildDownMigration(PostgresBuildContext ctx, StringBuffer buf) {
|
|
||||||
buf.writeln('DROP TABLE "${ctx.tableName}";');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,939 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.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 'build_context.dart';
|
|
||||||
import 'postgres_build_context.dart';
|
|
||||||
|
|
||||||
const List<String> RELATIONS = const ['or'];
|
|
||||||
const List<String> RESTRICTORS = const ['limit', 'offset'];
|
|
||||||
const Map<String, String> SORT_MODES = const {
|
|
||||||
'Descending': 'DESC',
|
|
||||||
'Ascending': 'ASC'
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: HasOne, HasMany
|
|
||||||
class PostgresORMGenerator extends GeneratorForAnnotation<ORM> {
|
|
||||||
/// 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 PostgresORMGenerator(
|
|
||||||
{this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> generateForAnnotatedElement(
|
|
||||||
Element element, ORM annotation, BuildStep buildStep) async {
|
|
||||||
if (buildStep.inputId.path.contains('.orm.g.dart')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element is! ClassElement)
|
|
||||||
throw 'Only classes can be annotated with @ORM().';
|
|
||||||
var resolver = await buildStep.resolver;
|
|
||||||
var lib =
|
|
||||||
generateOrmLibrary(element.library, resolver, buildStep).buildAst();
|
|
||||||
if (lib == null) return null;
|
|
||||||
return prettyToSource(lib);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(p.basename(buildStep.inputId.path)));
|
|
||||||
var elements = getElementsFromLibraryElement(libraryElement)
|
|
||||||
.where((el) => el is ClassElement);
|
|
||||||
Map<ClassElement, PostgresBuildContext> contexts = {};
|
|
||||||
List<String> done = [];
|
|
||||||
List<String> imported = [];
|
|
||||||
|
|
||||||
for (var element in elements) {
|
|
||||||
if (!done.contains(element.name)) {
|
|
||||||
var ann = element.metadata
|
|
||||||
.firstWhere((a) => matchAnnotation(ORM, a), orElse: () => null);
|
|
||||||
if (ann != null) {
|
|
||||||
var ctx = contexts[element] = buildContext(
|
|
||||||
element,
|
|
||||||
instantiateAnnotation(ann),
|
|
||||||
buildStep,
|
|
||||||
resolver,
|
|
||||||
autoSnakeCaseNames != false,
|
|
||||||
autoIdAndDateFields != false);
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
var field = ctx.resolveRelationshipField(name);
|
|
||||||
var uri = field.type.element.source.uri;
|
|
||||||
var pathName = p
|
|
||||||
.basenameWithoutExtension(p.basenameWithoutExtension(uri.path));
|
|
||||||
var source =
|
|
||||||
'$pathName.orm.g.dart'; //uri.resolve('$pathName.orm.g.dart').toString();
|
|
||||||
// TODO: Find good way to source url...
|
|
||||||
source = new ReCase(relationship.isList
|
|
||||||
? relationship.modelType.name
|
|
||||||
: field.type.name)
|
|
||||||
.snakeCase +
|
|
||||||
'.orm.g.dart';
|
|
||||||
|
|
||||||
if (!imported.contains(source)) {
|
|
||||||
lib.addDirective(new ImportBuilder(source));
|
|
||||||
imported.add(source);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contexts.isEmpty) return null;
|
|
||||||
|
|
||||||
done.clear();
|
|
||||||
for (var element in contexts.keys) {
|
|
||||||
if (!done.contains(element.name)) {
|
|
||||||
var ctx = contexts[element];
|
|
||||||
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 connection = reference('connection');
|
|
||||||
|
|
||||||
// Add _unions
|
|
||||||
clazz.addField(varFinal('_unions',
|
|
||||||
value: map({}),
|
|
||||||
type: new TypeBuilder('Map',
|
|
||||||
genericTypes: [ctx.queryClassBuilder, lib$core.bool])));
|
|
||||||
|
|
||||||
var unions = <String, bool>{'union': false, 'unionAll': true};
|
|
||||||
unions.forEach((name, all) {
|
|
||||||
var meth = new MethodBuilder(name, returnType: lib$core.$void);
|
|
||||||
meth.addPositional(parameter('query', [ctx.queryClassBuilder]));
|
|
||||||
meth.addStatement(
|
|
||||||
literal(all).asAssign(reference('_unions')[reference('query')]));
|
|
||||||
clazz.addMethod(meth);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add _sortMode
|
|
||||||
clazz.addField(varField('_sortKey', type: lib$core.String));
|
|
||||||
clazz.addField(varField('_sortMode', type: lib$core.String));
|
|
||||||
|
|
||||||
SORT_MODES.keys.forEach((sort) {
|
|
||||||
var m = new MethodBuilder('sort$sort', returnType: lib$core.$void);
|
|
||||||
m.addPositional(parameter('key', [lib$core.String]));
|
|
||||||
m.addStatement(literal(sort).asAssign(reference('_sortMode')));
|
|
||||||
m.addStatement((literal(ctx.prefix) + reference('key'))
|
|
||||||
.parentheses()
|
|
||||||
.asAssign(reference('_sortKey')));
|
|
||||||
clazz.addMethod(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add limit, offset
|
|
||||||
for (var restrictor in RESTRICTORS) {
|
|
||||||
clazz.addField(varField(restrictor, type: lib$core.int));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add and, or, not
|
|
||||||
for (var relation in RELATIONS) {
|
|
||||||
clazz.addField(varFinal('_$relation',
|
|
||||||
type: new TypeBuilder('List', genericTypes: [ctx.whereClassBuilder]),
|
|
||||||
value: list([])));
|
|
||||||
var relationMethod =
|
|
||||||
new MethodBuilder(relation, returnType: lib$core.$void);
|
|
||||||
relationMethod
|
|
||||||
.addPositional(parameter('selector', [ctx.whereClassBuilder]));
|
|
||||||
relationMethod.addStatement(
|
|
||||||
reference('_$relation').invoke('add', [reference('selector')]));
|
|
||||||
clazz.addMethod(relationMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add _buildSelectQuery()
|
|
||||||
|
|
||||||
// Add where...
|
|
||||||
clazz.addField(varFinal('where',
|
|
||||||
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));
|
|
||||||
|
|
||||||
// Add getOne()...
|
|
||||||
clazz.addMethod(buildGetOneMethod(ctx), asStatic: true);
|
|
||||||
|
|
||||||
// Add update()...
|
|
||||||
clazz.addMethod(buildUpdateMethod(ctx));
|
|
||||||
|
|
||||||
// Add delete()...
|
|
||||||
clazz.addMethod(buildDeleteMethod(ctx));
|
|
||||||
|
|
||||||
// Add deleteOne()...
|
|
||||||
clazz.addMethod(buildDeleteOneMethod(ctx), asStatic: true);
|
|
||||||
|
|
||||||
// Add insert()...
|
|
||||||
clazz.addMethod(buildInsertMethod(ctx), asStatic: true);
|
|
||||||
|
|
||||||
// Add insertX()
|
|
||||||
clazz.addMethod(buildInsertModelMethod(ctx), asStatic: true);
|
|
||||||
|
|
||||||
// Add updateX()
|
|
||||||
clazz.addMethod(buildUpdateModelMethod(ctx), asStatic: true);
|
|
||||||
|
|
||||||
// Add getAll() => new TodoQuery().get();
|
|
||||||
clazz.addMethod(
|
|
||||||
new MethodBuilder('getAll',
|
|
||||||
returnType: new TypeBuilder('Stream',
|
|
||||||
genericTypes: [ctx.modelClassBuilder]),
|
|
||||||
returns: ctx.queryClassBuilder
|
|
||||||
.newInstance([]).invoke('get', [connection]))
|
|
||||||
..addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder])),
|
|
||||||
asStatic: true);
|
|
||||||
|
|
||||||
return clazz;
|
|
||||||
}
|
|
||||||
|
|
||||||
String computeSelector(PostgresBuildContext ctx) {
|
|
||||||
var buf = new StringBuffer();
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
// Add all regular fields
|
|
||||||
ctx.fields.forEach((f) {
|
|
||||||
if (i++ > 0) buf.write(', ');
|
|
||||||
var name = ctx.resolveFieldName(f.name);
|
|
||||||
buf.write(ctx.prefix + "$name");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add all relationship fields...
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
relationship.modelTypeContext.fields.forEach((f) {
|
|
||||||
if (i++ > 0) buf.write(', ');
|
|
||||||
var name = relationship.modelTypeContext.resolveFieldName(f.name);
|
|
||||||
buf.write('${relationship.foreignTable}.$name');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return buf.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildToSqlMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('toSql', returnType: lib$core.String);
|
|
||||||
meth.addPositional(parameter('prefix', [lib$core.String]).asOptional());
|
|
||||||
var buf = reference('buf');
|
|
||||||
meth.addStatement(
|
|
||||||
varField('buf', value: lib$core.StringBuffer.newInstance([])));
|
|
||||||
|
|
||||||
// Write prefix, or default to SELECT
|
|
||||||
var prefix = reference('prefix');
|
|
||||||
meth.addStatement(buf.invoke('write', [
|
|
||||||
prefix.notEquals(literal(null)).ternary(prefix,
|
|
||||||
literal('SELECT ${computeSelector(ctx)} FROM "${ctx.tableName}"'))
|
|
||||||
]));
|
|
||||||
|
|
||||||
var relationsIfThen = ifThen(prefix.equals(literal(null)));
|
|
||||||
|
|
||||||
// Apply relationships
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
|
|
||||||
// TODO: Belongs to many, has many
|
|
||||||
if (relationship.isSingular) {
|
|
||||||
String b = ' LEFT OUTER JOIN ${relationship.foreignTable} ON ${ctx.tableName}.${relationship.localKey} = ${relationship.foreignTable}.${relationship.foreignKey}';
|
|
||||||
relationsIfThen.addStatement(buf.invoke('write', [literal(b)]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
meth.addStatement(relationsIfThen);
|
|
||||||
|
|
||||||
meth.addStatement(varField('whereClause',
|
|
||||||
value: reference('where').invoke('toWhereClause', [])));
|
|
||||||
|
|
||||||
var whereClause = reference('whereClause');
|
|
||||||
|
|
||||||
meth.addStatement(ifThen(whereClause.notEquals(literal(null)), [
|
|
||||||
buf.invoke('write', [literal(' ') + whereClause])
|
|
||||||
]));
|
|
||||||
|
|
||||||
for (var relation in RELATIONS) {
|
|
||||||
var ref = reference('_$relation'),
|
|
||||||
x = reference('x'),
|
|
||||||
whereClause = reference('whereClause');
|
|
||||||
var upper = relation.toUpperCase();
|
|
||||||
var closure = new MethodBuilder.closure();
|
|
||||||
closure.addPositional(parameter('x'));
|
|
||||||
closure.addStatement(varField('whereClause',
|
|
||||||
value: x.invoke('toWhereClause', [],
|
|
||||||
namedArguments: {'keyword': literal(false)})));
|
|
||||||
closure.addStatement(ifThen(whereClause.notEquals(literal(null)), [
|
|
||||||
buf.invoke('write', [literal(' $upper (') + whereClause + literal(')')])
|
|
||||||
]));
|
|
||||||
|
|
||||||
meth.addStatement(ref.invoke('forEach', [closure]));
|
|
||||||
}
|
|
||||||
|
|
||||||
var ifNoPrefix = ifThen(reference('prefix').equals(literal(null)));
|
|
||||||
|
|
||||||
for (var restrictor in RESTRICTORS) {
|
|
||||||
var ref = reference(restrictor);
|
|
||||||
var upper = restrictor.toUpperCase();
|
|
||||||
ifNoPrefix.addStatement(ifThen(ref.notEquals(literal(null)), [
|
|
||||||
buf.invoke('write', [literal(' $upper ') + ref.invoke('toString', [])])
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortMode = reference('_sortMode');
|
|
||||||
|
|
||||||
SORT_MODES.forEach((k, sort) {
|
|
||||||
ifNoPrefix.addStatement(ifThen(sortMode.equals(literal(k)), [
|
|
||||||
buf.invoke('write', [
|
|
||||||
literal(' ORDER BY "') + reference('_sortKey') + literal('" $sort')
|
|
||||||
])
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add unions
|
|
||||||
var unionClosure = new MethodBuilder.closure();
|
|
||||||
unionClosure.addPositional(parameter('query'));
|
|
||||||
unionClosure.addPositional(parameter('all'));
|
|
||||||
unionClosure.addStatement(buf.invoke('write', [literal(' UNION')]));
|
|
||||||
unionClosure.addStatement(ifThen(reference('all'), [
|
|
||||||
buf.invoke('write', [literal(' ALL')])
|
|
||||||
]));
|
|
||||||
unionClosure.addStatement(buf.invoke('write', [literal(' (')]));
|
|
||||||
unionClosure.addStatement(varField('sql',
|
|
||||||
value: reference('query').invoke('toSql', []).invoke(
|
|
||||||
'replaceAll', [literal(';'), literal('')])));
|
|
||||||
unionClosure
|
|
||||||
.addStatement(buf.invoke('write', [reference('sql') + literal(')')]));
|
|
||||||
|
|
||||||
ifNoPrefix
|
|
||||||
.addStatement(reference('_unions').invoke('forEach', [unionClosure]));
|
|
||||||
|
|
||||||
ifNoPrefix.addStatement(buf.invoke('write', [literal(';')]));
|
|
||||||
|
|
||||||
meth.addStatement(ifNoPrefix);
|
|
||||||
meth.addStatement(buf.invoke('toString', []).asReturn());
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildParseRowMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('parseRow', returnType: ctx.modelClassBuilder);
|
|
||||||
meth.addPositional(parameter('row', [lib$core.List]));
|
|
||||||
//meth.addStatement(lib$core.print.call(
|
|
||||||
// [literal('ROW MAP: ') + reference('row').invoke('toString', [])]));
|
|
||||||
var row = reference('row');
|
|
||||||
|
|
||||||
// We want to create a Map using the SQL row.
|
|
||||||
Map<String, ExpressionBuilder> data = {};
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
var name = ctx.resolveFieldName(field.name);
|
|
||||||
var rowKey = row[literal(i++)];
|
|
||||||
|
|
||||||
if (field.name == 'id' && ctx.shimmed.containsKey('id')) {
|
|
||||||
data[name] = rowKey.invoke('toString', []);
|
|
||||||
} else
|
|
||||||
data[name] = rowKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invoke fromJson()
|
|
||||||
var result = reference('result');
|
|
||||||
meth.addStatement(varField('result',
|
|
||||||
value: ctx.modelClassBuilder
|
|
||||||
.newInstance([map(data)], constructor: 'fromJson')));
|
|
||||||
|
|
||||||
// For each relationship, try to parse
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
int minIndex = i;
|
|
||||||
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
var rc = new ReCase(relationship.isList
|
|
||||||
? relationship.modelType.name
|
|
||||||
: relationship.dartType.name);
|
|
||||||
var relationshipQuery = new TypeBuilder('${rc.pascalCase}Query');
|
|
||||||
List<ExpressionBuilder> relationshipRow = [];
|
|
||||||
|
|
||||||
relationship.modelTypeContext.fields.forEach((f) {
|
|
||||||
relationshipRow.add(row[literal(i++)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
meth.addStatement(ifThen(row.property('length') > literal(minIndex), [
|
|
||||||
relationshipQuery.invoke(
|
|
||||||
'parseRow', [list(relationshipRow)]).asAssign(result.property(name))
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then, call a .fromJson() constructor
|
|
||||||
meth.addStatement(result.asReturn());
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _invokeStreamClosure(
|
|
||||||
PostgresBuildContext ctx, ExpressionBuilder future, MethodBuilder meth) {
|
|
||||||
var ctrl = reference('ctrl');
|
|
||||||
// Invoke query...
|
|
||||||
var catchError = ctrl.property('addError');
|
|
||||||
var then = new MethodBuilder.closure(modifier: MethodModifier.asAsync)
|
|
||||||
..addPositional(parameter('rows'));
|
|
||||||
|
|
||||||
var forEachClosure =
|
|
||||||
new MethodBuilder.closure(modifier: MethodModifier.asAsync);
|
|
||||||
forEachClosure.addPositional(parameter('row'));
|
|
||||||
forEachClosure.addStatement(varField('parsed',
|
|
||||||
value: reference('parseRow').call([reference('row')])));
|
|
||||||
_applyRelationshipsToOutput(
|
|
||||||
ctx, reference('parsed'), reference('row'), forEachClosure);
|
|
||||||
forEachClosure.addStatement(reference('parsed').asReturn());
|
|
||||||
|
|
||||||
then.addStatement(varField('futures',
|
|
||||||
value: reference('rows').invoke('map', [forEachClosure])));
|
|
||||||
then.addStatement(varField('output',
|
|
||||||
value:
|
|
||||||
lib$async.Future.invoke('wait', [reference('futures')]).asAwait()));
|
|
||||||
then.addStatement(
|
|
||||||
reference('output').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());
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildGetMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('get',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Stream', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth.addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
var streamController = new TypeBuilder('StreamController',
|
|
||||||
genericTypes: [ctx.modelClassBuilder]);
|
|
||||||
meth.addStatement(varField('ctrl',
|
|
||||||
type: streamController, value: streamController.newInstance([])));
|
|
||||||
|
|
||||||
var future =
|
|
||||||
reference('connection').invoke('query', [reference('toSql').call([])]);
|
|
||||||
_invokeStreamClosure(ctx, future, meth);
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildGetOneMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('getOne',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth.addPositional(parameter('id', [lib$core.int]));
|
|
||||||
meth.addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
|
|
||||||
var query = reference('query'),
|
|
||||||
whereId = query.property('where').property('id');
|
|
||||||
meth.addStatement(
|
|
||||||
varField('query', value: ctx.queryClassBuilder.newInstance([])));
|
|
||||||
meth.addStatement(whereId.invoke('equals', [reference('id')]));
|
|
||||||
|
|
||||||
// Return null on error
|
|
||||||
var catchErr = new MethodBuilder.closure(returns: literal(null));
|
|
||||||
catchErr.addPositional(parameter('_'));
|
|
||||||
|
|
||||||
meth.addStatement(query
|
|
||||||
.invoke('get', [reference('connection')])
|
|
||||||
.property('first')
|
|
||||||
.invoke('catchError', [catchErr])
|
|
||||||
.asReturn());
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addAllNamed(MethodBuilder meth, PostgresBuildContext ctx) {
|
|
||||||
// Add all named params
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
if (field.name != 'id') {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addReturning(StringBuffer buf, PostgresBuildContext ctx) {
|
|
||||||
buf.write(' RETURNING ');
|
|
||||||
int i = 0;
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
if (i++ > 0) buf.write(', ');
|
|
||||||
var name = ctx.resolveFieldName(field.name);
|
|
||||||
buf.write('"$name"');
|
|
||||||
});
|
|
||||||
|
|
||||||
buf.write(';');
|
|
||||||
}
|
|
||||||
|
|
||||||
void _ensureDates(MethodBuilder meth, PostgresBuildContext ctx) {
|
|
||||||
if (ctx.fields.any((f) => f.name == 'createdAt' || f.name == 'updatedAt')) {
|
|
||||||
meth.addStatement(varField('__ormNow__',
|
|
||||||
value: lib$core.DateTime.newInstance([], constructor: 'now')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, ExpressionBuilder> _buildSubstitutionValues(
|
|
||||||
PostgresBuildContext ctx) {
|
|
||||||
Map<String, ExpressionBuilder> substitutionValues = {};
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
if (field.name == 'id')
|
|
||||||
return;
|
|
||||||
else if (field.name == 'createdAt' || field.name == 'updatedAt') {
|
|
||||||
var ref = reference(field.name);
|
|
||||||
substitutionValues[field.name] =
|
|
||||||
ref.notEquals(literal(null)).ternary(ref, reference('__ormNow__'));
|
|
||||||
} else
|
|
||||||
substitutionValues[field.name] = reference(field.name);
|
|
||||||
});
|
|
||||||
return substitutionValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpressionBuilder _executeQuery(ExpressionBuilder queryString,
|
|
||||||
MethodBuilder meth, Map<String, ExpressionBuilder> substitutionValues) {
|
|
||||||
var connection = reference('connection');
|
|
||||||
var query = queryString;
|
|
||||||
return connection.invoke('query', [query],
|
|
||||||
namedArguments: {'substitutionValues': map(substitutionValues)});
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildUpdateMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('update',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Stream', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth.addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
_addAllNamed(meth, ctx);
|
|
||||||
|
|
||||||
var buf = new StringBuffer('UPDATE "${ctx.tableName}" SET (');
|
|
||||||
int i = 0;
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
if (field.name == 'id')
|
|
||||||
return;
|
|
||||||
else {
|
|
||||||
if (i++ > 0) buf.write(', ');
|
|
||||||
var key = ctx.resolveFieldName(field.name);
|
|
||||||
buf.write('"$key"');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
buf.write(') = (');
|
|
||||||
i = 0;
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
if (field.name == 'id')
|
|
||||||
return;
|
|
||||||
else {
|
|
||||||
if (i++ > 0) buf.write(', ');
|
|
||||||
buf.write('@${field.name}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
buf.write(') ');
|
|
||||||
|
|
||||||
var $buf = reference('buf');
|
|
||||||
var whereClause = reference('whereClause');
|
|
||||||
meth.addStatement(varField('buf',
|
|
||||||
value: lib$core.StringBuffer.newInstance([literal(buf.toString())])));
|
|
||||||
meth.addStatement(varField('whereClause',
|
|
||||||
value: reference('where').invoke('toWhereClause', [])));
|
|
||||||
|
|
||||||
meth.addStatement(ifThen(whereClause.notEquals(literal(null)), [
|
|
||||||
$buf.invoke('write', [whereClause])
|
|
||||||
]));
|
|
||||||
|
|
||||||
var buf2 = new StringBuffer();
|
|
||||||
_addReturning(buf2, ctx);
|
|
||||||
_ensureDates(meth, ctx);
|
|
||||||
var substitutionValues = _buildSubstitutionValues(ctx);
|
|
||||||
|
|
||||||
var ctrlType = new TypeBuilder('StreamController',
|
|
||||||
genericTypes: [ctx.modelClassBuilder]);
|
|
||||||
meth.addStatement(varField('ctrl', value: ctrlType.newInstance([])));
|
|
||||||
var result = _executeQuery(
|
|
||||||
$buf.invoke('toString', []) + literal(buf2.toString()),
|
|
||||||
meth,
|
|
||||||
substitutionValues);
|
|
||||||
_invokeStreamClosure(ctx, result, meth);
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildDeleteMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('delete',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Stream', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth.addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
|
|
||||||
var litBuf = new StringBuffer();
|
|
||||||
_addReturning(litBuf, ctx);
|
|
||||||
|
|
||||||
var streamController = new TypeBuilder('StreamController',
|
|
||||||
genericTypes: [ctx.modelClassBuilder]);
|
|
||||||
meth.addStatement(varField('ctrl',
|
|
||||||
type: streamController, value: streamController.newInstance([])));
|
|
||||||
|
|
||||||
var future = reference('connection').invoke('query', [
|
|
||||||
reference('toSql').call([literal('DELETE FROM "${ctx.tableName}"')]) +
|
|
||||||
literal(litBuf.toString())
|
|
||||||
]);
|
|
||||||
_invokeStreamClosure(ctx, future, meth);
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildDeleteOneMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('deleteOne',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]))
|
|
||||||
..addPositional(parameter('id', [lib$core.int]))
|
|
||||||
..addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
|
|
||||||
var id = reference('id'),
|
|
||||||
connection = reference('connection'),
|
|
||||||
query = reference('query');
|
|
||||||
meth.addStatement(
|
|
||||||
varField('query', value: ctx.queryClassBuilder.newInstance([])));
|
|
||||||
meth.addStatement(
|
|
||||||
query.property('where').property('id').invoke('equals', [id]));
|
|
||||||
meth.addStatement(
|
|
||||||
query.invoke('delete', [connection]).property('first').asReturn());
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildInsertMethod(PostgresBuildContext ctx) {
|
|
||||||
var meth = new MethodBuilder('insert',
|
|
||||||
modifier: MethodModifier.asAsync,
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
meth.addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
|
|
||||||
// Add all named params
|
|
||||||
_addAllNamed(meth, ctx);
|
|
||||||
|
|
||||||
var buf = new StringBuffer('INSERT INTO "${ctx.tableName}" (');
|
|
||||||
int i = 0;
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
if (field.name == 'id')
|
|
||||||
return;
|
|
||||||
else {
|
|
||||||
if (i++ > 0) buf.write(', ');
|
|
||||||
var key = ctx.resolveFieldName(field.name);
|
|
||||||
buf.write('"$key"');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
buf.write(') VALUES (');
|
|
||||||
i = 0;
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
if (field.name == 'id')
|
|
||||||
return;
|
|
||||||
else {
|
|
||||||
if (i++ > 0) buf.write(', ');
|
|
||||||
buf.write('@${field.name}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
buf.write(')');
|
|
||||||
// meth.addStatement(lib$core.print.call([literal(buf.toString())]));
|
|
||||||
|
|
||||||
_addReturning(buf, ctx);
|
|
||||||
_ensureDates(meth, ctx);
|
|
||||||
|
|
||||||
var substitutionValues = _buildSubstitutionValues(ctx);
|
|
||||||
|
|
||||||
var connection = reference('connection');
|
|
||||||
var query = literal(buf.toString());
|
|
||||||
var result = reference('result'), output = reference('output');
|
|
||||||
meth.addStatement(varField('result',
|
|
||||||
value: connection.invoke('query', [
|
|
||||||
query
|
|
||||||
], namedArguments: {
|
|
||||||
'substitutionValues': map(substitutionValues)
|
|
||||||
}).asAwait()));
|
|
||||||
|
|
||||||
meth.addStatement(varField('output',
|
|
||||||
value: reference('parseRow').call([result[literal(0)]])));
|
|
||||||
|
|
||||||
_applyRelationshipsToOutput(ctx, output, result[literal(0)], meth);
|
|
||||||
|
|
||||||
meth.addStatement(output.asReturn());
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _applyRelationshipsToOutput(PostgresBuildContext ctx,
|
|
||||||
ExpressionBuilder output, ExpressionBuilder row, MethodBuilder meth) {
|
|
||||||
// Every relationship should fill itself in with a query
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
|
|
||||||
var rc = new ReCase(relationship.isList
|
|
||||||
? relationship.modelType.name
|
|
||||||
: relationship.dartType.name);
|
|
||||||
var type = new TypeBuilder('${rc.pascalCase}Query');
|
|
||||||
|
|
||||||
// Resolve index within row...
|
|
||||||
bool matched = false;
|
|
||||||
int col = 0;
|
|
||||||
for (var field in ctx.fields) {
|
|
||||||
if (field is RelationshipConstraintField &&
|
|
||||||
field.originalName == name) {
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
} else
|
|
||||||
col++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched) {
|
|
||||||
matched = ctx.resolveRelationshipField(name) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched)
|
|
||||||
throw 'Couldn\'t resolve row index for relationship "${name}".';
|
|
||||||
|
|
||||||
var idAsInt = row[literal(col)];
|
|
||||||
|
|
||||||
if (relationship.isSingular) {
|
|
||||||
if (relationship.isBelongsTo) {
|
|
||||||
meth.addStatement(type
|
|
||||||
.invoke('getOne', [idAsInt, reference('connection')])
|
|
||||||
.asAwait()
|
|
||||||
.asAssign(output.property(name)));
|
|
||||||
} else {
|
|
||||||
var query = reference('${rc.camelCase}Query');
|
|
||||||
meth.addStatement(
|
|
||||||
varField('${rc.camelCase}Query', value: type.newInstance([])));
|
|
||||||
// Set id to row[0]
|
|
||||||
meth.addStatement(query
|
|
||||||
.property('where')
|
|
||||||
.property('id')
|
|
||||||
.invoke('equals', [row[literal(0)]]));
|
|
||||||
var fetched = query
|
|
||||||
.invoke('get', [reference('connection')])
|
|
||||||
.property('first')
|
|
||||||
.invoke('catchError', [
|
|
||||||
new MethodBuilder.closure(returns: literal(null))
|
|
||||||
..addPositional(parameter('_'))
|
|
||||||
])
|
|
||||||
.asAwait();
|
|
||||||
meth.addStatement(fetched.asAssign(output.property(name)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var query = reference('${rc.camelCase}Query');
|
|
||||||
meth.addStatement(
|
|
||||||
varField('${rc.camelCase}Query', value: type.newInstance([])));
|
|
||||||
ExpressionBuilder fetched;
|
|
||||||
|
|
||||||
// TODO: HasMany
|
|
||||||
if (relationship.isBelongsTo) {
|
|
||||||
meth.addStatement(query
|
|
||||||
.property('where')
|
|
||||||
.property('id')
|
|
||||||
.invoke('equals', [idAsInt]));
|
|
||||||
fetched = query.invoke('get', [reference('connection')]).invoke(
|
|
||||||
'toList', []).asAwait();
|
|
||||||
}
|
|
||||||
|
|
||||||
meth.addStatement(output.property(name).invoke('addAll', [fetched]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addRelationshipConstraintsNamed(
|
|
||||||
MethodBuilder m, PostgresBuildContext ctx) {
|
|
||||||
ctx.relationships.forEach((name, r) {
|
|
||||||
var relationship = ctx.populateRelationship(name);
|
|
||||||
|
|
||||||
if (relationship.isBelongsTo) {
|
|
||||||
var rc = new ReCase(relationship.localKey);
|
|
||||||
m.addNamed(parameter(rc.camelCase, [lib$core.int]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildInsertModelMethod(PostgresBuildContext ctx) {
|
|
||||||
var rc = new ReCase(ctx.modelClassName);
|
|
||||||
var meth = new MethodBuilder('insert${rc.pascalCase}',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
|
|
||||||
meth.addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
meth.addPositional(parameter(rc.camelCase, [ctx.modelClassBuilder]));
|
|
||||||
_addRelationshipConstraintsNamed(meth, ctx);
|
|
||||||
|
|
||||||
Map<String, ExpressionBuilder> args = {};
|
|
||||||
var ref = reference(rc.camelCase);
|
|
||||||
|
|
||||||
ctx.fields.forEach((f) {
|
|
||||||
if (f.name != 'id') {
|
|
||||||
args[f.name] = f is RelationshipConstraintField
|
|
||||||
? reference(f.name)
|
|
||||||
: ref.property(f.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
meth.addStatement(ctx.queryClassBuilder
|
|
||||||
.invoke('insert', [reference('connection')], namedArguments: args)
|
|
||||||
.asReturn());
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodBuilder buildUpdateModelMethod(PostgresBuildContext ctx) {
|
|
||||||
var rc = new ReCase(ctx.modelClassName);
|
|
||||||
var meth = new MethodBuilder('update${rc.pascalCase}',
|
|
||||||
returnType:
|
|
||||||
new TypeBuilder('Future', genericTypes: [ctx.modelClassBuilder]));
|
|
||||||
|
|
||||||
meth.addPositional(
|
|
||||||
parameter('connection', [ctx.postgreSQLConnectionBuilder]));
|
|
||||||
meth.addPositional(parameter(rc.camelCase, [ctx.modelClassBuilder]));
|
|
||||||
|
|
||||||
// var query = new XQuery();
|
|
||||||
var ref = reference(rc.camelCase);
|
|
||||||
var query = reference('query');
|
|
||||||
meth.addStatement(
|
|
||||||
varField('query', value: ctx.queryClassBuilder.newInstance([])));
|
|
||||||
|
|
||||||
// query.where.id.equals(x.id);
|
|
||||||
meth.addStatement(query.property('where').property('id').invoke('equals', [
|
|
||||||
lib$core.int.invoke('parse', [ref.property('id')])
|
|
||||||
]));
|
|
||||||
|
|
||||||
// return query.update(connection, ...).first;
|
|
||||||
Map<String, ExpressionBuilder> args = {};
|
|
||||||
ctx.fields.forEach((f) {
|
|
||||||
if (f.name != 'id') {
|
|
||||||
if (f is RelationshipConstraintField) {
|
|
||||||
// Need to int.parse the related id and pass it
|
|
||||||
var relation = ref.property(f.originalName);
|
|
||||||
var relationship = ctx.populateRelationship(f.originalName);
|
|
||||||
args[f.name] = lib$core.int
|
|
||||||
.invoke('parse', [relation.property(relationship.foreignKey)]);
|
|
||||||
} else
|
|
||||||
args[f.name] = ref.property(f.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var update =
|
|
||||||
query.invoke('update', [reference('connection')], namedArguments: args);
|
|
||||||
meth.addStatement(update.property('first').asReturn());
|
|
||||||
|
|
||||||
return meth;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClassBuilder buildWhereClass(PostgresBuildContext ctx) {
|
|
||||||
var clazz = new ClassBuilder(ctx.whereClassName);
|
|
||||||
|
|
||||||
ctx.fields.forEach((field) {
|
|
||||||
TypeBuilder queryBuilderType;
|
|
||||||
List<ExpressionBuilder> args = [];
|
|
||||||
|
|
||||||
if (field.name == 'id') {
|
|
||||||
queryBuilderType = new TypeBuilder('NumericSqlExpressionBuilder',
|
|
||||||
genericTypes: [lib$core.int]);
|
|
||||||
} else {
|
|
||||||
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.tableName + '.' + 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);
|
|
||||||
toWhereClause.addNamed(parameter('keyword', [lib$core.bool]));
|
|
||||||
|
|
||||||
// List<String> 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.isAssignableTo(ctx.dateTimeType)
|
|
||||||
? queryBuilder.invoke('compile', [])
|
|
||||||
: (literal('${ctx.tableName}.$name ') +
|
|
||||||
queryBuilder.invoke('compile', []));
|
|
||||||
|
|
||||||
toWhereClause.addStatement(ifThen(queryBuilder.property('hasValue'), [
|
|
||||||
expressions.invoke('add', [toAdd])
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
var kw = reference('keyword')
|
|
||||||
.notEquals(literal(false))
|
|
||||||
.ternary(literal('WHERE '), literal(''))
|
|
||||||
.parentheses();
|
|
||||||
|
|
||||||
// return expressions.isEmpty ? null : ('WHERE ' + expressions.join(' AND '));
|
|
||||||
toWhereClause.addStatement(expressions
|
|
||||||
.property('isEmpty')
|
|
||||||
.ternary(literal(null),
|
|
||||||
(kw + expressions.invoke('join', [literal(' AND ')])).parentheses())
|
|
||||||
.asReturn());
|
|
||||||
|
|
||||||
clazz.addMethod(toWhereClause);
|
|
||||||
|
|
||||||
return clazz;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,259 +0,0 @@
|
||||||
import 'package:analyzer/dart/constant/value.dart';
|
|
||||||
import 'package:analyzer/dart/element/element.dart';
|
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
|
||||||
import 'package:analyzer/src/generated/resolver.dart';
|
|
||||||
import 'package:angel_orm/angel_orm.dart';
|
|
||||||
import 'package:angel_serialize_generator/context.dart';
|
|
||||||
import 'package:build/build.dart';
|
|
||||||
import 'package:code_builder/code_builder.dart';
|
|
||||||
import 'package:inflection/inflection.dart';
|
|
||||||
import 'package:recase/recase.dart';
|
|
||||||
import 'package:source_gen/source_gen.dart';
|
|
||||||
import 'build_context.dart';
|
|
||||||
|
|
||||||
class PostgresBuildContext extends BuildContext {
|
|
||||||
DartType _dateTimeTypeCache;
|
|
||||||
LibraryElement _libraryCache;
|
|
||||||
TypeProvider _typeProviderCache;
|
|
||||||
TypeBuilder _modelClassBuilder,
|
|
||||||
_queryClassBuilder,
|
|
||||||
_whereClassBuilder,
|
|
||||||
_postgresqlConnectionBuilder;
|
|
||||||
String _prefix;
|
|
||||||
final Map<String, Relationship> _populatedRelationships = {};
|
|
||||||
final Map<String, Column> columnInfo = {};
|
|
||||||
final Map<String, IndexType> indices = {};
|
|
||||||
final Map<String, Relationship> relationships = {};
|
|
||||||
final bool autoSnakeCaseNames, autoIdAndDateFields;
|
|
||||||
final String tableName;
|
|
||||||
final ORM ormAnnotation;
|
|
||||||
final BuildContext raw;
|
|
||||||
final Resolver resolver;
|
|
||||||
final BuildStep buildStep;
|
|
||||||
String primaryKeyName = 'id';
|
|
||||||
|
|
||||||
PostgresBuildContext(
|
|
||||||
this.raw, this.ormAnnotation, this.resolver, this.buildStep,
|
|
||||||
{this.tableName, this.autoSnakeCaseNames, this.autoIdAndDateFields})
|
|
||||||
: super(raw.annotation,
|
|
||||||
originalClassName: raw.originalClassName,
|
|
||||||
sourceFilename: raw.sourceFilename);
|
|
||||||
|
|
||||||
final List<FieldElement> fields = [], relationshipFields = [];
|
|
||||||
|
|
||||||
TypeBuilder get modelClassBuilder =>
|
|
||||||
_modelClassBuilder ??= new TypeBuilder(modelClassName);
|
|
||||||
|
|
||||||
TypeBuilder get queryClassBuilder =>
|
|
||||||
_queryClassBuilder ??= new TypeBuilder(queryClassName);
|
|
||||||
|
|
||||||
TypeBuilder get whereClassBuilder =>
|
|
||||||
_whereClassBuilder ??= new TypeBuilder(whereClassName);
|
|
||||||
|
|
||||||
TypeBuilder get postgreSQLConnectionBuilder =>
|
|
||||||
_postgresqlConnectionBuilder ??= new TypeBuilder('PostgreSQLConnection');
|
|
||||||
|
|
||||||
String get prefix {
|
|
||||||
if (_prefix != null) return _prefix;
|
|
||||||
if (relationships.isEmpty)
|
|
||||||
return _prefix = '';
|
|
||||||
else
|
|
||||||
return _prefix = tableName + '.';
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> get aliases => raw.aliases;
|
|
||||||
|
|
||||||
Map<String, bool> 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';
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
FieldElement resolveRelationshipField(String name) =>
|
|
||||||
relationshipFields.firstWhere((f) => f.name == name, orElse: () => null);
|
|
||||||
|
|
||||||
PopulatedRelationship populateRelationship(String name) {
|
|
||||||
return _populatedRelationships.putIfAbsent(name, () {
|
|
||||||
var f = raw.fields.firstWhere((f) => f.name == name);
|
|
||||||
var relationship = relationships[name];
|
|
||||||
DartType refType = f.type;
|
|
||||||
|
|
||||||
if (refType.isAssignableTo(typeProvider.listType) || refType.name == 'List') {
|
|
||||||
var iType = refType as InterfaceType;
|
|
||||||
|
|
||||||
if (iType.typeArguments.isEmpty)
|
|
||||||
throw 'Relationship "${f.name}" cannot be modeled as a generic List.';
|
|
||||||
|
|
||||||
refType = iType.typeArguments.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
var typeName =
|
|
||||||
refType.name.startsWith('_') ? refType.name.substring(1) : refType.name;
|
|
||||||
var rc = new ReCase(typeName);
|
|
||||||
|
|
||||||
if (relationship.type == RelationshipType.HAS_ONE ||
|
|
||||||
relationship.type == RelationshipType.HAS_MANY) {
|
|
||||||
var single = singularize(tableName);
|
|
||||||
var foreignKey = relationship.foreignTable ??
|
|
||||||
(autoSnakeCaseNames != false
|
|
||||||
? '${single}_id'
|
|
||||||
: '${single}Id');
|
|
||||||
var localKey = relationship.localKey ?? 'id';
|
|
||||||
var foreignTable = relationship.foreignTable ??
|
|
||||||
(autoSnakeCaseNames != false
|
|
||||||
? pluralize(rc.snakeCase)
|
|
||||||
: pluralize(typeName));
|
|
||||||
return new PopulatedRelationship(
|
|
||||||
relationship.type,
|
|
||||||
f.name,
|
|
||||||
f.type,
|
|
||||||
buildStep,
|
|
||||||
resolver,
|
|
||||||
autoSnakeCaseNames,
|
|
||||||
autoIdAndDateFields,
|
|
||||||
relationship.type == RelationshipType.HAS_ONE,
|
|
||||||
typeProvider,
|
|
||||||
localKey: localKey,
|
|
||||||
foreignKey: foreignKey,
|
|
||||||
foreignTable: foreignTable,
|
|
||||||
cascadeOnDelete: relationship.cascadeOnDelete);
|
|
||||||
} else if (relationship.type == RelationshipType.BELONGS_TO ||
|
|
||||||
relationship.type == RelationshipType.BELONGS_TO_MANY) {
|
|
||||||
var localKey = relationship.localKey ??
|
|
||||||
(autoSnakeCaseNames != false
|
|
||||||
? '${rc.snakeCase}_id'
|
|
||||||
: '${typeName}Id');
|
|
||||||
var foreignKey = relationship.foreignKey ?? 'id';
|
|
||||||
var foreignTable = relationship.foreignTable ??
|
|
||||||
(autoSnakeCaseNames != false
|
|
||||||
? pluralize(rc.snakeCase)
|
|
||||||
: pluralize(typeName));
|
|
||||||
return new PopulatedRelationship(
|
|
||||||
relationship.type,
|
|
||||||
f.name,
|
|
||||||
f.type,
|
|
||||||
buildStep,
|
|
||||||
resolver,
|
|
||||||
autoSnakeCaseNames,
|
|
||||||
autoIdAndDateFields,
|
|
||||||
relationship.type == RelationshipType.BELONGS_TO,
|
|
||||||
typeProvider,
|
|
||||||
localKey: localKey,
|
|
||||||
foreignKey: foreignKey,
|
|
||||||
foreignTable: foreignTable,
|
|
||||||
cascadeOnDelete: relationship.cascadeOnDelete);
|
|
||||||
} else
|
|
||||||
throw new UnsupportedError(
|
|
||||||
'Invalid relationship type: ${relationship.type}');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PopulatedRelationship extends Relationship {
|
|
||||||
bool _isList;
|
|
||||||
DartType _modelType;
|
|
||||||
PostgresBuildContext _modelTypeContext;
|
|
||||||
DartObject _modelTypeORM;
|
|
||||||
final String originalName;
|
|
||||||
final DartType dartType;
|
|
||||||
final BuildStep buildStep;
|
|
||||||
final Resolver resolver;
|
|
||||||
final bool autoSnakeCaseNames, autoIdAndDateFields;
|
|
||||||
final bool isSingular;
|
|
||||||
final TypeProvider typeProvider;
|
|
||||||
|
|
||||||
PopulatedRelationship(
|
|
||||||
int type,
|
|
||||||
this.originalName,
|
|
||||||
this.dartType,
|
|
||||||
this.buildStep,
|
|
||||||
this.resolver,
|
|
||||||
this.autoSnakeCaseNames,
|
|
||||||
this.autoIdAndDateFields,
|
|
||||||
this.isSingular,
|
|
||||||
this.typeProvider,
|
|
||||||
{String localKey,
|
|
||||||
String foreignKey,
|
|
||||||
String foreignTable,
|
|
||||||
bool cascadeOnDelete})
|
|
||||||
: super(type,
|
|
||||||
localKey: localKey,
|
|
||||||
foreignKey: foreignKey,
|
|
||||||
foreignTable: foreignTable,
|
|
||||||
cascadeOnDelete: cascadeOnDelete);
|
|
||||||
|
|
||||||
bool get isBelongsTo =>
|
|
||||||
type == RelationshipType.BELONGS_TO ||
|
|
||||||
type == RelationshipType.BELONGS_TO_MANY;
|
|
||||||
|
|
||||||
bool get isHas =>
|
|
||||||
type == RelationshipType.HAS_ONE || type == RelationshipType.HAS_MANY;
|
|
||||||
|
|
||||||
bool get isList => _isList ??=
|
|
||||||
dartType.isAssignableTo(typeProvider.listType) || dartType.name == 'List';
|
|
||||||
|
|
||||||
DartType get modelType {
|
|
||||||
if (_modelType != null) return _modelType;
|
|
||||||
DartType searchType = dartType;
|
|
||||||
var ormChecker = new TypeChecker.fromRuntime(ORM);
|
|
||||||
|
|
||||||
// Get inner type from List if any...
|
|
||||||
if (!isSingular) {
|
|
||||||
if (!isList)
|
|
||||||
throw '"$originalName" is a many-to-one relationship, and thus it should be represented as a List within your Dart class. You have it represented as ${dartType.name}.';
|
|
||||||
else {
|
|
||||||
var iType = dartType as InterfaceType;
|
|
||||||
if (iType.typeArguments.isEmpty)
|
|
||||||
throw '"$originalName" is a many-to-one relationship, and should be modeled as a List that references another model type. Example: `List<T>`, where T is a model type.';
|
|
||||||
else
|
|
||||||
searchType = iType.typeArguments.first;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (searchType != null) {
|
|
||||||
var classElement = searchType.element as ClassElement;
|
|
||||||
var ormAnnotation = ormChecker.firstAnnotationOf(classElement);
|
|
||||||
|
|
||||||
if (ormAnnotation != null) {
|
|
||||||
_modelTypeORM = ormAnnotation;
|
|
||||||
return _modelType = searchType;
|
|
||||||
} else {
|
|
||||||
// If we didn't find an @ORM(), then refer to the parent type.
|
|
||||||
searchType = classElement.supertype;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new StateError(
|
|
||||||
'Neither ${dartType.name} nor its parent types are annotated with an @ORM() annotation. It is impossible to compute this relationship.');
|
|
||||||
}
|
|
||||||
|
|
||||||
PostgresBuildContext get modelTypeContext {
|
|
||||||
if (_modelTypeContext != null) return _modelTypeContext;
|
|
||||||
var reader = new ConstantReader(_modelTypeORM);
|
|
||||||
if (reader.isNull)
|
|
||||||
reader = null;
|
|
||||||
else
|
|
||||||
reader = reader.read('tableName');
|
|
||||||
var orm = reader == null
|
|
||||||
? new ORM()
|
|
||||||
: new ORM(reader.isString ? reader.stringValue : null);
|
|
||||||
return _modelTypeContext = buildContext(modelType.element, orm, buildStep,
|
|
||||||
resolver, autoSnakeCaseNames, autoIdAndDateFields);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,5 +33,10 @@ final PhaseGroup PHASES = new PhaseGroup()
|
||||||
new GeneratorBuilder([new PostgresORMGenerator()],
|
new GeneratorBuilder([new PostgresORMGenerator()],
|
||||||
isStandalone: true, generatedExtension: '.orm.g.dart'),
|
isStandalone: true, generatedExtension: '.orm.g.dart'),
|
||||||
DEPENDENT_MODELS))
|
DEPENDENT_MODELS))
|
||||||
|
..addPhase(new Phase()
|
||||||
|
..addAction(
|
||||||
|
new GeneratorBuilder([new PostgresServiceGenerator()],
|
||||||
|
isStandalone: true, generatedExtension: '.service.g.dart'),
|
||||||
|
ALL_MODELS))
|
||||||
..addPhase(new Phase()
|
..addPhase(new Phase()
|
||||||
..addAction(new SQLMigrationGenerator(temporary: true), ALL_MODELS));
|
..addAction(new SQLMigrationGenerator(temporary: true), ALL_MODELS));
|
||||||
|
|
Loading…
Reference in a new issue