From 3a85da79be485c1af8eae8cc229019baba1a3013 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Thu, 18 Aug 2022 19:11:37 +0200 Subject: [PATCH 01/14] add output option to migration tool --- bin/src/output.dart | 102 ++++++++++++++++++++++++++++++++++++++++++++ bin/stormberry.dart | 68 +++++++++++++++++++---------- pubspec.yaml | 1 + 3 files changed, 148 insertions(+), 23 deletions(-) create mode 100644 bin/src/output.dart diff --git a/bin/src/output.dart b/bin/src/output.dart new file mode 100644 index 0000000..66571a7 --- /dev/null +++ b/bin/src/output.dart @@ -0,0 +1,102 @@ +import 'dart:io'; + +import 'package:stormberry/stormberry.dart'; +import 'package:path/path.dart' as path; +import 'differentiator.dart'; +import 'schema.dart'; + +Future writeFile(Directory dir, String name, String content) { + var file = File(path.join(dir.path, '$name.sql')); + print('Writing file ${file.path}'); + return file.writeAsString(content); +} + +Future outputSchema(Directory dir, DatabaseSchemaDiff diff) async { + + for (var table in diff.tables.added) { + await writeFile(dir, 'create_${table.name}', """ + CREATE TABLE IF NOT EXISTS "${table.name}" ( + ${table.columns.values.map((c) => '"${c.name}" ${c.type} ${c.isNullable ? 'NULL' : 'NOT NULL'}').join(",")} + ) + """); + } + + await patchViews(dir, diff); +} + +Future patchViews(Directory dir, DatabaseSchemaDiff diff) async { + var toDrop = {...diff.views.removed, ...diff.views.modified.prev}; + var toAdd = {...diff.views.added, ...diff.views.modified.newly}; + + String? nodePath(ViewNode node, [Set visited = const {}]) { + if (visited.contains(node)) return node.view.name; + for (var child in node.children) { + var s = nodePath(child, {...visited, node}); + if (s != null) { + return '${node.view.name} -> $s'; + } + } + return null; + } + + var currViewNodes = ViewSchema.buildGraph(diff.existingSchema.views.values.toSet()); + + Iterable getParents(ViewNode n) => [n, ...n.parents.expand(getParents)]; + var toDropNodes = currViewNodes.where((n) => toDrop.contains(n.view)).expand(getParents).toSet(); + var toDropGraph = toDropNodes.where((n) => n.parents.isEmpty).toSet(); + + while (toDropGraph.isNotEmpty) { + var node = toDropGraph.first; + toDropGraph.remove(node); + toDropNodes.remove(node); + + if (!toDrop.contains(node.view)) { + toAdd.add(node.view); + } + + await writeFile(dir, 'drop_${node.view.name}', 'DROP VIEW ${node.view.name}'); + + for (var child in node.children) { + child.parents.remove(node); + if (child.parents.isEmpty) { + toDropGraph.add(child); + } + } + } + + if (toDropNodes.isNotEmpty) { + print('Error: Cyclic dependencies in dropped table views found: ${nodePath(toDropNodes.first)}'); + throw Exception(); + } + + await removeUnused(dir, diff); + + var toAddNodes = ViewSchema.buildGraph(toAdd); + var toAddGraph = toAddNodes.where((n) => n.children.isEmpty).toSet(); + + while (toAddGraph.isNotEmpty) { + var node = toAddGraph.first; + toAddGraph.remove(node); + toAddNodes.remove(node); + + await writeFile(dir, 'create_${node.view.name}', 'CREATE VIEW ${node.view.name} AS \n${node.view.definition}'); + + for (var parent in node.parents) { + parent.children.remove(node); + if (parent.children.isEmpty) { + toAddGraph.add(parent); + } + } + } + + if (toAddNodes.isNotEmpty) { + print('Error: Cyclic dependencies in added table views found: ${nodePath(toAddNodes.first)}'); + throw Exception(); + } +} + +Future removeUnused(Directory dir, DatabaseSchemaDiff diff) async { + for (var table in diff.tables.removed) { + await writeFile(dir, 'drop_${table.name}', 'DROP TABLE "${table.name}" CASCADE'); + } +} diff --git a/bin/stormberry.dart b/bin/stormberry.dart index 4c8b566..b10d1a9 100644 --- a/bin/stormberry.dart +++ b/bin/stormberry.dart @@ -7,12 +7,14 @@ import 'package:stormberry/stormberry.dart'; import 'package:yaml/yaml.dart'; import 'src/differentiator.dart'; +import 'src/output.dart'; import 'src/patcher.dart'; import 'src/schema.dart'; Future main(List args) async { bool dryRun = args.contains('--dry-run'); String? dbName = args.where((a) => a.startsWith('-db=')).map((a) => a.split('=')[1]).firstOrNull; + String? output = args.where((a) => a.startsWith('-o=')).map((a) => a.split('=')[1]).firstOrNull; bool applyChanges = args.contains('--apply-changes'); var pubspecYaml = File('pubspec.yaml'); @@ -94,38 +96,58 @@ Future main(List args) async { await db.close(); exit(1); } else { - await db.startTransaction(); + if (output == null) { + await db.startTransaction(); - String? answerApplyChanges; - if (!applyChanges) { - stdout.write('Do you want to apply these changes? (yes/no): '); - answerApplyChanges = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); - } + String? answerApplyChanges; + if (!applyChanges) { + stdout.write('Do you want to apply these changes? (yes/no): '); + answerApplyChanges = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); + } - if (applyChanges || answerApplyChanges == 'yes') { - print('Database schema changed, applying updates now:'); + if (applyChanges || answerApplyChanges == 'yes') { + print('Database schema changed, applying updates now:'); - try { - db.debugPrint = true; - await patchSchema(db, diff); - } catch (_) { + try { + db.debugPrint = true; + await patchSchema(db, diff); + } catch (_) { + db.cancelTransaction(); + } + } else { db.cancelTransaction(); } - } else { - db.cancelTransaction(); - } - var updateWasSuccessFull = await db.finishTransaction(); + var updateWasSuccessFull = await db.finishTransaction(); - print('========================'); - if (updateWasSuccessFull) { - print('---\nDATABASE UPDATE SUCCESSFUL'); + print('========================'); + if (updateWasSuccessFull) { + print('---\nDATABASE UPDATE SUCCESSFUL'); + } else { + print('---\nALL CHANGES REVERTED, EXITING'); + } + + await db.close(); + exit(updateWasSuccessFull ? 0 : 1); } else { - print('---\nALL CHANGES REVERTED, EXITING'); - } + await db.close(); + var dir = Directory(output); - await db.close(); - exit(updateWasSuccessFull ? 0 : 1); + String? answerApplyChanges; + if (!applyChanges) { + stdout.write('Do you want to write these migrations to ${dir.path}? (yes/no): '); + answerApplyChanges = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); + } + + if (applyChanges || answerApplyChanges == 'yes') { + + if (!dir.existsSync()) { + dir.createSync(recursive: true); + } + + await outputSchema(dir, diff); + } + } } } else { print('NO CHANGES, ALL DONE'); diff --git a/pubspec.yaml b/pubspec.yaml index 7ae8ff0..2bbaff0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: collection: ^1.16.0 crypto: ^3.0.1 dart_style: ^2.2.2 + path: ^1.8.2 postgres: ^2.4.3 source_gen: ^1.2.1 yaml: ^3.1.0 From 7d40a2c1311b47d39d88e73dde252ab02d105193 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Fri, 19 Aug 2022 19:00:21 +0200 Subject: [PATCH 02/14] remove legacy trigger code --- bin/src/differentiator.dart | 14 -------------- bin/src/inspector.dart | 25 +------------------------ bin/src/output.dart | 1 - bin/src/patcher.dart | 17 +---------------- bin/src/schema.dart | 3 --- 5 files changed, 2 insertions(+), 58 deletions(-) diff --git a/bin/src/differentiator.dart b/bin/src/differentiator.dart index 5de0f55..13de5ff 100644 --- a/bin/src/differentiator.dart +++ b/bin/src/differentiator.dart @@ -46,20 +46,6 @@ Future getSchemaDiff(Database db, DatabaseSchema dbSchema) a tableDiff.constraints.added.add(newConstraint); } - for (var extTrigger in extTable.triggers) { - var newTrigger = newTable.triggers.where((t) => t == extTrigger).firstOrNull; - - if (newTrigger != null) { - newTable.triggers.remove(newTrigger); - } else { - tableDiff.triggers.removed.add(extTrigger); - } - } - - for (var newTrigger in newTable.triggers) { - tableDiff.triggers.added.add(newTrigger); - } - for (var extIndex in extTable.indexes) { var newIndex = newTable.indexes.where((t) => t == extIndex).firstOrNull; diff --git a/bin/src/inspector.dart b/bin/src/inspector.dart index c9dc31e..8f699ce 100644 --- a/bin/src/inspector.dart +++ b/bin/src/inspector.dart @@ -12,7 +12,7 @@ Future inspectDatabaseSchema(Database db) async { var tableMap = row.toColumnMap(); var tableName = tableMap['table_name'] as String; - var tableScheme = TableSchema(tableName, columns: {}, constraints: [], triggers: [], indexes: []); + var tableScheme = TableSchema(tableName, columns: {}, constraints: [], indexes: []); schema.tables[tableName] = tableScheme; var columns = await db.query("SELECT * FROM information_schema.columns WHERE table_name = '$tableName'"); @@ -78,29 +78,6 @@ Future inspectDatabaseSchema(Database db) async { } } - var triggers = await db.query(""" - SELECT t.trigger_name, t.event_object_table, t.action_statement, t.action_orientation, t.action_timing, - array_to_json(ARRAY_AGG(t.event_manipulation)) as events, - array_to_json(ARRAY_AGG(tuc.event_object_column)) as columns - FROM information_schema.triggers t - LEFT JOIN information_schema.triggered_update_columns tuc - ON t.trigger_name = tuc.trigger_name AND t.event_manipulation = 'UPDATE' - GROUP BY t.trigger_name, t.event_object_table, t.action_statement, t.action_orientation, t.action_timing - """); - for (var row in triggers) { - var triggerMap = row.toColumnMap(); - var tableName = triggerMap['event_object_table'] as String; - var statement = triggerMap['action_statement'] as String; - var match = RegExp(r'^EXECUTE FUNCTION ([\w_]+)\((.*)\)$').firstMatch(statement)!; - - schema.tables[tableName]?.triggers.add(TableTrigger( - triggerMap['trigger_name'] as String, - triggerMap['columns'].firstWhere((c) => c != null) as String, - match.group(1)!, - match.group(2)!.split(', ').map((a) => a.substring(1, a.length - 1)).toList(), - )); - } - var indexes = await db.query(r""" SELECT * FROM pg_catalog.pg_indexes WHERE schemaname = 'public' AND indexname LIKE '\_\_%' """); diff --git a/bin/src/output.dart b/bin/src/output.dart index 66571a7..3b531bd 100644 --- a/bin/src/output.dart +++ b/bin/src/output.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:stormberry/stormberry.dart'; import 'package:path/path.dart' as path; import 'differentiator.dart'; import 'schema.dart'; diff --git a/bin/src/patcher.dart b/bin/src/patcher.dart index 9dd9bb0..17f5ae9 100644 --- a/bin/src/patcher.dart +++ b/bin/src/patcher.dart @@ -39,7 +39,7 @@ Future patchSchema(Database db, DatabaseSchemaDiff diff) async { }), ...table.columns.modified.expand((c) sync* { if (c.prev.type != 'serial' && c.newly.type == 'serial') { - yield 'ALTER COLUMN \"${c.prev.name}\" SET DATA TYPE int8 USING ${c.newly.name}::int8'; + yield 'ALTER COLUMN "${c.prev.name}" SET DATA TYPE int8 USING ${c.newly.name}::int8'; yield "ALTER COLUMN \"${c.prev.name}\" SET DEFAULT nextval('${table.name}_${c.newly.name}_seq')"; } else { var update = c.prev.type != c.newly.type @@ -122,15 +122,6 @@ Future patchSchema(Database db, DatabaseSchemaDiff diff) async { } for (var table in diff.tables.added) { - for (var trigger in table.triggers) { - await db.query(""" - CREATE TRIGGER "${trigger.name}" - AFTER DELETE OR UPDATE OF "${trigger.column}" ON "${table.name}" - FOR EACH ROW - EXECUTE FUNCTION ${trigger.function}(${trigger.args.map((a) => "'$a'").join(", ")}); - """); - } - for (var index in table.indexes) { await db.query('CREATE ${index.statement(table.name)}'); } @@ -221,12 +212,6 @@ Future removeUnused(Database db, DatabaseSchemaDiff diff) async { } for (var table in diff.tables.removed) { - for (var trigger in table.triggers) { - await db.query(''' - DROP TRIGGER "${trigger.name}" ON "${table.name}" - '''); - } - for (var index in table.indexes) { await db.query('DROP INDEX "__${index.name}"'); } diff --git a/bin/src/schema.dart b/bin/src/schema.dart index 9fcfbd2..df66534 100644 --- a/bin/src/schema.dart +++ b/bin/src/schema.dart @@ -70,14 +70,12 @@ class TableSchema { final String name; final Map columns; final List constraints; - final List triggers; final List indexes; const TableSchema( this.name, { this.columns = const {}, this.constraints = const [], - this.triggers = const [], this.indexes = const [], }); @@ -85,7 +83,6 @@ class TableSchema { name, columns: {...columns}, constraints: [...constraints], - triggers: [...triggers], indexes: [...indexes], ); } From 36fb7fb8c1981c202799940e63f852901d440df3 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Fri, 19 Aug 2022 20:09:13 +0200 Subject: [PATCH 03/14] add support for target annotations on views --- .../builder/generators/view_generator.dart | 1 + lib/src/builder/view_builder.dart | 55 ++++ lib/src/core/annotations.dart | 3 +- test/multi_schema_test.dart | 8 +- test/{ => packages}/multi_schema/build.yaml | 0 .../{ => packages}/multi_schema/lib/main.dart | 0 .../multi_schema/lib/modelsA.dart | 0 .../multi_schema/lib/modelsA.schema.g.dart | 0 .../multi_schema/lib/modelsB.dart | 0 .../multi_schema/lib/modelsB.schema.g.dart | 0 test/{ => packages}/multi_schema/pubspec.yaml | 2 +- test/packages/serialization/build.yaml | 13 + test/packages/serialization/lib/main.dart | 12 + test/packages/serialization/lib/models.dart | 28 ++ .../serialization/lib/models.mapper.g.dart | 198 ++++++++++++++ .../serialization/lib/models.schema.g.dart | 257 ++++++++++++++++++ test/packages/serialization/pubspec.yaml | 14 + test/serialization_test.dart | 42 +++ 18 files changed, 627 insertions(+), 6 deletions(-) rename test/{ => packages}/multi_schema/build.yaml (100%) rename test/{ => packages}/multi_schema/lib/main.dart (100%) rename test/{ => packages}/multi_schema/lib/modelsA.dart (100%) rename test/{ => packages}/multi_schema/lib/modelsA.schema.g.dart (100%) rename test/{ => packages}/multi_schema/lib/modelsB.dart (100%) rename test/{ => packages}/multi_schema/lib/modelsB.schema.g.dart (100%) rename test/{ => packages}/multi_schema/pubspec.yaml (88%) create mode 100644 test/packages/serialization/build.yaml create mode 100644 test/packages/serialization/lib/main.dart create mode 100644 test/packages/serialization/lib/models.dart create mode 100644 test/packages/serialization/lib/models.mapper.g.dart create mode 100644 test/packages/serialization/lib/models.schema.g.dart create mode 100644 test/packages/serialization/pubspec.yaml create mode 100644 test/serialization_test.dart diff --git a/lib/src/builder/generators/view_generator.dart b/lib/src/builder/generators/view_generator.dart index 894d63e..f304dc4 100644 --- a/lib/src/builder/generators/view_generator.dart +++ b/lib/src/builder/generators/view_generator.dart @@ -66,6 +66,7 @@ class ViewGenerator { ${view.entityName} decode(TypedMap map) => ${view.className}(${view.columns.map((c) => '${c.paramName}: ${_getInitializer(c)}').join(',')}); } + ${view.targetAnnotation ?? ''} class ${view.className}${implementsBase ? ' implements ${view.table.element.name}' : ''} { ${view.className}(${view.columns.isEmpty ? '' : '{${view.columns.map((c) => '${c.isNullable ? '' : 'required '}this.${c.paramName}').join(', ')}}'}); diff --git a/lib/src/builder/view_builder.dart b/lib/src/builder/view_builder.dart index 49fddec..dfde37d 100644 --- a/lib/src/builder/view_builder.dart +++ b/lib/src/builder/view_builder.dart @@ -1,6 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:source_gen/source_gen.dart'; import '../../internals.dart'; import '../core/case_style.dart'; @@ -179,4 +180,58 @@ class ViewBuilder { return columns; } + + String? get targetAnnotation { + if (annotation != null && !annotation!.getField('annotation')!.isNull) { + return '@' + annotation!.getField('annotation')!.toSource(); + } + return null; + } +} + +extension ObjectSource on DartObject { + String toSource() { + var reader = ConstantReader(this); + + if (reader.isLiteral) { + if (reader.isString) { + return "'${reader.literalValue}'"; + } + return reader.literalValue!.toString(); + } + + var rev = reader.revive(); + + var str = ''; + if (rev.source.fragment.isNotEmpty) { + str = rev.source.fragment; + + if (rev.accessor.isNotEmpty) { + str += '.${rev.accessor}'; + } + str += '('; + var isFirst = true; + + for (var p in rev.positionalArguments) { + if (!isFirst) { + str += ', '; + } + isFirst = false; + str += p.toSource(); + } + + for (var p in rev.namedArguments.entries) { + if (!isFirst) { + str += ', '; + } + isFirst = false; + str += '${p.key}: ${p.value.toSource()}'; + } + + str += ')'; + } else { + str = rev.accessor; + } + return str; + } } diff --git a/lib/src/core/annotations.dart b/lib/src/core/annotations.dart index 50f03f1..59daccc 100644 --- a/lib/src/core/annotations.dart +++ b/lib/src/core/annotations.dart @@ -14,7 +14,8 @@ class Model { class View { final String name; final List fields; - const View([this.name = '', this.fields = const []]); + final dynamic annotation; + const View([this.name = '', this.fields = const [], this.annotation]); } /// Used to define fields of [View]s diff --git a/test/multi_schema_test.dart b/test/multi_schema_test.dart index e4988cf..e04f4d4 100644 --- a/test/multi_schema_test.dart +++ b/test/multi_schema_test.dart @@ -8,15 +8,15 @@ void main() { var proc = await Process.start( 'dart', 'run build_runner build --delete-conflicting-outputs'.split(' '), - workingDirectory: 'test/multi_schema', + workingDirectory: 'test/packages/multi_schema', ); proc.stdout.listen((e) => stdout.add(e)); expect(await proc.exitCode, equals(0)); - var schemaA = File('test/multi_schema/lib/modelsA.schema.g.dart'); - var schemaB = File('test/multi_schema/lib/modelsB.schema.g.dart'); + var schemaA = File('test/packages/multi_schema/lib/modelsA.schema.g.dart'); + var schemaB = File('test/packages/multi_schema/lib/modelsB.schema.g.dart'); expect(schemaA.existsSync(), equals(true)); expect(schemaB.existsSync(), equals(true)); @@ -26,7 +26,7 @@ void main() { var proc = await Process.start( 'dart', 'run stormberry --apply-changes'.split(' '), - workingDirectory: 'test/multi_schema', + workingDirectory: 'test/packages/multi_schema', environment: { 'DB_HOST': 'localhost', 'DB_PORT': '2222', diff --git a/test/multi_schema/build.yaml b/test/packages/multi_schema/build.yaml similarity index 100% rename from test/multi_schema/build.yaml rename to test/packages/multi_schema/build.yaml diff --git a/test/multi_schema/lib/main.dart b/test/packages/multi_schema/lib/main.dart similarity index 100% rename from test/multi_schema/lib/main.dart rename to test/packages/multi_schema/lib/main.dart diff --git a/test/multi_schema/lib/modelsA.dart b/test/packages/multi_schema/lib/modelsA.dart similarity index 100% rename from test/multi_schema/lib/modelsA.dart rename to test/packages/multi_schema/lib/modelsA.dart diff --git a/test/multi_schema/lib/modelsA.schema.g.dart b/test/packages/multi_schema/lib/modelsA.schema.g.dart similarity index 100% rename from test/multi_schema/lib/modelsA.schema.g.dart rename to test/packages/multi_schema/lib/modelsA.schema.g.dart diff --git a/test/multi_schema/lib/modelsB.dart b/test/packages/multi_schema/lib/modelsB.dart similarity index 100% rename from test/multi_schema/lib/modelsB.dart rename to test/packages/multi_schema/lib/modelsB.dart diff --git a/test/multi_schema/lib/modelsB.schema.g.dart b/test/packages/multi_schema/lib/modelsB.schema.g.dart similarity index 100% rename from test/multi_schema/lib/modelsB.schema.g.dart rename to test/packages/multi_schema/lib/modelsB.schema.g.dart diff --git a/test/multi_schema/pubspec.yaml b/test/packages/multi_schema/pubspec.yaml similarity index 88% rename from test/multi_schema/pubspec.yaml rename to test/packages/multi_schema/pubspec.yaml index 474baa7..7529fcc 100644 --- a/test/multi_schema/pubspec.yaml +++ b/test/packages/multi_schema/pubspec.yaml @@ -5,7 +5,7 @@ environment: dependencies: stormberry: - path: ../../ + path: ../../../ dev_dependencies: build_runner: ^2.1.7 diff --git a/test/packages/serialization/build.yaml b/test/packages/serialization/build.yaml new file mode 100644 index 0000000..b072068 --- /dev/null +++ b/test/packages/serialization/build.yaml @@ -0,0 +1,13 @@ +targets: + $default: + builders: + stormberry: + generate_for: + - lib/models.dart + serialization: + dependencies: + - serialization_test + builders: + dart_mappable_builder: + generate_for: + - lib/models.dart \ No newline at end of file diff --git a/test/packages/serialization/lib/main.dart b/test/packages/serialization/lib/main.dart new file mode 100644 index 0000000..97d4950 --- /dev/null +++ b/test/packages/serialization/lib/main.dart @@ -0,0 +1,12 @@ +import 'models.dart'; + +Future main() async { + var user = DefaultUserView(id: 'abc', name: 'Tom', securityNumber: '12345'); + + print(user.toJson()); + print(Mapper.asString(user.copyWith(name: 'Alex'))); + + var company = DefaultCompanyView(id: '01', member: PublicUserView(id: 'def', name: 'Susan')); + + print(company.toJson()); +} diff --git a/test/packages/serialization/lib/models.dart b/test/packages/serialization/lib/models.dart new file mode 100644 index 0000000..9631fc1 --- /dev/null +++ b/test/packages/serialization/lib/models.dart @@ -0,0 +1,28 @@ +import 'package:dart_mappable/dart_mappable.dart'; +import 'package:stormberry/stormberry.dart'; + +export 'package:dart_mappable/dart_mappable.dart'; +export 'models.schema.g.dart'; +export 'models.mapper.g.dart'; + +@Model(views: [ + View('Default', [], MappableClass()), + View('Public', [Field.hidden('securityNumber')], MappableClass()), +]) +abstract class User { + @PrimaryKey() + String get id; + + String get name; + String get securityNumber; +} + +@Model(views: [ + View('Default', [Field.view('member', as: 'Public')], MappableClass()), +]) +abstract class Company { + @PrimaryKey() + String get id; + + User get member; +} diff --git a/test/packages/serialization/lib/models.mapper.g.dart b/test/packages/serialization/lib/models.mapper.g.dart new file mode 100644 index 0000000..77301f2 --- /dev/null +++ b/test/packages/serialization/lib/models.mapper.g.dart @@ -0,0 +1,198 @@ +import 'package:dart_mappable/dart_mappable.dart'; +import 'package:dart_mappable/internals.dart'; + +import 'models.schema.g.dart'; + + +// === ALL STATICALLY REGISTERED MAPPERS === + +var _mappers = { + // class mappers + DefaultUserViewMapper._(), + PublicUserViewMapper._(), + DefaultCompanyViewMapper._(), + // enum mappers + // custom mappers +}; + + +// === GENERATED CLASS MAPPERS AND EXTENSIONS === + +class DefaultUserViewMapper extends BaseMapper { + DefaultUserViewMapper._(); + + @override Function get decoder => decode; + DefaultUserView decode(dynamic v) => checked(v, (Map map) => fromMap(map)); + DefaultUserView fromMap(Map map) => DefaultUserView(id: Mapper.i.$get(map, 'id'), name: Mapper.i.$get(map, 'name'), securityNumber: Mapper.i.$get(map, 'securityNumber')); + + @override Function get encoder => (DefaultUserView v) => encode(v); + dynamic encode(DefaultUserView v) => toMap(v); + Map toMap(DefaultUserView d) => {'id': Mapper.i.$enc(d.id, 'id'), 'name': Mapper.i.$enc(d.name, 'name'), 'securityNumber': Mapper.i.$enc(d.securityNumber, 'securityNumber')}; + + @override String stringify(DefaultUserView self) => 'DefaultUserView(id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)}, securityNumber: ${Mapper.asString(self.securityNumber)})'; + @override int hash(DefaultUserView self) => Mapper.hash(self.id) ^ Mapper.hash(self.name) ^ Mapper.hash(self.securityNumber); + @override bool equals(DefaultUserView self, DefaultUserView other) => Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name) && Mapper.isEqual(self.securityNumber, other.securityNumber); + + @override Function get typeFactory => (f) => f(); +} + +extension DefaultUserViewMapperExtension on DefaultUserView { + String toJson() => Mapper.toJson(this); + Map toMap() => Mapper.toMap(this); + DefaultUserViewCopyWith get copyWith => DefaultUserViewCopyWith(this, $identity); +} + +abstract class DefaultUserViewCopyWith<$R> { + factory DefaultUserViewCopyWith(DefaultUserView value, Then then) = _DefaultUserViewCopyWithImpl<$R>; + $R call({String? id, String? name, String? securityNumber}); + $R apply(DefaultUserView Function(DefaultUserView) transform); +} + +class _DefaultUserViewCopyWithImpl<$R> extends BaseCopyWith implements DefaultUserViewCopyWith<$R> { + _DefaultUserViewCopyWithImpl(DefaultUserView value, Then then) : super(value, then); + + @override $R call({String? id, String? name, String? securityNumber}) => $then(DefaultUserView(id: id ?? $value.id, name: name ?? $value.name, securityNumber: securityNumber ?? $value.securityNumber)); +} + +class PublicUserViewMapper extends BaseMapper { + PublicUserViewMapper._(); + + @override Function get decoder => decode; + PublicUserView decode(dynamic v) => checked(v, (Map map) => fromMap(map)); + PublicUserView fromMap(Map map) => PublicUserView(id: Mapper.i.$get(map, 'id'), name: Mapper.i.$get(map, 'name')); + + @override Function get encoder => (PublicUserView v) => encode(v); + dynamic encode(PublicUserView v) => toMap(v); + Map toMap(PublicUserView p) => {'id': Mapper.i.$enc(p.id, 'id'), 'name': Mapper.i.$enc(p.name, 'name')}; + + @override String stringify(PublicUserView self) => 'PublicUserView(id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)})'; + @override int hash(PublicUserView self) => Mapper.hash(self.id) ^ Mapper.hash(self.name); + @override bool equals(PublicUserView self, PublicUserView other) => Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name); + + @override Function get typeFactory => (f) => f(); +} + +extension PublicUserViewMapperExtension on PublicUserView { + String toJson() => Mapper.toJson(this); + Map toMap() => Mapper.toMap(this); + PublicUserViewCopyWith get copyWith => PublicUserViewCopyWith(this, $identity); +} + +abstract class PublicUserViewCopyWith<$R> { + factory PublicUserViewCopyWith(PublicUserView value, Then then) = _PublicUserViewCopyWithImpl<$R>; + $R call({String? id, String? name}); + $R apply(PublicUserView Function(PublicUserView) transform); +} + +class _PublicUserViewCopyWithImpl<$R> extends BaseCopyWith implements PublicUserViewCopyWith<$R> { + _PublicUserViewCopyWithImpl(PublicUserView value, Then then) : super(value, then); + + @override $R call({String? id, String? name}) => $then(PublicUserView(id: id ?? $value.id, name: name ?? $value.name)); +} + +class DefaultCompanyViewMapper extends BaseMapper { + DefaultCompanyViewMapper._(); + + @override Function get decoder => decode; + DefaultCompanyView decode(dynamic v) => checked(v, (Map map) => fromMap(map)); + DefaultCompanyView fromMap(Map map) => DefaultCompanyView(id: Mapper.i.$get(map, 'id'), member: Mapper.i.$get(map, 'member')); + + @override Function get encoder => (DefaultCompanyView v) => encode(v); + dynamic encode(DefaultCompanyView v) => toMap(v); + Map toMap(DefaultCompanyView d) => {'id': Mapper.i.$enc(d.id, 'id'), 'member': Mapper.i.$enc(d.member, 'member')}; + + @override String stringify(DefaultCompanyView self) => 'DefaultCompanyView(id: ${Mapper.asString(self.id)}, member: ${Mapper.asString(self.member)})'; + @override int hash(DefaultCompanyView self) => Mapper.hash(self.id) ^ Mapper.hash(self.member); + @override bool equals(DefaultCompanyView self, DefaultCompanyView other) => Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.member, other.member); + + @override Function get typeFactory => (f) => f(); +} + +extension DefaultCompanyViewMapperExtension on DefaultCompanyView { + String toJson() => Mapper.toJson(this); + Map toMap() => Mapper.toMap(this); + DefaultCompanyViewCopyWith get copyWith => DefaultCompanyViewCopyWith(this, $identity); +} + +abstract class DefaultCompanyViewCopyWith<$R> { + factory DefaultCompanyViewCopyWith(DefaultCompanyView value, Then then) = _DefaultCompanyViewCopyWithImpl<$R>; + PublicUserViewCopyWith<$R> get member; + $R call({String? id, PublicUserView? member}); + $R apply(DefaultCompanyView Function(DefaultCompanyView) transform); +} + +class _DefaultCompanyViewCopyWithImpl<$R> extends BaseCopyWith implements DefaultCompanyViewCopyWith<$R> { + _DefaultCompanyViewCopyWithImpl(DefaultCompanyView value, Then then) : super(value, then); + + @override PublicUserViewCopyWith<$R> get member => PublicUserViewCopyWith($value.member, (v) => call(member: v)); + @override $R call({String? id, PublicUserView? member}) => $then(DefaultCompanyView(id: id ?? $value.id, member: member ?? $value.member)); +} + + +// === GENERATED ENUM MAPPERS AND EXTENSIONS === + + + + +// === GENERATED UTILITY CODE === + +class Mapper { + Mapper._(); + + static MapperContainer i = MapperContainer(_mappers); + + static T fromValue(dynamic value) => i.fromValue(value); + static T fromMap(Map map) => i.fromMap(map); + static T fromIterable(Iterable iterable) => i.fromIterable(iterable); + static T fromJson(String json) => i.fromJson(json); + + static dynamic toValue(dynamic value) => i.toValue(value); + static Map toMap(dynamic object) => i.toMap(object); + static Iterable toIterable(dynamic object) => i.toIterable(object); + static String toJson(dynamic object) => i.toJson(object); + + static bool isEqual(dynamic value, Object? other) => i.isEqual(value, other); + static int hash(dynamic value) => i.hash(value); + static String asString(dynamic value) => i.asString(value); + + static void use(BaseMapper mapper) => i.use(mapper); + static BaseMapper? unuse() => i.unuse(); + static void useAll(List mappers) => i.useAll(mappers); + + static BaseMapper? get([Type? type]) => i.get(type); + static List getAll() => i.getAll(); +} + +mixin Mappable implements MappableMixin { + String toJson() => Mapper.toJson(this); + Map toMap() => Mapper.toMap(this); + + @override + String toString() { + return _guard(() => Mapper.asString(this), super.toString); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (runtimeType == other.runtimeType && + _guard(() => Mapper.isEqual(this, other), () => super == other)); + } + + @override + int get hashCode { + return _guard(() => Mapper.hash(this), () => super.hashCode); + } + + T _guard(T Function() fn, T Function() fallback) { + try { + return fn(); + } on MapperException catch (e) { + if (e.isUnsupportedOrUnallowed()) { + return fallback(); + } else { + rethrow; + } + } + } +} diff --git a/test/packages/serialization/lib/models.schema.g.dart b/test/packages/serialization/lib/models.schema.g.dart new file mode 100644 index 0000000..9c4bbea --- /dev/null +++ b/test/packages/serialization/lib/models.schema.g.dart @@ -0,0 +1,257 @@ +// ignore_for_file: prefer_relative_imports +import 'package:stormberry/internals.dart'; +import 'package:serialization_test/models.dart'; + +extension Repositories on Database { + UserRepository get users => UserRepository._(this); + CompanyRepository get companies => CompanyRepository._(this); +} + +final registry = ModelRegistry({}); + +abstract class UserRepository + implements + ModelRepository, + ModelRepositoryInsert, + ModelRepositoryUpdate, + ModelRepositoryDelete { + factory UserRepository._(Database db) = _UserRepository; + + Future queryDefaultView(String id); + Future> queryDefaultViews([QueryParams? params]); + Future queryPublicView(String id); + Future> queryPublicViews([QueryParams? params]); +} + +class _UserRepository extends BaseRepository + with + RepositoryInsertMixin, + RepositoryUpdateMixin, + RepositoryDeleteMixin + implements UserRepository { + _UserRepository(Database db) : super(db: db); + + @override + Future queryDefaultView(String id) { + return queryOne(id, DefaultUserViewQueryable()); + } + + @override + Future> queryDefaultViews([QueryParams? params]) { + return queryMany(DefaultUserViewQueryable(), params); + } + + @override + Future queryPublicView(String id) { + return queryOne(id, PublicUserViewQueryable()); + } + + @override + Future> queryPublicViews([QueryParams? params]) { + return queryMany(PublicUserViewQueryable(), params); + } + + @override + Future insert(Database db, List requests) async { + if (requests.isEmpty) return; + + await db.query(""" + INSERT INTO "users" ( "company_id", "id", "name", "security_number" ) + VALUES ${requests.map((r) => '( ${registry.encode(r.companyId)}, ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.securityNumber)} )').join(', ')} +ON CONFLICT ( "id" ) DO UPDATE SET "company_id" = EXCLUDED."company_id", "name" = EXCLUDED."name", "security_number" = EXCLUDED."security_number" + """); + } + + @override + Future update(Database db, List requests) async { + if (requests.isEmpty) return; + await db.query(""" + UPDATE "users" + SET "company_id" = COALESCE(UPDATED."company_id"::text, "users"."company_id"), "name" = COALESCE(UPDATED."name"::text, "users"."name"), "security_number" = COALESCE(UPDATED."security_number"::text, "users"."security_number") + FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.companyId)}, ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.securityNumber)} )').join(', ')} ) + AS UPDATED("company_id", "id", "name", "security_number") + WHERE "users"."id" = UPDATED."id" + """); + } + + @override + Future delete(Database db, List keys) async { + if (keys.isEmpty) return; + await db.query(""" + DELETE FROM "users" + WHERE "users"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} ) + """); + } +} + +abstract class CompanyRepository + implements + ModelRepository, + ModelRepositoryInsert, + ModelRepositoryUpdate, + ModelRepositoryDelete { + factory CompanyRepository._(Database db) = _CompanyRepository; + + Future queryDefaultView(String id); + Future> queryDefaultViews([QueryParams? params]); +} + +class _CompanyRepository extends BaseRepository + with + RepositoryInsertMixin, + RepositoryUpdateMixin, + RepositoryDeleteMixin + implements CompanyRepository { + _CompanyRepository(Database db) : super(db: db); + + @override + Future queryDefaultView(String id) { + return queryOne(id, DefaultCompanyViewQueryable()); + } + + @override + Future> queryDefaultViews([QueryParams? params]) { + return queryMany(DefaultCompanyViewQueryable(), params); + } + + @override + Future insert(Database db, List requests) async { + if (requests.isEmpty) return; + + await db.query(""" + INSERT INTO "companies" ( "id", "member_id" ) + VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.memberId)} )').join(', ')} +ON CONFLICT ( "id" ) DO UPDATE SET "member_id" = EXCLUDED."member_id" + """); + } + + @override + Future update(Database db, List requests) async { + if (requests.isEmpty) return; + await db.query(""" + UPDATE "companies" + SET "member_id" = COALESCE(UPDATED."member_id"::text, "companies"."member_id") + FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.memberId)} )').join(', ')} ) + AS UPDATED("id", "member_id") + WHERE "companies"."id" = UPDATED."id" + """); + } + + @override + Future delete(Database db, List keys) async { + if (keys.isEmpty) return; + await db.query(""" + DELETE FROM "companies" + WHERE "companies"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} ) + """); + } +} + +class UserInsertRequest { + UserInsertRequest({this.companyId, required this.id, required this.name, required this.securityNumber}); + String? companyId; + String id; + String name; + String securityNumber; +} + +class CompanyInsertRequest { + CompanyInsertRequest({required this.id, required this.memberId}); + String id; + String memberId; +} + +class UserUpdateRequest { + UserUpdateRequest({this.companyId, required this.id, this.name, this.securityNumber}); + String? companyId; + String id; + String? name; + String? securityNumber; +} + +class CompanyUpdateRequest { + CompanyUpdateRequest({required this.id, required this.memberId}); + String id; + String memberId; +} + +class DefaultUserViewQueryable extends KeyedViewQueryable { + @override + String get keyName => 'id'; + + @override + String encodeKey(String key) => registry.encode(key); + + @override + String get tableName => 'default_users_view'; + + @override + String get tableAlias => 'users'; + + @override + DefaultUserView decode(TypedMap map) => DefaultUserView( + id: map.get('id', registry.decode), + name: map.get('name', registry.decode), + securityNumber: map.get('security_number', registry.decode)); +} + +@MappableClass() +class DefaultUserView { + DefaultUserView({required this.id, required this.name, required this.securityNumber}); + + final String id; + final String name; + final String securityNumber; +} + +class PublicUserViewQueryable extends KeyedViewQueryable { + @override + String get keyName => 'id'; + + @override + String encodeKey(String key) => registry.encode(key); + + @override + String get tableName => 'public_users_view'; + + @override + String get tableAlias => 'users'; + + @override + PublicUserView decode(TypedMap map) => + PublicUserView(id: map.get('id', registry.decode), name: map.get('name', registry.decode)); +} + +@MappableClass() +class PublicUserView { + PublicUserView({required this.id, required this.name}); + + final String id; + final String name; +} + +class DefaultCompanyViewQueryable extends KeyedViewQueryable { + @override + String get keyName => 'id'; + + @override + String encodeKey(String key) => registry.encode(key); + + @override + String get tableName => 'default_companies_view'; + + @override + String get tableAlias => 'companies'; + + @override + DefaultCompanyView decode(TypedMap map) => DefaultCompanyView( + id: map.get('id', registry.decode), member: map.get('member', PublicUserViewQueryable().decoder)); +} + +@MappableClass() +class DefaultCompanyView { + DefaultCompanyView({required this.id, required this.member}); + + final String id; + final PublicUserView member; +} diff --git a/test/packages/serialization/pubspec.yaml b/test/packages/serialization/pubspec.yaml new file mode 100644 index 0000000..7164d42 --- /dev/null +++ b/test/packages/serialization/pubspec.yaml @@ -0,0 +1,14 @@ +name: serialization_test + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + stormberry: + path: ../../../ + dart_mappable: 1.2.0 + +dev_dependencies: + build_runner: ^2.1.7 + dart_mappable_builder: 1.2.0 + lints: ^1.0.1 \ No newline at end of file diff --git a/test/serialization_test.dart b/test/serialization_test.dart new file mode 100644 index 0000000..805f3ed --- /dev/null +++ b/test/serialization_test.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + group('serialization', () { + test('generates outputs', () async { + var proc = await Process.start( + 'dart', + 'run build_runner build --delete-conflicting-outputs'.split(' '), + workingDirectory: 'test/packages/serialization', + ); + + expect(await proc.exitCode, equals(0)); + + var schema = File('test/packages/serialization/lib/models.schema.g.dart'); + var mapper = File('test/packages/serialization/lib/models.mapper.g.dart'); + + expect(schema.existsSync(), equals(true)); + expect(mapper.existsSync(), equals(true)); + }, timeout: Timeout(Duration(seconds: 60))); + + test('serialize models', () async { + + var proc = await Process.start( + 'dart', + 'run lib/main.dart'.split(' '), + workingDirectory: 'test/packages/serialization', + ); + + var output = await proc.stdout.map((e) => utf8.decode(e)).fold('', (s, e) => s + e); + + var lines = output.split('\n'); + + expect(lines[0], equals('{"id":"abc","name":"Tom","securityNumber":"12345"}')); + expect(lines[1], equals('DefaultUserView(id: abc, name: Alex, securityNumber: 12345)')); + expect(lines[2], equals('{"id":"01","member":{"id":"def","name":"Susan"}}')); + + }, timeout: Timeout(Duration(seconds: 60))); + }); +} From 2d1c1e57e1c2ff8a11b8867351aa51a46ea5b014 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Sat, 20 Aug 2022 14:58:08 +0200 Subject: [PATCH 04/14] improve migrations output --- bin/src/differentiator.dart | 13 +- bin/src/output.dart | 192 +++++++++++++++++++++---- bin/src/patcher.dart | 61 +------- bin/src/schema.dart | 24 ++-- example/migrations/0_create_tables.sql | 42 ++++++ example/migrations/1_alter_tables.sql | 35 +++++ example/migrations/2_create_views.sql | 134 +++++++++++++++++ lib/src/core/annotations.dart | 12 +- lib/src/helpers/json_schema.dart | 4 +- 9 files changed, 391 insertions(+), 126 deletions(-) create mode 100644 example/migrations/0_create_tables.sql create mode 100644 example/migrations/1_alter_tables.sql create mode 100644 example/migrations/2_create_views.sql diff --git a/bin/src/differentiator.dart b/bin/src/differentiator.dart index 13de5ff..2e88504 100644 --- a/bin/src/differentiator.dart +++ b/bin/src/differentiator.dart @@ -120,16 +120,6 @@ void printDiff(DatabaseSchemaDiff diff) { print("-- CONSTRAINT ON ${table.name} ${constr.toString().replaceAll(RegExp("[\n\\s]+"), " ")}"); } - for (var trigger in table.triggers.added) { - print('++ TRIGGER ${trigger.name} ON ${table.name}.${trigger.column} ' - "EXECUTE ${trigger.function}(${trigger.args.join(", ")})"); - } - - for (var trigger in table.triggers.removed) { - print('-- TRIGGER ${trigger.name} ON ${table.name}.${trigger.column} ' - "EXECUTE ${trigger.function}(${trigger.args.join(", ")})"); - } - for (var index in table.indexes.added) { print("++ ${index.statement(table.name).replaceAll(RegExp("[\n\\s]+"), " ")}"); } @@ -168,13 +158,12 @@ class TableSchemaDiff { String name; Diff> columns = Diff(); Diff constraints = Diff(); - Diff triggers = Diff(); Diff indexes = Diff(); TableSchemaDiff(this.name); bool get hasChanges => - columns.hasChanges() || constraints.hasChanges() || triggers.hasChanges() || indexes.hasChanges(); + columns.hasChanges() || constraints.hasChanges() || indexes.hasChanges(); } class Diff { diff --git a/bin/src/output.dart b/bin/src/output.dart index 3b531bd..a6a0e61 100644 --- a/bin/src/output.dart +++ b/bin/src/output.dart @@ -4,20 +4,127 @@ import 'package:path/path.dart' as path; import 'differentiator.dart'; import 'schema.dart'; +var fileIndex = 0; + Future writeFile(Directory dir, String name, String content) { - var file = File(path.join(dir.path, '$name.sql')); + var file = File(path.join(dir.path, '${fileIndex++}_$name.sql')); print('Writing file ${file.path}'); return file.writeAsString(content); } Future outputSchema(Directory dir, DatabaseSchemaDiff diff) async { + if (diff.tables.added.isNotEmpty) { + var createTables = ''; + + for (var table in diff.tables.added) { + if (createTables.isNotEmpty) { + createTables += '\n\n'; + } + createTables += '' + 'CREATE TABLE IF NOT EXISTS "${table.name}" (\n' + '${table.columns.values.map((c) => ' "${c.name}" ${c.type} ${c.isNullable ? 'NULL' : 'NOT NULL'}').join(",\n")}\n' + ');'; + } + + await writeFile(dir, 'create_tables', createTables); + } + + var alterTables = ''; + void appendStatement(String statement) { + if (alterTables.isNotEmpty) { + alterTables += '\n\n'; + } + alterTables += statement; + } + + for (var table in diff.tables.modified) { + for (var index in table.indexes.removed) { + appendStatement('DROP INDEX "__${index.name}";'); + } + + if (table.constraints.removed.isNotEmpty) { + appendStatement('' + 'ALTER TABLE "${table.name}"\n' + ' ${table.constraints.removed.map((c) => 'DROP CONSTRAINT IF EXISTS "${c.name}" CASCADE').join(",\n ")};'); + } + } + + for (var table in diff.tables.modified) { + if (table.columns.added.isNotEmpty || table.columns.modified.isNotEmpty) { + var updatedColumns = [ + ...table.columns.added.map((c) { + return 'ADD COLUMN "${c.name}" ${c.type} ${c.isNullable ? 'NULL' : 'NOT NULL'}'; + }), + ...table.columns.modified.expand((c) sync* { + if (c.prev.type != 'serial' && c.newly.type == 'serial') { + yield 'ALTER COLUMN "${c.prev.name}" SET DATA TYPE int8 USING ${c.newly.name}::int8'; + yield "ALTER COLUMN \"${c.prev.name}\" SET DEFAULT nextval('${table.name}_${c.newly.name}_seq')"; + } else { + var update = c.prev.type != c.newly.type + ? 'SET DATA TYPE ${c.newly.type} USING ${c.newly.name}::${c.newly.type}' + : '${c.newly.isNullable ? 'DROP' : 'SET'} NOT NULL'; + yield 'ALTER COLUMN "${c.prev.name}" $update'; + } + }), + ]; + + for (var c in table.columns.modified.where((c) => c.prev.type != 'serial' && c.newly.type == 'serial')) { + appendStatement('CREATE SEQUENCE IF NOT EXISTS ${table.name}_${c.newly.name}_seq ' + 'OWNED BY "public"."${table.name}"."${c.newly.name}";'); + } + + appendStatement('ALTER TABLE "${table.name}"\n' + ' ${updatedColumns.join(",\n ")};'); + } + } + + for (var table in diff.tables.modified) { + var uniqueConstraints = + table.constraints.added.where((c) => c is PrimaryKeyConstraint || c is UniqueConstraint).toList(); + if (uniqueConstraints.isNotEmpty) { + appendStatement('ALTER TABLE "${table.name}"\n' + ' ${uniqueConstraints.map((c) => 'ADD ${c.toString()}').join(",\n ")};'); + } + } + + for (var table in diff.tables.added) { + var uniqueConstraints = table.constraints.where((c) => c is PrimaryKeyConstraint || c is UniqueConstraint).toList(); + if (uniqueConstraints.isNotEmpty) { + appendStatement('ALTER TABLE "${table.name}"\n' + ' ${uniqueConstraints.map((c) => 'ADD ${c.toString()}').join(",\n ")};'); + } + } + + for (var table in diff.tables.modified) { + var foreignKeyConstraints = table.constraints.added.whereType().toList(); + if (foreignKeyConstraints.isNotEmpty) { + appendStatement('ALTER TABLE "${table.name}"\n' + ' ${foreignKeyConstraints.map((c) => 'ADD ${c.toString()}').join(",\n ")};'); + } + } for (var table in diff.tables.added) { - await writeFile(dir, 'create_${table.name}', """ - CREATE TABLE IF NOT EXISTS "${table.name}" ( - ${table.columns.values.map((c) => '"${c.name}" ${c.type} ${c.isNullable ? 'NULL' : 'NOT NULL'}').join(",")} - ) - """); + var foreignKeyConstraints = table.constraints.whereType().toList(); + if (foreignKeyConstraints.isNotEmpty) { + appendStatement('ALTER TABLE "${table.name}"\n' + ' ${foreignKeyConstraints.map((c) => 'ADD ${c.toString()}').join(",\n ")};'); + } + } + + for (var table in diff.tables.modified) { + for (var index in table.indexes.added) { + appendStatement('CREATE ${index.statement(table.name)};'); + } + } + + for (var table in diff.tables.added) { + for (var index in table.indexes) { + appendStatement('CREATE ${index.statement(table.name)};'); + } + } + + if (alterTables.isNotEmpty) { + await writeFile(dir, 'alter_tables', alterTables); } await patchViews(dir, diff); @@ -44,23 +151,32 @@ Future patchViews(Directory dir, DatabaseSchemaDiff diff) async { var toDropNodes = currViewNodes.where((n) => toDrop.contains(n.view)).expand(getParents).toSet(); var toDropGraph = toDropNodes.where((n) => n.parents.isEmpty).toSet(); - while (toDropGraph.isNotEmpty) { - var node = toDropGraph.first; - toDropGraph.remove(node); - toDropNodes.remove(node); + if (toDropGraph.isNotEmpty) { + var dropViews = ''; - if (!toDrop.contains(node.view)) { - toAdd.add(node.view); - } + while (toDropGraph.isNotEmpty) { + var node = toDropGraph.first; + toDropGraph.remove(node); + toDropNodes.remove(node); - await writeFile(dir, 'drop_${node.view.name}', 'DROP VIEW ${node.view.name}'); + if (!toDrop.contains(node.view)) { + toAdd.add(node.view); + } - for (var child in node.children) { - child.parents.remove(node); - if (child.parents.isEmpty) { - toDropGraph.add(child); + if (dropViews.isNotEmpty) { + dropViews += '\n\n'; + } + dropViews += 'DROP VIEW ${node.view.name};'; + + for (var child in node.children) { + child.parents.remove(node); + if (child.parents.isEmpty) { + toDropGraph.add(child); + } } } + + await writeFile(dir, 'drop_views', dropViews); } if (toDropNodes.isNotEmpty) { @@ -73,19 +189,28 @@ Future patchViews(Directory dir, DatabaseSchemaDiff diff) async { var toAddNodes = ViewSchema.buildGraph(toAdd); var toAddGraph = toAddNodes.where((n) => n.children.isEmpty).toSet(); - while (toAddGraph.isNotEmpty) { - var node = toAddGraph.first; - toAddGraph.remove(node); - toAddNodes.remove(node); + if (toAddGraph.isNotEmpty) { + var createViews = ''; - await writeFile(dir, 'create_${node.view.name}', 'CREATE VIEW ${node.view.name} AS \n${node.view.definition}'); + while (toAddGraph.isNotEmpty) { + var node = toAddGraph.first; + toAddGraph.remove(node); + toAddNodes.remove(node); - for (var parent in node.parents) { - parent.children.remove(node); - if (parent.children.isEmpty) { - toAddGraph.add(parent); + if (createViews.isNotEmpty) { + createViews += '\n\n'; + } + createViews += 'CREATE VIEW ${node.view.name} AS \n${node.view.definition};'; + + for (var parent in node.parents) { + parent.children.remove(node); + if (parent.children.isEmpty) { + toAddGraph.add(parent); + } } } + + await writeFile(dir, 'create_views', createViews); } if (toAddNodes.isNotEmpty) { @@ -95,7 +220,16 @@ Future patchViews(Directory dir, DatabaseSchemaDiff diff) async { } Future removeUnused(Directory dir, DatabaseSchemaDiff diff) async { - for (var table in diff.tables.removed) { - await writeFile(dir, 'drop_${table.name}', 'DROP TABLE "${table.name}" CASCADE'); + if (diff.tables.removed.isNotEmpty) { + var dropTables = ''; + + for (var table in diff.tables.removed) { + if (dropTables.isNotEmpty) { + dropTables += '\n\n'; + } + dropTables += 'DROP TABLE "${table.name}" CASCADE;'; + } + + await writeFile(dir, 'drop_tables', dropTables); } } diff --git a/bin/src/patcher.dart b/bin/src/patcher.dart index 17f5ae9..42b2921 100644 --- a/bin/src/patcher.dart +++ b/bin/src/patcher.dart @@ -13,12 +13,6 @@ Future patchSchema(Database db, DatabaseSchemaDiff diff) async { } for (var table in diff.tables.modified) { - for (var trigger in table.triggers.removed) { - await db.query(''' - DROP TRIGGER "${trigger.name}" ON "${table.name}" - '''); - } - for (var index in table.indexes.removed) { await db.query('DROP INDEX "__${index.name}"'); } @@ -104,18 +98,7 @@ Future patchSchema(Database db, DatabaseSchemaDiff diff) async { } } - await _createArrayKeysCheckFunction(db); - for (var table in diff.tables.modified) { - for (var trigger in table.triggers.added) { - await db.query(""" - CREATE TRIGGER "${trigger.name}" - AFTER DELETE OR UPDATE OF "${trigger.column}" ON "${table.name}" - FOR EACH ROW - EXECUTE FUNCTION ${trigger.function}(${trigger.args.map((a) => "'$a'").join(", ")}); - """); - } - for (var index in table.indexes.added) { await db.query('CREATE ${index.statement(table.name)}'); } @@ -227,46 +210,4 @@ Future removeUnused(Database db, DatabaseSchemaDiff diff) async { for (var table in diff.tables.removed) { await db.query('DROP TABLE "${table.name}" CASCADE'); } -} - -Future _createArrayKeysCheckFunction(Database db) async { - var tempDebugPrint = db.debugPrint; - db.debugPrint = false; - await db.query(""" - CREATE OR REPLACE FUNCTION check_array_keys() - RETURNS TRIGGER - LANGUAGE plpgsql - AS \$function\$ - DECLARE - tableName TEXT; - columnName TEXT; - keyName TEXT; - BEGIN - tableName := TG_ARGV[0]; - columnName := TG_ARGV[1]; - keyName := TG_ARGV[2]; - - IF NEW IS NULL THEN - EXECUTE ' - UPDATE ' || quote_ident(tableName) || ' - SET ' || quote_ident(columnName) || ' = array_remove(' - || quote_ident(columnName) || ', \$1.' || quote_ident(keyName) - || ') - WHERE \$1.' || quote_ident(keyName) || ' = ANY (' || quote_ident(columnName) || ') - ' USING OLD; - ELSE - EXECUTE ' - UPDATE ' || quote_ident(tableName) || ' - SET ' || quote_ident(columnName) || ' = array_replace(' - || quote_ident(columnName) || ', \$1.' || quote_ident(keyName) || ', \$2.' || quote_ident(keyName) - || ') - WHERE \$1.' || quote_ident(keyName) || ' = ANY (' || quote_ident(columnName) || ') - ' USING OLD, NEW; - END IF; - - RETURN NULL; - END; - \$function\$ - """); - db.debugPrint = tempDebugPrint; -} +} \ No newline at end of file diff --git a/bin/src/schema.dart b/bin/src/schema.dart index df66534..66f3400 100644 --- a/bin/src/schema.dart +++ b/bin/src/schema.dart @@ -136,9 +136,7 @@ class PrimaryKeyConstraint extends TableConstraint { @override String toString() { - return ''' - PRIMARY KEY ( "$column" ) - '''; + return 'PRIMARY KEY ( "$column" )'; } @override @@ -164,11 +162,9 @@ class ForeignKeyConstraint extends TableConstraint { @override String toString() { - return ''' - FOREIGN KEY ( "$srcColumn" ) - REFERENCES $table ( "$column" ) - ON DELETE ${_ac(onDelete)} ON UPDATE ${_ac(onUpdate)} - '''; + return 'FOREIGN KEY ( "$srcColumn" ) ' + 'REFERENCES $table ( "$column" ) ' + 'ON DELETE ${_ac(onDelete)} ON UPDATE ${_ac(onUpdate)}'; } String _ac(ForeignKeyAction action) { @@ -200,9 +196,7 @@ class UniqueConstraint extends TableConstraint { @override String toString() { - return ''' - UNIQUE ( "$column" ) - '''; + return 'UNIQUE ( "$column" )'; } @override @@ -247,11 +241,9 @@ extension TableIndexParser on TableIndex { } String statement(String tableName) { - return """ - ${unique ? 'UNIQUE' : ''} INDEX "__$name" - ON "$tableName" USING ${algorithm.toString().split(".")[1]} ( $joinedColumns ) - ${condition != null ? 'WHERE $condition' : ''} - """; + return '${unique ? 'UNIQUE' : ''} INDEX "__$name" ' + 'ON "$tableName" USING ${algorithm.toString().split(".")[1]} ( $joinedColumns ) ' + '${condition != null ? 'WHERE $condition' : ''}'; } } diff --git a/example/migrations/0_create_tables.sql b/example/migrations/0_create_tables.sql new file mode 100644 index 0000000..0a76a39 --- /dev/null +++ b/example/migrations/0_create_tables.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS "accounts" ( + "id" serial NOT NULL, + "first_name" text NOT NULL, + "last_name" text NOT NULL, + "location" point NOT NULL, + "data" serial NOT NULL, + "company_id" text NULL +); + +CREATE TABLE IF NOT EXISTS "billing_addresses" ( + "account_id" int8 NULL, + "company_id" text NULL, + "city" text NOT NULL, + "postcode" text NOT NULL, + "name" text NOT NULL, + "street" text NOT NULL +); + +CREATE TABLE IF NOT EXISTS "companies" ( + "id" text NOT NULL, + "name" text NOT NULL +); + +CREATE TABLE IF NOT EXISTS "invoices" ( + "account_id" int8 NULL, + "company_id" text NULL, + "id" text NOT NULL, + "title" text NOT NULL, + "invoice_id" text NOT NULL +); + +CREATE TABLE IF NOT EXISTS "parties" ( + "sponsor_id" text NULL, + "id" text NOT NULL, + "name" text NOT NULL, + "date" int8 NOT NULL +); + +CREATE TABLE IF NOT EXISTS "accounts_parties" ( + "account_id" int8 NOT NULL, + "party_id" text NOT NULL +); \ No newline at end of file diff --git a/example/migrations/1_alter_tables.sql b/example/migrations/1_alter_tables.sql new file mode 100644 index 0000000..b1fa582 --- /dev/null +++ b/example/migrations/1_alter_tables.sql @@ -0,0 +1,35 @@ +ALTER TABLE "accounts" + ADD PRIMARY KEY ( "id" ); + +ALTER TABLE "billing_addresses" + ADD UNIQUE ( "account_id" ); + +ALTER TABLE "companies" + ADD PRIMARY KEY ( "id" ); + +ALTER TABLE "invoices" + ADD PRIMARY KEY ( "id" ); + +ALTER TABLE "parties" + ADD PRIMARY KEY ( "id" ); + +ALTER TABLE "accounts_parties" + ADD PRIMARY KEY ( "account_id", "party_id" ); + +ALTER TABLE "accounts" + ADD FOREIGN KEY ( "company_id" ) REFERENCES companies ( "id" ) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "billing_addresses" + ADD FOREIGN KEY ( "account_id" ) REFERENCES accounts ( "id" ) ON DELETE CASCADE ON UPDATE CASCADE, + ADD FOREIGN KEY ( "company_id" ) REFERENCES companies ( "id" ) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "invoices" + ADD FOREIGN KEY ( "account_id" ) REFERENCES accounts ( "id" ) ON DELETE SET NULL ON UPDATE CASCADE, + ADD FOREIGN KEY ( "company_id" ) REFERENCES companies ( "id" ) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "parties" + ADD FOREIGN KEY ( "sponsor_id" ) REFERENCES companies ( "id" ) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "accounts_parties" + ADD FOREIGN KEY ( "account_id" ) REFERENCES accounts ( "id" ) ON DELETE CASCADE ON UPDATE CASCADE, + ADD FOREIGN KEY ( "party_id" ) REFERENCES parties ( "id" ) ON DELETE CASCADE ON UPDATE CASCADE; \ No newline at end of file diff --git a/example/migrations/2_create_views.sql b/example/migrations/2_create_views.sql new file mode 100644 index 0000000..4436d8e --- /dev/null +++ b/example/migrations/2_create_views.sql @@ -0,0 +1,134 @@ +CREATE VIEW billing_addresses_view AS +SELECT "billing_addresses".* +FROM "billing_addresses" +WHERE '_#dbf42d5d7461f07a564bb43b6e8d38ff097ba5ea#_' IS NOT NULL; + +CREATE VIEW owner_invoices_view AS +SELECT "invoices".* +FROM "invoices" +WHERE '_#e5f94a9602353fec4c4d2e04c795cf62ca4b1226#_' IS NOT NULL; + +CREATE VIEW company_parties_view AS +SELECT "parties".* +FROM "parties" +WHERE '_#088e3580cd83cc28fd25aa3ff86b9fc46addc894#_' IS NOT NULL; + +CREATE VIEW member_companies_view AS +SELECT "companies".*, "addresses"."data" as "addresses" +FROM "companies" +LEFT JOIN ( + SELECT "billing_addresses"."company_id", + to_jsonb(array_agg("billing_addresses".*)) as data + FROM "billing_addresses_view" "billing_addresses" + GROUP BY "billing_addresses"."company_id" +) "addresses" +ON "companies"."id" = "addresses"."company_id" +WHERE '_#63bb7d78bea9d92382206080c274b26e8c3e3f1a#_' IS NOT NULL; + +CREATE VIEW company_accounts_view AS +SELECT "accounts".*, array_to_json(ARRAY (( + SELECT * + FROM jsonb_array_elements("parties".data) AS "parties" + WHERE (parties -> 'sponsor_id') = to_jsonb (accounts.company_id) +)) ) AS "parties" +FROM "accounts" +LEFT JOIN ( + SELECT "accounts_parties"."account_id", + to_jsonb(array_agg("parties".*)) as data + FROM "accounts_parties" + LEFT JOIN "company_parties_view" "parties" + ON "parties"."id" = "accounts_parties"."party_id" + GROUP BY "accounts_parties"."account_id" +) "parties" +ON "accounts"."id" = "parties"."account_id" +WHERE '_#e3332abebaed52c92240088408cde459d4269cd1#_' IS NOT NULL; + +CREATE VIEW guest_parties_view AS +SELECT "parties".*, row_to_json("sponsor".*) as "sponsor" +FROM "parties" +LEFT JOIN "member_companies_view" "sponsor" +ON "parties"."sponsor_id" = "sponsor"."id" +WHERE '_#ead223f933e4d5e9d76e71517fdce0d1621491c5#_' IS NOT NULL; + +CREATE VIEW admin_companies_view AS +SELECT "companies".*, "members"."data" as "members", "addresses"."data" as "addresses", "invoices"."data" as "invoices", "parties"."data" as "parties" +FROM "companies" +LEFT JOIN ( + SELECT "accounts"."company_id", + to_jsonb(array_agg("accounts".*)) as data + FROM "company_accounts_view" "accounts" + GROUP BY "accounts"."company_id" +) "members" +ON "companies"."id" = "members"."company_id" +LEFT JOIN ( + SELECT "billing_addresses"."company_id", + to_jsonb(array_agg("billing_addresses".*)) as data + FROM "billing_addresses_view" "billing_addresses" + GROUP BY "billing_addresses"."company_id" +) "addresses" +ON "companies"."id" = "addresses"."company_id" +LEFT JOIN ( + SELECT "invoices"."company_id", + to_jsonb(array_agg("invoices".*)) as data + FROM "owner_invoices_view" "invoices" + GROUP BY "invoices"."company_id" +) "invoices" +ON "companies"."id" = "invoices"."company_id" +LEFT JOIN ( + SELECT "parties"."sponsor_id", + to_jsonb(array_agg("parties".*)) as data + FROM "company_parties_view" "parties" + GROUP BY "parties"."sponsor_id" +) "parties" +ON "companies"."id" = "parties"."sponsor_id" +WHERE '_#466c9600172ef6af2874adf687244613a5a75add#_' IS NOT NULL; + +CREATE VIEW user_accounts_view AS +SELECT "accounts".*, row_to_json("billingAddress".*) as "billingAddress", "invoices"."data" as "invoices", row_to_json("company".*) as "company", "parties"."data" as "parties" +FROM "accounts" +LEFT JOIN "billing_addresses_view" "billingAddress" +ON "accounts"."id" = "billingAddress"."account_id" +LEFT JOIN ( + SELECT "invoices"."account_id", + to_jsonb(array_agg("invoices".*)) as data + FROM "owner_invoices_view" "invoices" + GROUP BY "invoices"."account_id" +) "invoices" +ON "accounts"."id" = "invoices"."account_id" +LEFT JOIN "member_companies_view" "company" +ON "accounts"."company_id" = "company"."id" +LEFT JOIN ( + SELECT "accounts_parties"."account_id", + to_jsonb(array_agg("parties".*)) as data + FROM "accounts_parties" + LEFT JOIN "guest_parties_view" "parties" + ON "parties"."id" = "accounts_parties"."party_id" + GROUP BY "accounts_parties"."account_id" +) "parties" +ON "accounts"."id" = "parties"."account_id" +WHERE '_#afa18427bb7f6bd4f6cf5872d9ae8ecccf56e227#_' IS NOT NULL; + +CREATE VIEW admin_accounts_view AS +SELECT "accounts".*, row_to_json("billingAddress".*) as "billingAddress", "invoices"."data" as "invoices", row_to_json("company".*) as "company", "parties"."data" as "parties" +FROM "accounts" +LEFT JOIN "billing_addresses_view" "billingAddress" +ON "accounts"."id" = "billingAddress"."account_id" +LEFT JOIN ( + SELECT "invoices"."account_id", + to_jsonb(array_agg("invoices".*)) as data + FROM "owner_invoices_view" "invoices" + GROUP BY "invoices"."account_id" +) "invoices" +ON "accounts"."id" = "invoices"."account_id" +LEFT JOIN "member_companies_view" "company" +ON "accounts"."company_id" = "company"."id" +LEFT JOIN ( + SELECT "accounts_parties"."account_id", + to_jsonb(array_agg("parties".*)) as data + FROM "accounts_parties" + LEFT JOIN "guest_parties_view" "parties" + ON "parties"."id" = "accounts_parties"."party_id" + GROUP BY "accounts_parties"."account_id" +) "parties" +ON "accounts"."id" = "parties"."account_id" +WHERE '_#afa18427bb7f6bd4f6cf5872d9ae8ecccf56e227#_' IS NOT NULL; \ No newline at end of file diff --git a/lib/src/core/annotations.dart b/lib/src/core/annotations.dart index 59daccc..38b312a 100644 --- a/lib/src/core/annotations.dart +++ b/lib/src/core/annotations.dart @@ -56,13 +56,11 @@ abstract class ListTransformer extends Transformer { @override String transform(String column, String table) { var w = where(column, table); - return ''' - array_to_json(ARRAY (( - SELECT ${select(column, table) ?? '*'} - FROM jsonb_array_elements("$column".data) AS "$column" - ${w != null ? 'WHERE $w' : ''} - )) ) AS "$column" - '''; + return 'array_to_json(ARRAY ((\n' + ' SELECT ${select(column, table) ?? '*'}\n' + ' FROM jsonb_array_elements("$column".data) AS "$column"\n' + '${w != null ? ' WHERE $w\n' : ''}' + ')) ) AS "$column"'; } } diff --git a/lib/src/helpers/json_schema.dart b/lib/src/helpers/json_schema.dart index 2eaa8c4..de44e37 100644 --- a/lib/src/helpers/json_schema.dart +++ b/lib/src/helpers/json_schema.dart @@ -23,7 +23,7 @@ Map buildViewSchema(Map map) { map['table_name'] as String, map['primary_key_name'] as String?, (map['columns'] as List).map((c) => ViewColumnSchema.fromMap(c as Map)).toList(), - ).replaceAll(RegExp(r'\s+'), ' '); + ); var hash = sha1.convert(utf8.encode(definition)).toString(); @@ -130,6 +130,6 @@ String buildViewQuery(String tableName, String? primaryKeyName, List ', ${j.key}').join()}\n' - 'FROM "$tableName"\n' + 'FROM "$tableName"${joins.isNotEmpty ? '\n' : ''}' '${joins.map((j) => j.value).join('\n')}'; } From a5bb91838271690945d3f69ca08b72965dc43545 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Sat, 20 Aug 2022 15:18:11 +0200 Subject: [PATCH 05/14] added annotation support for insert and update requests --- .../builder/generators/insert_generator.dart | 1 + .../builder/generators/update_generator.dart | 1 + lib/src/builder/table_builder.dart | 10 +++ lib/src/builder/utils.dart | 52 +++++++++++++ lib/src/builder/view_builder.dart | 49 +----------- lib/src/core/annotations.dart | 5 ++ test/packages/serialization/lib/main.dart | 3 + test/packages/serialization/lib/models.dart | 12 ++- .../serialization/lib/models.mapper.g.dart | 74 +++++++++++++++++++ .../serialization/lib/models.schema.g.dart | 2 + test/serialization_test.dart | 4 + 11 files changed, 161 insertions(+), 52 deletions(-) diff --git a/lib/src/builder/generators/insert_generator.dart b/lib/src/builder/generators/insert_generator.dart index 6b61f7e..a257797 100644 --- a/lib/src/builder/generators/insert_generator.dart +++ b/lib/src/builder/generators/insert_generator.dart @@ -160,6 +160,7 @@ class InsertGenerator { } return ''' + ${table.insertRequestAnnotation ?? ''} class $requestClassName { $requestClassName({${requestFields.map((f) => '${f.key.endsWith('?') ? '' : 'required '}this.${f.value}').join(', ')}}); ${requestFields.map((f) => '${f.key} ${f.value};').join('\n')} diff --git a/lib/src/builder/generators/update_generator.dart b/lib/src/builder/generators/update_generator.dart index e1983fd..56b212f 100644 --- a/lib/src/builder/generators/update_generator.dart +++ b/lib/src/builder/generators/update_generator.dart @@ -120,6 +120,7 @@ class UpdateGenerator { } return ''' + ${table.updateRequestAnnotation ?? ''} class $requestClassName { $requestClassName({${requestFields.map((f) => '${f.key.endsWith('?') ? '' : 'required '}this.${f.value}').join(', ')}}); ${requestFields.map((f) => '${f.key} ${f.value};').join('\n')} diff --git a/lib/src/builder/table_builder.dart b/lib/src/builder/table_builder.dart index 06104b3..5cc3352 100644 --- a/lib/src/builder/table_builder.dart +++ b/lib/src/builder/table_builder.dart @@ -24,6 +24,8 @@ class TableBuilder { late FieldElement? primaryKeyParameter; late List views; late List indexes; + String? insertRequestAnnotation; + String? updateRequestAnnotation; TableBuilder(this.element, this.annotation, this.state) { tableName = _getTableName(); @@ -39,6 +41,14 @@ class TableBuilder { indexes = annotation.read('indexes').listValue.map((o) { return IndexBuilder(this, o); }).toList(); + + if (!annotation.read('insertRequestAnnotation').isNull) { + insertRequestAnnotation = '@' + annotation.read('insertRequestAnnotation').toSource(); + } + + if (!annotation.read('updateRequestAnnotation').isNull) { + updateRequestAnnotation = '@' + annotation.read('updateRequestAnnotation').toSource(); + } } String _getTableName({bool singular = false}) { diff --git a/lib/src/builder/utils.dart b/lib/src/builder/utils.dart index 10f7b1a..9b63c1d 100644 --- a/lib/src/builder/utils.dart +++ b/lib/src/builder/utils.dart @@ -1,5 +1,6 @@ import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:source_gen/source_gen.dart'; @@ -63,3 +64,54 @@ String? getAnnotationCode(Element annotatedElement, Type annotationType, String return null; } + +extension ObjectSource on DartObject { + String toSource() { + return ConstantReader(this).toSource(); + } +} + +extension ReaderSource on ConstantReader { + String toSource() { + if (isLiteral) { + if (isString) { + return "'$literalValue'"; + } + return literalValue!.toString(); + } + + var rev = revive(); + + var str = ''; + if (rev.source.fragment.isNotEmpty) { + str = rev.source.fragment; + + if (rev.accessor.isNotEmpty) { + str += '.${rev.accessor}'; + } + str += '('; + var isFirst = true; + + for (var p in rev.positionalArguments) { + if (!isFirst) { + str += ', '; + } + isFirst = false; + str += p.toSource(); + } + + for (var p in rev.namedArguments.entries) { + if (!isFirst) { + str += ', '; + } + isFirst = false; + str += '${p.key}: ${p.value.toSource()}'; + } + + str += ')'; + } else { + str = rev.accessor; + } + return str; + } +} diff --git a/lib/src/builder/view_builder.dart b/lib/src/builder/view_builder.dart index dfde37d..f08ba16 100644 --- a/lib/src/builder/view_builder.dart +++ b/lib/src/builder/view_builder.dart @@ -187,51 +187,4 @@ class ViewBuilder { } return null; } -} - -extension ObjectSource on DartObject { - String toSource() { - var reader = ConstantReader(this); - - if (reader.isLiteral) { - if (reader.isString) { - return "'${reader.literalValue}'"; - } - return reader.literalValue!.toString(); - } - - var rev = reader.revive(); - - var str = ''; - if (rev.source.fragment.isNotEmpty) { - str = rev.source.fragment; - - if (rev.accessor.isNotEmpty) { - str += '.${rev.accessor}'; - } - str += '('; - var isFirst = true; - - for (var p in rev.positionalArguments) { - if (!isFirst) { - str += ', '; - } - isFirst = false; - str += p.toSource(); - } - - for (var p in rev.namedArguments.entries) { - if (!isFirst) { - str += ', '; - } - isFirst = false; - str += '${p.key}: ${p.value.toSource()}'; - } - - str += ')'; - } else { - str = rev.accessor; - } - return str; - } -} +} \ No newline at end of file diff --git a/lib/src/core/annotations.dart b/lib/src/core/annotations.dart index 38b312a..6860f0c 100644 --- a/lib/src/core/annotations.dart +++ b/lib/src/core/annotations.dart @@ -4,9 +4,14 @@ import 'database.dart'; class Model { final List views; final List indexes; + final dynamic insertRequestAnnotation; + final dynamic updateRequestAnnotation; + const Model({ this.views = const [], this.indexes = const [], + this.insertRequestAnnotation, + this.updateRequestAnnotation, }); } diff --git a/test/packages/serialization/lib/main.dart b/test/packages/serialization/lib/main.dart index 97d4950..d57b97a 100644 --- a/test/packages/serialization/lib/main.dart +++ b/test/packages/serialization/lib/main.dart @@ -9,4 +9,7 @@ Future main() async { var company = DefaultCompanyView(id: '01', member: PublicUserView(id: 'def', name: 'Susan')); print(company.toJson()); + + var request = UserUpdateRequest(id: 'abc', securityNumber: '007'); + print(request.toJson()); } diff --git a/test/packages/serialization/lib/models.dart b/test/packages/serialization/lib/models.dart index 9631fc1..9b8dca8 100644 --- a/test/packages/serialization/lib/models.dart +++ b/test/packages/serialization/lib/models.dart @@ -5,10 +5,14 @@ export 'package:dart_mappable/dart_mappable.dart'; export 'models.schema.g.dart'; export 'models.mapper.g.dart'; -@Model(views: [ - View('Default', [], MappableClass()), - View('Public', [Field.hidden('securityNumber')], MappableClass()), -]) +@Model( + views: [ + View('Default', [], MappableClass()), + View('Public', [Field.hidden('securityNumber')], MappableClass()), + ], + insertRequestAnnotation: MappableClass(), + updateRequestAnnotation: MappableClass(), +) abstract class User { @PrimaryKey() String get id; diff --git a/test/packages/serialization/lib/models.mapper.g.dart b/test/packages/serialization/lib/models.mapper.g.dart index 77301f2..e1f8875 100644 --- a/test/packages/serialization/lib/models.mapper.g.dart +++ b/test/packages/serialization/lib/models.mapper.g.dart @@ -8,6 +8,8 @@ import 'models.schema.g.dart'; var _mappers = { // class mappers + UserInsertRequestMapper._(), + UserUpdateRequestMapper._(), DefaultUserViewMapper._(), PublicUserViewMapper._(), DefaultCompanyViewMapper._(), @@ -18,6 +20,78 @@ var _mappers = { // === GENERATED CLASS MAPPERS AND EXTENSIONS === +class UserInsertRequestMapper extends BaseMapper { + UserInsertRequestMapper._(); + + @override Function get decoder => decode; + UserInsertRequest decode(dynamic v) => checked(v, (Map map) => fromMap(map)); + UserInsertRequest fromMap(Map map) => UserInsertRequest(companyId: Mapper.i.$getOpt(map, 'companyId'), id: Mapper.i.$get(map, 'id'), name: Mapper.i.$get(map, 'name'), securityNumber: Mapper.i.$get(map, 'securityNumber')); + + @override Function get encoder => (UserInsertRequest v) => encode(v); + dynamic encode(UserInsertRequest v) => toMap(v); + Map toMap(UserInsertRequest u) => {'companyId': Mapper.i.$enc(u.companyId, 'companyId'), 'id': Mapper.i.$enc(u.id, 'id'), 'name': Mapper.i.$enc(u.name, 'name'), 'securityNumber': Mapper.i.$enc(u.securityNumber, 'securityNumber')}; + + @override String stringify(UserInsertRequest self) => 'UserInsertRequest(companyId: ${Mapper.asString(self.companyId)}, id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)}, securityNumber: ${Mapper.asString(self.securityNumber)})'; + @override int hash(UserInsertRequest self) => Mapper.hash(self.companyId) ^ Mapper.hash(self.id) ^ Mapper.hash(self.name) ^ Mapper.hash(self.securityNumber); + @override bool equals(UserInsertRequest self, UserInsertRequest other) => Mapper.isEqual(self.companyId, other.companyId) && Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name) && Mapper.isEqual(self.securityNumber, other.securityNumber); + + @override Function get typeFactory => (f) => f(); +} + +extension UserInsertRequestMapperExtension on UserInsertRequest { + String toJson() => Mapper.toJson(this); + Map toMap() => Mapper.toMap(this); + UserInsertRequestCopyWith get copyWith => UserInsertRequestCopyWith(this, $identity); +} + +abstract class UserInsertRequestCopyWith<$R> { + factory UserInsertRequestCopyWith(UserInsertRequest value, Then then) = _UserInsertRequestCopyWithImpl<$R>; + $R call({String? companyId, String? id, String? name, String? securityNumber}); + $R apply(UserInsertRequest Function(UserInsertRequest) transform); +} + +class _UserInsertRequestCopyWithImpl<$R> extends BaseCopyWith implements UserInsertRequestCopyWith<$R> { + _UserInsertRequestCopyWithImpl(UserInsertRequest value, Then then) : super(value, then); + + @override $R call({Object? companyId = $none, String? id, String? name, String? securityNumber}) => $then(UserInsertRequest(companyId: or(companyId, $value.companyId), id: id ?? $value.id, name: name ?? $value.name, securityNumber: securityNumber ?? $value.securityNumber)); +} + +class UserUpdateRequestMapper extends BaseMapper { + UserUpdateRequestMapper._(); + + @override Function get decoder => decode; + UserUpdateRequest decode(dynamic v) => checked(v, (Map map) => fromMap(map)); + UserUpdateRequest fromMap(Map map) => UserUpdateRequest(companyId: Mapper.i.$getOpt(map, 'companyId'), id: Mapper.i.$get(map, 'id'), name: Mapper.i.$getOpt(map, 'name'), securityNumber: Mapper.i.$getOpt(map, 'securityNumber')); + + @override Function get encoder => (UserUpdateRequest v) => encode(v); + dynamic encode(UserUpdateRequest v) => toMap(v); + Map toMap(UserUpdateRequest u) => {'companyId': Mapper.i.$enc(u.companyId, 'companyId'), 'id': Mapper.i.$enc(u.id, 'id'), 'name': Mapper.i.$enc(u.name, 'name'), 'securityNumber': Mapper.i.$enc(u.securityNumber, 'securityNumber')}; + + @override String stringify(UserUpdateRequest self) => 'UserUpdateRequest(companyId: ${Mapper.asString(self.companyId)}, id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)}, securityNumber: ${Mapper.asString(self.securityNumber)})'; + @override int hash(UserUpdateRequest self) => Mapper.hash(self.companyId) ^ Mapper.hash(self.id) ^ Mapper.hash(self.name) ^ Mapper.hash(self.securityNumber); + @override bool equals(UserUpdateRequest self, UserUpdateRequest other) => Mapper.isEqual(self.companyId, other.companyId) && Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name) && Mapper.isEqual(self.securityNumber, other.securityNumber); + + @override Function get typeFactory => (f) => f(); +} + +extension UserUpdateRequestMapperExtension on UserUpdateRequest { + String toJson() => Mapper.toJson(this); + Map toMap() => Mapper.toMap(this); + UserUpdateRequestCopyWith get copyWith => UserUpdateRequestCopyWith(this, $identity); +} + +abstract class UserUpdateRequestCopyWith<$R> { + factory UserUpdateRequestCopyWith(UserUpdateRequest value, Then then) = _UserUpdateRequestCopyWithImpl<$R>; + $R call({String? companyId, String? id, String? name, String? securityNumber}); + $R apply(UserUpdateRequest Function(UserUpdateRequest) transform); +} + +class _UserUpdateRequestCopyWithImpl<$R> extends BaseCopyWith implements UserUpdateRequestCopyWith<$R> { + _UserUpdateRequestCopyWithImpl(UserUpdateRequest value, Then then) : super(value, then); + + @override $R call({Object? companyId = $none, String? id, Object? name = $none, Object? securityNumber = $none}) => $then(UserUpdateRequest(companyId: or(companyId, $value.companyId), id: id ?? $value.id, name: or(name, $value.name), securityNumber: or(securityNumber, $value.securityNumber))); +} + class DefaultUserViewMapper extends BaseMapper { DefaultUserViewMapper._(); diff --git a/test/packages/serialization/lib/models.schema.g.dart b/test/packages/serialization/lib/models.schema.g.dart index 9c4bbea..4da80ca 100644 --- a/test/packages/serialization/lib/models.schema.g.dart +++ b/test/packages/serialization/lib/models.schema.g.dart @@ -147,6 +147,7 @@ ON CONFLICT ( "id" ) DO UPDATE SET "member_id" = EXCLUDED."member_id" } } +@MappableClass() class UserInsertRequest { UserInsertRequest({this.companyId, required this.id, required this.name, required this.securityNumber}); String? companyId; @@ -161,6 +162,7 @@ class CompanyInsertRequest { String memberId; } +@MappableClass() class UserUpdateRequest { UserUpdateRequest({this.companyId, required this.id, this.name, this.securityNumber}); String? companyId; diff --git a/test/serialization_test.dart b/test/serialization_test.dart index 805f3ed..c8c6efb 100644 --- a/test/serialization_test.dart +++ b/test/serialization_test.dart @@ -33,9 +33,13 @@ void main() { var lines = output.split('\n'); + expect(lines, hasLength(5)); + expect(lines[0], equals('{"id":"abc","name":"Tom","securityNumber":"12345"}')); expect(lines[1], equals('DefaultUserView(id: abc, name: Alex, securityNumber: 12345)')); expect(lines[2], equals('{"id":"01","member":{"id":"def","name":"Susan"}}')); + expect(lines[3], equals('{"companyId":null,"id":"abc","name":null,"securityNumber":"007"}')); + expect(lines[4], equals('')); }, timeout: Timeout(Duration(seconds: 60))); }); From 83dc6672ba6b13670a5f425e3a3fb0c62070a40a Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Sat, 20 Aug 2022 18:18:19 +0200 Subject: [PATCH 06/14] fix type decoding --- lib/src/internals/model_registry.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/src/internals/model_registry.dart b/lib/src/internals/model_registry.dart index db79259..65c0e19 100644 --- a/lib/src/internals/model_registry.dart +++ b/lib/src/internals/model_registry.dart @@ -54,17 +54,15 @@ class ModelRegistry { ModelRegistry(Map converters) : converters = {..._baseConverters, ...converters}; T decode(dynamic value) { - if (converters[T] != null) { + if (value is T) { + return value; + } else if (converters[T] != null) { return converters[T]!.decode(value) as T; } else { - try { - return value as T; - } catch (_) { - throw ConverterException( - 'Cannot decode value $value of type ${value.runtimeType} to type $T: Unknown type.\n' - 'Did you forgot to include the class or register a custom type converter?', - ); - } + throw ConverterException( + 'Cannot decode value $value of type ${value.runtimeType} to type $T: Unknown type.\n' + 'Did you forgot to include the class or register a custom type converter?', + ); } } From 1572cfc97ba330c6bbb2376daf67aac7e7c3256c Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Sat, 20 Aug 2022 19:01:13 +0200 Subject: [PATCH 07/14] fixed missing default view and view case style --- build.yaml | 8 +- .../builder/generators/delete_generator.dart | 8 +- .../builder/generators/insert_generator.dart | 20 +- .../builder/generators/update_generator.dart | 14 +- .../builder/generators/view_generator.dart | 9 +- lib/src/builder/stormberry_builder.dart | 5 +- lib/src/builder/table_builder.dart | 4 + lib/src/builder/utils.dart | 47 +++- lib/src/builder/view_builder.dart | 23 +- pubspec.yaml | 1 + test/generation_test.dart | 17 ++ test/generation_test.schema.g.dart | 208 ++++++++++++++++++ 12 files changed, 324 insertions(+), 40 deletions(-) create mode 100644 test/generation_test.dart create mode 100644 test/generation_test.schema.g.dart diff --git a/build.yaml b/build.yaml index 4efcef8..1cbb9cf 100644 --- a/build.yaml +++ b/build.yaml @@ -17,4 +17,10 @@ builders: post_process_builders: output_cleanup: import: "package:stormberry/builder.dart" - builder_factory: "outputCleanup" \ No newline at end of file + builder_factory: "outputCleanup" +targets: + $default: + builders: + stormberry: + generate_for: + - test/generation_test.dart \ No newline at end of file diff --git a/lib/src/builder/generators/delete_generator.dart b/lib/src/builder/generators/delete_generator.dart index 2aa3c74..f4a3253 100644 --- a/lib/src/builder/generators/delete_generator.dart +++ b/lib/src/builder/generators/delete_generator.dart @@ -9,10 +9,10 @@ class DeleteGenerator { @override Future delete(Database db, List<$keyType> keys) async { if (keys.isEmpty) return; - await db.query(""" - DELETE FROM "${table.tableName}" - WHERE "${table.tableName}"."${table.primaryKeyColumn!.columnName}" IN ( \${keys.map((k) => registry.encode(k)).join(',')} ) - """); + await db.query( + 'DELETE FROM "${table.tableName}"\\n' + 'WHERE "${table.tableName}"."${table.primaryKeyColumn!.columnName}" IN ( \${keys.map((k) => registry.encode(k)).join(',')} )', + ); } '''; } diff --git a/lib/src/builder/generators/insert_generator.dart b/lib/src/builder/generators/insert_generator.dart index a257797..68422a3 100644 --- a/lib/src/builder/generators/insert_generator.dart +++ b/lib/src/builder/generators/insert_generator.dart @@ -94,20 +94,20 @@ class InsertGenerator { var conflictColumns = table.columns .whereType() .where((c) => c != table.primaryKeyColumn && (c is! FieldColumnBuilder || !c.isAutoIncrement)); - onConflictClause = '\nON CONFLICT ( "${table.primaryKeyColumn!.columnName}" ) DO UPDATE SET ' - '${conflictColumns.map((c) => '"${c.columnName}" = EXCLUDED."${c.columnName}"').join(', ')}'; + onConflictClause = '\n\'ON CONFLICT ( "${table.primaryKeyColumn!.columnName}" ) DO UPDATE SET ' + '${conflictColumns.map((c) => '"${c.columnName}" = EXCLUDED."${c.columnName}"').join(', ')}\''; } } else if (table.columns.where((c) => c is ForeignColumnBuilder && c.isUnique).length == 1) { var foreignColumn = table.columns.whereType().first; var conflictColumns = table.columns.whereType().where((c) => !c.isAutoIncrement); - onConflictClause = '\nON CONFLICT ( "${foreignColumn.columnName}" ) DO UPDATE SET ' - '${conflictColumns.map((c) => '"${c.columnName}" = EXCLUDED."${c.columnName}"').join(', ')}'; + onConflictClause = '\n\'ON CONFLICT ( "${foreignColumn.columnName}" ) DO UPDATE SET ' + '${conflictColumns.map((c) => '"${c.columnName}" = EXCLUDED."${c.columnName}"').join(', ')}\''; } else if (table.columns.where((c) => c is ForeignColumnBuilder && c.isUnique).length > 1) { var conflictColumns = table.columns.whereType().where((c) => !c.isAutoIncrement); conflictKeyStatement = 'var conflictKey = requests.isEmpty ? null : ${table.columns.whereType().map((c) => 'requests.first.${c.paramName} != null ? ${c.isUnique ? "'${c.columnName}'" : 'mull'} : ').join()} null;'; - onConflictClause = "\n\${conflictKey != null ? 'ON CONFLICT (\"\$conflictKey\" ) DO UPDATE SET " - "${conflictColumns.map((c) => '"${c.columnName}" = EXCLUDED."${c.columnName}"').join(', ')}' : ''}"; + onConflictClause = "\n'\${conflictKey != null ? 'ON CONFLICT (\"\$conflictKey\" ) DO UPDATE SET " + "${conflictColumns.map((c) => '"${c.columnName}" = EXCLUDED."${c.columnName}"').join(', ')}' : ''}'"; } var insertColumns = table.columns.whereType(); @@ -118,10 +118,10 @@ class InsertGenerator { if (requests.isEmpty) return${keyReturnStatement != null ? ' []' : ''}; ${autoIncrementStatement ?? ''} ${conflictKeyStatement ?? ''} - await db.query(""" - INSERT INTO "${table.tableName}" ( ${insertColumns.map((c) => '"${c.columnName}"').join(', ')} ) - VALUES \${requests.map((r) => '( ${insertColumns.map((c) => c is FieldColumnBuilder && c.isAutoIncrement ? '\${registry.encode(autoIncrements[requests.indexOf(r)][\'${c.columnName}\'])}' : '\${registry.encode(r.${c.paramName})}').join(', ')} )').join(', ')}${onConflictClause ?? ''} - """); + await db.query( + 'INSERT INTO "${table.tableName}" ( ${insertColumns.map((c) => '"${c.columnName}"').join(', ')} )\\n' + 'VALUES \${requests.map((r) => '( ${insertColumns.map((c) => c is FieldColumnBuilder && c.isAutoIncrement ? '\${registry.encode(autoIncrements[requests.indexOf(r)][\'${c.columnName}\'])}' : '\${registry.encode(r.${c.paramName})}').join(', ')} )').join(', ')}\\n'${onConflictClause ?? ''}, + ); ${deepInserts.isNotEmpty ? deepInserts.join() : ''} ${keyReturnStatement ?? ''} } diff --git a/lib/src/builder/generators/update_generator.dart b/lib/src/builder/generators/update_generator.dart index 56b212f..202cbf5 100644 --- a/lib/src/builder/generators/update_generator.dart +++ b/lib/src/builder/generators/update_generator.dart @@ -69,13 +69,13 @@ class UpdateGenerator { @override Future update(Database db, List<${table.element.name}UpdateRequest> requests) async { if (requests.isEmpty) return; - await db.query(""" - UPDATE "${table.tableName}" - SET ${setColumns.map((c) => '"${c.columnName}" = COALESCE(UPDATED."${c.columnName}"::${c.sqlType}, "${table.tableName}"."${c.columnName}")').join(', ')} - FROM ( VALUES \${requests.map((r) => '( ${updateColumns.map((c) => '\${registry.encode(r.${c.paramName})}').join(', ')} )').join(', ')} ) - AS UPDATED(${updateColumns.map((c) => '"${c.columnName}"').join(', ')}) - WHERE ${hasPrimaryKey ? '"${table.tableName}"."${table.primaryKeyColumn!.columnName}" = UPDATED."${table.primaryKeyColumn!.columnName}"' : table.columns.whereType().map((c) => '"${table.tableName}"."${c.columnName}" = UPDATED."${c.columnName}"').join(' AND ')} - """); + await db.query( + 'UPDATE "${table.tableName}"\\n' + 'SET ${setColumns.map((c) => '"${c.columnName}" = COALESCE(UPDATED."${c.columnName}"::${c.sqlType}, "${table.tableName}"."${c.columnName}")').join(', ')}\\n' + 'FROM ( VALUES \${requests.map((r) => '( ${updateColumns.map((c) => '\${registry.encode(r.${c.paramName})}').join(', ')} )').join(', ')} )\\n' + 'AS UPDATED(${updateColumns.map((c) => '"${c.columnName}"').join(', ')})\\n' + 'WHERE ${hasPrimaryKey ? '"${table.tableName}"."${table.primaryKeyColumn!.columnName}" = UPDATED."${table.primaryKeyColumn!.columnName}"' : table.columns.whereType().map((c) => '"${table.tableName}"."${c.columnName}" = UPDATED."${c.columnName}"').join(' AND ')}', + ); ${deepUpdates.isNotEmpty ? deepUpdates.join() : ''} } '''; diff --git a/lib/src/builder/generators/view_generator.dart b/lib/src/builder/generators/view_generator.dart index f304dc4..fd300d7 100644 --- a/lib/src/builder/generators/view_generator.dart +++ b/lib/src/builder/generators/view_generator.dart @@ -9,27 +9,26 @@ class ViewGenerator { var str = StringBuffer(); for (var view in table.views) { - var viewClassName = view.className; var viewName = view.viewName; if (table.primaryKeyColumn != null) { var paramType = table.primaryKeyColumn!.dartType; var paramName = table.primaryKeyColumn!.paramName; - var signature = 'Future<$viewClassName?> query$viewName($paramType $paramName)'; + var signature = 'Future<${view.entityName}?> query$viewName($paramType $paramName)'; if (abstract) { str.writeln('$signature;'); } else { str.writeln( - '@override $signature {\nreturn queryOne($paramName, ${view.className}Queryable());\n}', + '@override $signature {\nreturn queryOne($paramName, ${view.entityName}Queryable());\n}', ); } } - var signature = 'Future> query${viewName}s([QueryParams? params])'; + var signature = 'Future> query${viewName}s([QueryParams? params])'; if (abstract) { str.writeln('$signature;'); } else { - str.writeln('@override $signature {\nreturn queryMany(${view.className}Queryable(), params);\n}'); + str.writeln('@override $signature {\nreturn queryMany(${view.entityName}Queryable(), params);\n}'); } } diff --git a/lib/src/builder/stormberry_builder.dart b/lib/src/builder/stormberry_builder.dart index 870b900..bb66ad0 100644 --- a/lib/src/builder/stormberry_builder.dart +++ b/lib/src/builder/stormberry_builder.dart @@ -61,6 +61,8 @@ class StormberryBuilder implements Builder { Map generate(List libraries, BuildStep buildStep) { BuilderState state = BuilderState(options); + state.imports.add(Uri.parse('package:stormberry/internals.dart')); + for (var library in libraries) { if (library.isInSdk) { continue; @@ -99,8 +101,7 @@ class StormberryBuilder implements Builder { map['.output.g.dart'] = DartFormatter(pageWidth: 120).format(''' // ignore_for_file: prefer_relative_imports - import 'package:stormberry/internals.dart'; - ${state.imports.map((i) => "import '$i';").join('\n')} + ${writeImports(state.imports, buildStep.inputId)} ${RepositoryGenerator().generateRepositories(state)} '''); diff --git a/lib/src/builder/table_builder.dart b/lib/src/builder/table_builder.dart index 5cc3352..cd04ca4 100644 --- a/lib/src/builder/table_builder.dart +++ b/lib/src/builder/table_builder.dart @@ -38,6 +38,10 @@ class TableBuilder { return ViewBuilder(this, o); }).toList(); + if (views.isEmpty) { + views.add(ViewBuilder(this, null)); + } + indexes = annotation.read('indexes').listValue.map((o) { return IndexBuilder(this, o); }).toList(); diff --git a/lib/src/builder/utils.dart b/lib/src/builder/utils.dart index 9b63c1d..ecd74ca 100644 --- a/lib/src/builder/utils.dart +++ b/lib/src/builder/utils.dart @@ -2,8 +2,9 @@ import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart'; - +import 'package:path/path.dart' as path; import '../../internals.dart'; import '../core/case_style.dart'; @@ -115,3 +116,47 @@ extension ReaderSource on ConstantReader { return str; } } + +String writeImports(Set imports, AssetId input) { + + List sdk = [], package = [], relative = []; + + for (var import in imports) { + if (import.isScheme('asset')) { + var relativePath = + path.relative(import.path, from: path.dirname(input.uri.path)); + + relative.add(relativePath); + } else if (import.isScheme('package') && + import.pathSegments.first == input.package && + input.pathSegments.first == 'lib') { + var libPath = + import.replace(pathSegments: import.pathSegments.skip(1)).path; + + var inputPath = input.uri + .replace(pathSegments: input.uri.pathSegments.skip(1)) + .path; + + var relativePath = + path.relative(libPath, from: path.dirname(inputPath)); + + relative.add(relativePath); + } else if (import.scheme == 'dart') { + sdk.add(import.toString()); + } else if (import.scheme == 'package') { + package.add(import.toString()); + } else { + relative.add(import.toString()); + } + } + + sdk.sort(); + package.sort(); + relative.sort(); + + String joined(List s) => s.isNotEmpty + ? '${s.map((s) => "import '$s';").join('\n')}\n\n' + : ''; + + return joined(sdk) + joined(package) + joined(relative); +} \ No newline at end of file diff --git a/lib/src/builder/view_builder.dart b/lib/src/builder/view_builder.dart index f08ba16..feabb87 100644 --- a/lib/src/builder/view_builder.dart +++ b/lib/src/builder/view_builder.dart @@ -35,9 +35,9 @@ class ViewColumn { var c = column; if (c is LinkedColumnBuilder) { if (viewAs != null) { - return c.linkBuilder.views.firstWhere((v) => v.name == viewAs!.toLowerCase()); + return c.linkBuilder.views.firstWhere((v) => v.name.toLowerCase() == viewAs!.toLowerCase()); } else { - return c.linkBuilder.views.firstWhere((v) => v.name.isEmpty); + return c.linkBuilder.views.firstWhere((v) => v.isDefaultView); } } return null; @@ -84,15 +84,18 @@ class ViewBuilder { ViewBuilder(this.table, this.annotation); - String get name => annotation?.getField('name')!.toStringValue()!.toLowerCase() ?? ''; + String get name => annotation?.getField('name')!.toStringValue() ?? ''; + + bool get isDefaultView => name.isEmpty; + String get className => CaseStyle.pascalCase - .transform(name.isNotEmpty ? '${name}_${table.element.name}_view' : '${table.element.name}_view'); + .transform('${!isDefaultView ? '${name}_' : ''}${table.element.name}_view'); - String get entityName => name.isEmpty ? table.element.name : className; + String get entityName => isDefaultView ? table.element.name : className; - String get viewName => CaseStyle.pascalCase.transform(name.isNotEmpty ? '${name}_view' : 'view'); + String get viewName => CaseStyle.pascalCase.transform(isDefaultView ? entityName : '${name}_view'); - String get viewTableName => name.isNotEmpty ? '${name}_${table.tableName}_view' : '${table.tableName}_view'; + String get viewTableName => CaseStyle.snakeCase.transform('${!isDefaultView ? '${name}_' : ''}${table.tableName}_view'); List? _columns; List get columns => _columns ??= _getViewColumns(); @@ -125,7 +128,7 @@ class ViewBuilder { var viewAs = viewField.getField('viewAs')!.toStringValue(); if (viewAs == null && column is LinkedColumnBuilder) { - if (!column.linkBuilder.views.any((v) => v.name.isEmpty)) { + if (!column.linkBuilder.views.any((v) => v.isDefaultView)) { column.linkBuilder.views.add(ViewBuilder(column.linkBuilder, null)); } } @@ -144,7 +147,7 @@ class ViewBuilder { .whereType() .firstWhere((node) => node.methodName.name == 'View' && - (node.argumentList.arguments.first as StringLiteral).stringValue?.toLowerCase() == name) + (node.argumentList.arguments.first as StringLiteral).stringValue?.toLowerCase() == name.toLowerCase()) .argumentList .arguments[1]; if (fields is ListLiteral) { @@ -169,7 +172,7 @@ class ViewBuilder { columns.add(ViewColumn(column, viewAs: viewAs, transformer: transformerCode)); } else { if (column is LinkedColumnBuilder) { - if (!column.linkBuilder.views.any((v) => v.name.isEmpty)) { + if (!column.linkBuilder.views.any((v) => v.isDefaultView)) { column.linkBuilder.views.add(ViewBuilder(column.linkBuilder, null)); } } diff --git a/pubspec.yaml b/pubspec.yaml index 2bbaff0..647dc94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: yaml: ^3.1.0 dev_dependencies: + build_runner: ^2.2.0 lints: ^1.0.1 test: ^1.21.1 diff --git a/test/generation_test.dart b/test/generation_test.dart new file mode 100644 index 0000000..89b4a3c --- /dev/null +++ b/test/generation_test.dart @@ -0,0 +1,17 @@ +import 'package:stormberry/stormberry.dart'; + +@Model() +abstract class User { + @PrimaryKey() + String get id; + + String get name; +} + +@Model(views: [View('SuperSecret')]) +abstract class Account { + @PrimaryKey() + String get id; +} + +void main() {} diff --git a/test/generation_test.schema.g.dart b/test/generation_test.schema.g.dart new file mode 100644 index 0000000..684531a --- /dev/null +++ b/test/generation_test.schema.g.dart @@ -0,0 +1,208 @@ +// ignore_for_file: prefer_relative_imports +import 'package:stormberry/internals.dart'; + +import 'generation_test.dart'; + +extension Repositories on Database { + UserRepository get users => UserRepository._(this); + AccountRepository get accounts => AccountRepository._(this); +} + +final registry = ModelRegistry({}); + +abstract class UserRepository + implements + ModelRepository, + ModelRepositoryInsert, + ModelRepositoryUpdate, + ModelRepositoryDelete { + factory UserRepository._(Database db) = _UserRepository; + + Future queryUser(String id); + Future> queryUsers([QueryParams? params]); +} + +class _UserRepository extends BaseRepository + with + RepositoryInsertMixin, + RepositoryUpdateMixin, + RepositoryDeleteMixin + implements UserRepository { + _UserRepository(Database db) : super(db: db); + + @override + Future queryUser(String id) { + return queryOne(id, UserQueryable()); + } + + @override + Future> queryUsers([QueryParams? params]) { + return queryMany(UserQueryable(), params); + } + + @override + Future insert(Database db, List requests) async { + if (requests.isEmpty) return; + + await db.query( + 'INSERT INTO "users" ( "id", "name" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)} )').join(', ')}\n' + 'ON CONFLICT ( "id" ) DO UPDATE SET "name" = EXCLUDED."name"', + ); + } + + @override + Future update(Database db, List requests) async { + if (requests.isEmpty) return; + await db.query( + 'UPDATE "users"\n' + 'SET "name" = COALESCE(UPDATED."name"::text, "users"."name")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)} )').join(', ')} )\n' + 'AS UPDATED("id", "name")\n' + 'WHERE "users"."id" = UPDATED."id"', + ); + } + + @override + Future delete(Database db, List keys) async { + if (keys.isEmpty) return; + await db.query( + 'DELETE FROM "users"\n' + 'WHERE "users"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} )', + ); + } +} + +abstract class AccountRepository + implements + ModelRepository, + ModelRepositoryInsert, + ModelRepositoryUpdate, + ModelRepositoryDelete { + factory AccountRepository._(Database db) = _AccountRepository; + + Future querySuperSecretView(String id); + Future> querySuperSecretViews([QueryParams? params]); +} + +class _AccountRepository extends BaseRepository + with + RepositoryInsertMixin, + RepositoryUpdateMixin, + RepositoryDeleteMixin + implements AccountRepository { + _AccountRepository(Database db) : super(db: db); + + @override + Future querySuperSecretView(String id) { + return queryOne(id, SuperSecretAccountViewQueryable()); + } + + @override + Future> querySuperSecretViews([QueryParams? params]) { + return queryMany(SuperSecretAccountViewQueryable(), params); + } + + @override + Future insert(Database db, List requests) async { + if (requests.isEmpty) return; + + await db.query( + 'INSERT INTO "accounts" ( "id" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.id)} )').join(', ')}\n' + 'ON CONFLICT ( "id" ) DO UPDATE SET ', + ); + } + + @override + Future update(Database db, List requests) async { + if (requests.isEmpty) return; + await db.query( + 'UPDATE "accounts"\n' + 'SET \n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)} )').join(', ')} )\n' + 'AS UPDATED("id")\n' + 'WHERE "accounts"."id" = UPDATED."id"', + ); + } + + @override + Future delete(Database db, List keys) async { + if (keys.isEmpty) return; + await db.query( + 'DELETE FROM "accounts"\n' + 'WHERE "accounts"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} )', + ); + } +} + +class UserInsertRequest { + UserInsertRequest({required this.id, required this.name}); + String id; + String name; +} + +class AccountInsertRequest { + AccountInsertRequest({required this.id}); + String id; +} + +class UserUpdateRequest { + UserUpdateRequest({required this.id, this.name}); + String id; + String? name; +} + +class AccountUpdateRequest { + AccountUpdateRequest({required this.id}); + String id; +} + +class UserQueryable extends KeyedViewQueryable { + @override + String get keyName => 'id'; + + @override + String encodeKey(String key) => registry.encode(key); + + @override + String get tableName => 'users_view'; + + @override + String get tableAlias => 'users'; + + @override + User decode(TypedMap map) => UserView(id: map.get('id', registry.decode), name: map.get('name', registry.decode)); +} + +class UserView implements User { + UserView({required this.id, required this.name}); + + @override + final String id; + @override + final String name; +} + +class SuperSecretAccountViewQueryable extends KeyedViewQueryable { + @override + String get keyName => 'id'; + + @override + String encodeKey(String key) => registry.encode(key); + + @override + String get tableName => 'super_secret_accounts_view'; + + @override + String get tableAlias => 'accounts'; + + @override + SuperSecretAccountView decode(TypedMap map) => SuperSecretAccountView(id: map.get('id', registry.decode)); +} + +class SuperSecretAccountView { + SuperSecretAccountView({required this.id}); + + final String id; +} From 1d5b0c160315d87b566dd462a6d7bef8bc7183ee Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Sat, 20 Aug 2022 19:40:07 +0200 Subject: [PATCH 08/14] update changelog and readme --- CHANGELOG.md | 6 ++++++ README.md | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 639b842..e721e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.8.0 + +- Added support for serialization through custom annotations +- Added support for manual migration output to .sql files +- Fixed bug with default views and value decoding + # 0.7.0 - Added `@AutoIncrement()` annotation for auto incrementing values diff --git a/README.md b/README.md index 4b181e9..206ccf9 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,25 @@ As mentioned before, when you have two-way relations in your models you must use any cyclic dependencies. `stormberry` can't resolve them for you, however it will warn you if it detects any when trying to [migrate your database schema](#database-migration-tool). +### Serialization + +When using views, you may need serialization capabilities to send them through an api. While stormberry does +not do serialization by itself, it enables you to use your favorite serialization package through custom annotations. + +When specifying a view, add a target annotation to its constructor: + +```dart +@Model( + views: [ + View('SomeView', [/*field modififers*/], MappableClass()) + ] +) +``` + +This uses the '@MappableClass()' annotation from the [`dart_mappable`](https://pub.dev/packages/dart_mappable) package, +which will be placed on the resulting `SomeView` entity class. Check out [this example](https://github.com/schultek/stormberry/tree/develop/test/packages/serialization) to see +how this can be used to generate serialization extensions for these classes. + ## Indexes As an advanced configuration you can specify indexes on your table using the `TableIndex` class. @@ -404,3 +423,5 @@ The tool supported the following options: - `--dry-run`: Logs any changes to the schema without writing to the database, and exists with code 1 if there are any. - `--apply-changes`: Apply any changes without asking for confirmation. +- `-o=`: Specify an output folder. When used, this will output all migration statements to + `.sql` files in this folder instead of applying them to the database. \ No newline at end of file From 4859818368e6598e822933831ae7f041474872b8 Mon Sep 17 00:00:00 2001 From: jaumard Date: Mon, 22 Aug 2022 08:56:08 +0200 Subject: [PATCH 09/14] Add ability to set a custom table name for legacy database support --- .../generators/repository_generator.dart | 2 +- lib/src/builder/table_builder.dart | 10 ++ lib/src/core/annotations.dart | 2 + test/generation_test.dart | 35 ++++++- test/generation_test.schema.g.dart | 98 +++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) diff --git a/lib/src/builder/generators/repository_generator.dart b/lib/src/builder/generators/repository_generator.dart index 9b3e23d..197edd5 100644 --- a/lib/src/builder/generators/repository_generator.dart +++ b/lib/src/builder/generators/repository_generator.dart @@ -10,7 +10,7 @@ class RepositoryGenerator { String generateRepositories(BuilderState state) { return ''' extension Repositories on Database { - ${state.builders.values.map((b) => ' ${b.element.name}Repository get ${CaseStyle.camelCase.transform(b.tableName)} => ${b.element.name}Repository._(this);\n').join()} + ${state.builders.values.map((b) => ' ${b.element.name}Repository get ${CaseStyle.camelCase.transform(b.className)} => ${b.element.name}Repository._(this);\n').join()} } final registry = ModelRegistry({ diff --git a/lib/src/builder/table_builder.dart b/lib/src/builder/table_builder.dart index cd04ca4..1eb1c96 100644 --- a/lib/src/builder/table_builder.dart +++ b/lib/src/builder/table_builder.dart @@ -20,6 +20,7 @@ class TableBuilder { ConstantReader annotation; BuilderState state; + late String className; late String tableName; late FieldElement? primaryKeyParameter; late List views; @@ -29,6 +30,7 @@ class TableBuilder { TableBuilder(this.element, this.annotation, this.state) { tableName = _getTableName(); + className = _getClassName(); primaryKeyParameter = element.fields .where((p) => primaryKeyChecker.hasAnnotationOf(p) || primaryKeyChecker.hasAnnotationOf(p.getter ?? p)) @@ -56,6 +58,14 @@ class TableBuilder { } String _getTableName({bool singular = false}) { + if (!annotation.read('tableName').isNull) { + return annotation.read('tableName').stringValue; + } + + return _getClassName(singular: singular); + } + + String _getClassName({bool singular = false}) { var name = element.name; if (!singular) { if (element.name.endsWith('s')) { diff --git a/lib/src/core/annotations.dart b/lib/src/core/annotations.dart index 6860f0c..9051d34 100644 --- a/lib/src/core/annotations.dart +++ b/lib/src/core/annotations.dart @@ -4,12 +4,14 @@ import 'database.dart'; class Model { final List views; final List indexes; + final String? tableName; final dynamic insertRequestAnnotation; final dynamic updateRequestAnnotation; const Model({ this.views = const [], this.indexes = const [], + this.tableName, this.insertRequestAnnotation, this.updateRequestAnnotation, }); diff --git a/test/generation_test.dart b/test/generation_test.dart index 89b4a3c..6bdbb40 100644 --- a/test/generation_test.dart +++ b/test/generation_test.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:stormberry/stormberry.dart'; +import 'package:test/test.dart'; @Model() abstract class User { @@ -14,4 +17,34 @@ abstract class Account { String get id; } -void main() {} +@Model(tableName: 'customTableName') +abstract class LegacyAccount { + @PrimaryKey() + String get id; +} + +void main() { + group('generation', () { + test('generates schemas', () async { + var proc = await Process.start( + 'dart', + 'run build_runner build --delete-conflicting-outputs'.split(' '), + workingDirectory: '.', + ); + + proc.stdout.listen((e) => stdout.add(e)); + + expect(await proc.exitCode, equals(0)); + + var schema = File('test/generation_test.schema.g.dart'); + + expect(schema.existsSync(), equals(true)); + }, timeout: Timeout(Duration(seconds: 60))); + + test('Test custom table name generated code', () async { + final schema = File('test/generation_test.schema.g.dart'); + final content = await schema.readAsString(); + expect(content.contains('String get tableAlias => \'customTableName\';'), equals(true)); + }); + }); +} diff --git a/test/generation_test.schema.g.dart b/test/generation_test.schema.g.dart index 684531a..4dd8a2c 100644 --- a/test/generation_test.schema.g.dart +++ b/test/generation_test.schema.g.dart @@ -6,6 +6,7 @@ import 'generation_test.dart'; extension Repositories on Database { UserRepository get users => UserRepository._(this); AccountRepository get accounts => AccountRepository._(this); + LegacyAccountRepository get customTableName => LegacyAccountRepository._(this); } final registry = ModelRegistry({}); @@ -136,6 +137,69 @@ class _AccountRepository extends BaseRepository } } +abstract class LegacyAccountRepository + implements + ModelRepository, + ModelRepositoryInsert, + ModelRepositoryUpdate, + ModelRepositoryDelete { + factory LegacyAccountRepository._(Database db) = _LegacyAccountRepository; + + Future queryLegacyAccount(String id); + Future> queryLegacyAccounts([QueryParams? params]); +} + +class _LegacyAccountRepository extends BaseRepository + with + RepositoryInsertMixin, + RepositoryUpdateMixin, + RepositoryDeleteMixin + implements LegacyAccountRepository { + _LegacyAccountRepository(Database db) : super(db: db); + + @override + Future queryLegacyAccount(String id) { + return queryOne(id, LegacyAccountQueryable()); + } + + @override + Future> queryLegacyAccounts([QueryParams? params]) { + return queryMany(LegacyAccountQueryable(), params); + } + + @override + Future insert(Database db, List requests) async { + if (requests.isEmpty) return; + + await db.query( + 'INSERT INTO "customTableName" ( "id" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.id)} )').join(', ')}\n' + 'ON CONFLICT ( "id" ) DO UPDATE SET ', + ); + } + + @override + Future update(Database db, List requests) async { + if (requests.isEmpty) return; + await db.query( + 'UPDATE "customTableName"\n' + 'SET \n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)} )').join(', ')} )\n' + 'AS UPDATED("id")\n' + 'WHERE "customTableName"."id" = UPDATED."id"', + ); + } + + @override + Future delete(Database db, List keys) async { + if (keys.isEmpty) return; + await db.query( + 'DELETE FROM "customTableName"\n' + 'WHERE "customTableName"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} )', + ); + } +} + class UserInsertRequest { UserInsertRequest({required this.id, required this.name}); String id; @@ -147,6 +211,11 @@ class AccountInsertRequest { String id; } +class LegacyAccountInsertRequest { + LegacyAccountInsertRequest({required this.id}); + String id; +} + class UserUpdateRequest { UserUpdateRequest({required this.id, this.name}); String id; @@ -158,6 +227,11 @@ class AccountUpdateRequest { String id; } +class LegacyAccountUpdateRequest { + LegacyAccountUpdateRequest({required this.id}); + String id; +} + class UserQueryable extends KeyedViewQueryable { @override String get keyName => 'id'; @@ -206,3 +280,27 @@ class SuperSecretAccountView { final String id; } + +class LegacyAccountQueryable extends KeyedViewQueryable { + @override + String get keyName => 'id'; + + @override + String encodeKey(String key) => registry.encode(key); + + @override + String get tableName => 'custom_table_name_view'; + + @override + String get tableAlias => 'customTableName'; + + @override + LegacyAccount decode(TypedMap map) => LegacyAccountView(id: map.get('id', registry.decode)); +} + +class LegacyAccountView implements LegacyAccount { + LegacyAccountView({required this.id}); + + @override + final String id; +} From af2585e317e75c1353c2e77b2e4027ab1e04b24c Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Thu, 15 Sep 2022 00:27:25 +0200 Subject: [PATCH 10/14] fix optional foreign keys for update request --- .../builder/generators/update_generator.dart | 2 +- lib/src/builder/table_builder.dart | 2 +- test/generation_test.dart | 1 + test/generation_test.schema.g.dart | 51 +++++++++++++++---- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/lib/src/builder/generators/update_generator.dart b/lib/src/builder/generators/update_generator.dart index 202cbf5..f636117 100644 --- a/lib/src/builder/generators/update_generator.dart +++ b/lib/src/builder/generators/update_generator.dart @@ -105,7 +105,7 @@ class UpdateGenerator { (column == table.primaryKeyColumn ? '' : '?'), column.paramName)); } else if (column is ForeignColumnBuilder) { - var fieldNullSuffix = column.isNullable ? '?' : ''; + var fieldNullSuffix = column == table.primaryKeyColumn ? '' : '?'; String fieldType; if (column.linkBuilder.primaryKeyColumn == null) { fieldType = column.linkBuilder.element.name; diff --git a/lib/src/builder/table_builder.dart b/lib/src/builder/table_builder.dart index cd04ca4..75bd7fb 100644 --- a/lib/src/builder/table_builder.dart +++ b/lib/src/builder/table_builder.dart @@ -133,7 +133,7 @@ class TableBuilder { ReferencingColumnBuilder otherColumn; - if (selfHasKey && (otherParam == null || !otherParam.type.isDartCoreList)) { + if (selfHasKey && otherParam != null && !otherParam.type.isDartCoreList) { otherColumn = ForeignColumnBuilder(otherParam, this, otherBuilder, state); var insertIndex = otherBuilder.columns.lastIndexWhere((c) => c is ForeignColumnBuilder) + 1; otherBuilder.columns.insert(insertIndex, otherColumn); diff --git a/test/generation_test.dart b/test/generation_test.dart index 89b4a3c..ffef060 100644 --- a/test/generation_test.dart +++ b/test/generation_test.dart @@ -6,6 +6,7 @@ abstract class User { String get id; String get name; + Account get account; } @Model(views: [View('SuperSecret')]) diff --git a/test/generation_test.schema.g.dart b/test/generation_test.schema.g.dart index 684531a..ab434a2 100644 --- a/test/generation_test.schema.g.dart +++ b/test/generation_test.schema.g.dart @@ -45,9 +45,9 @@ class _UserRepository extends BaseRepository if (requests.isEmpty) return; await db.query( - 'INSERT INTO "users" ( "id", "name" )\n' - 'VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)} )').join(', ')}\n' - 'ON CONFLICT ( "id" ) DO UPDATE SET "name" = EXCLUDED."name"', + 'INSERT INTO "users" ( "id", "name", "account_id" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.accountId)} )').join(', ')}\n' + 'ON CONFLICT ( "id" ) DO UPDATE SET "name" = EXCLUDED."name", "account_id" = EXCLUDED."account_id"', ); } @@ -56,9 +56,9 @@ class _UserRepository extends BaseRepository if (requests.isEmpty) return; await db.query( 'UPDATE "users"\n' - 'SET "name" = COALESCE(UPDATED."name"::text, "users"."name")\n' - 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)} )').join(', ')} )\n' - 'AS UPDATED("id", "name")\n' + 'SET "name" = COALESCE(UPDATED."name"::text, "users"."name"), "account_id" = COALESCE(UPDATED."account_id"::text, "users"."account_id")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.accountId)} )').join(', ')} )\n' + 'AS UPDATED("id", "name", "account_id")\n' 'WHERE "users"."id" = UPDATED."id"', ); } @@ -137,9 +137,10 @@ class _AccountRepository extends BaseRepository } class UserInsertRequest { - UserInsertRequest({required this.id, required this.name}); + UserInsertRequest({required this.id, required this.name, required this.accountId}); String id; String name; + String accountId; } class AccountInsertRequest { @@ -148,9 +149,10 @@ class AccountInsertRequest { } class UserUpdateRequest { - UserUpdateRequest({required this.id, this.name}); + UserUpdateRequest({required this.id, this.name, this.accountId}); String id; String? name; + String? accountId; } class AccountUpdateRequest { @@ -172,16 +174,21 @@ class UserQueryable extends KeyedViewQueryable { String get tableAlias => 'users'; @override - User decode(TypedMap map) => UserView(id: map.get('id', registry.decode), name: map.get('name', registry.decode)); + User decode(TypedMap map) => UserView( + id: map.get('id', registry.decode), + name: map.get('name', registry.decode), + account: map.get('account', AccountQueryable().decoder)); } class UserView implements User { - UserView({required this.id, required this.name}); + UserView({required this.id, required this.name, required this.account}); @override final String id; @override final String name; + @override + final Account account; } class SuperSecretAccountViewQueryable extends KeyedViewQueryable { @@ -206,3 +213,27 @@ class SuperSecretAccountView { final String id; } + +class AccountQueryable extends KeyedViewQueryable { + @override + String get keyName => 'id'; + + @override + String encodeKey(String key) => registry.encode(key); + + @override + String get tableName => 'accounts_view'; + + @override + String get tableAlias => 'accounts'; + + @override + Account decode(TypedMap map) => AccountView(id: map.get('id', registry.decode)); +} + +class AccountView implements Account { + AccountView({required this.id}); + + @override + final String id; +} From be139331f814358f25a7f7ec55ee630de1c3138a Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Thu, 15 Sep 2022 01:42:41 +0200 Subject: [PATCH 11/14] improve tool with proper cli interface --- bin/stormberry.dart | 162 ++-------------- {bin/src => lib/src/cli}/differentiator.dart | 5 +- {bin/src => lib/src/cli}/inspector.dart | 2 +- {bin/src => lib/src/cli}/output.dart | 0 {bin/src => lib/src/cli}/patcher.dart | 2 +- lib/src/cli/runner.dart | 187 +++++++++++++++++++ {bin/src => lib/src/cli}/schema.dart | 0 pubspec.yaml | 1 + 8 files changed, 206 insertions(+), 153 deletions(-) rename {bin/src => lib/src/cli}/differentiator.dart (97%) rename {bin/src => lib/src/cli}/inspector.dart (99%) rename {bin/src => lib/src/cli}/output.dart (100%) rename {bin/src => lib/src/cli}/patcher.dart (99%) create mode 100644 lib/src/cli/runner.dart rename {bin/src => lib/src/cli}/schema.dart (100%) diff --git a/bin/stormberry.dart b/bin/stormberry.dart index b10d1a9..d547efe 100644 --- a/bin/stormberry.dart +++ b/bin/stormberry.dart @@ -1,155 +1,21 @@ -import 'dart:convert'; import 'dart:io'; -import 'dart:isolate'; - -import 'package:collection/collection.dart'; -import 'package:stormberry/stormberry.dart'; -import 'package:yaml/yaml.dart'; - -import 'src/differentiator.dart'; -import 'src/output.dart'; -import 'src/patcher.dart'; -import 'src/schema.dart'; +import 'package:args/command_runner.dart'; +import 'package:stormberry/src/cli/runner.dart'; Future main(List args) async { - bool dryRun = args.contains('--dry-run'); - String? dbName = args.where((a) => a.startsWith('-db=')).map((a) => a.split('=')[1]).firstOrNull; - String? output = args.where((a) => a.startsWith('-o=')).map((a) => a.split('=')[1]).firstOrNull; - bool applyChanges = args.contains('--apply-changes'); - - var pubspecYaml = File('pubspec.yaml'); - - if (!pubspecYaml.existsSync()) { - print('Cannot find pubspec.yaml file in current directory.'); + var runner = CommandRunner( + 'stormberry', + 'Tool for migrating your database to the schema generated from your models.', + )..addCommand(MigrateCommand()); + + try { + await runner.run(args); + exit(0); + } on UsageException catch (e) { + print('${e.message}\n${e.usage}'); exit(1); - } - - var packageName = loadYaml(await pubspecYaml.readAsString())['name'] as String; - - var buildYaml = File('build.yaml'); - - if (!buildYaml.existsSync()) { - print('Cannot find build.yaml file in current directory.'); + } catch (e, st) { + print('$e\n$st'); exit(1); } - - var content = loadYaml(await buildYaml.readAsString()); - - List? generateTargets = (content['targets'] as YamlMap?) - ?.values - .map((t) => t['builders']?['stormberry']) - .where((b) => b != null) - .expand((b) => b['generate_for'] as List) - .map((d) => d as String) - .toList(); - - if (generateTargets == null || generateTargets.isEmpty) { - print('Cannot find stormberry generation targets in build.yaml. ' - 'Make sure you have the stormberry builder configured with at least one generation target.'); - exit(1); - } - - var schema = DatabaseSchema.empty(); - - for (var target in generateTargets) { - var schemaPath = target.replaceFirst('.dart', '.runner.g.dart'); - var file = File('.dart_tool/build/generated/$packageName/$schemaPath'); - if (!file.existsSync()) { - print('Could not run migration for target $target. Did you run the build script?'); - exit(1); - } - - var port = ReceivePort(); - await Isolate.spawnUri( - file.absolute.uri, - [], - port.sendPort, - packageConfig: Uri.parse('.packages'), - ); - - var schemaMap = jsonDecode(await port.first as String); - var targetSchema = DatabaseSchema.fromMap(schemaMap as Map); - - schema = schema.mergeWith(targetSchema); - } - - if (dbName == null && Platform.environment['DB_NAME'] == null) { - stdout.write('Select a database to update: '); - dbName = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); - } - - var db = Database(database: dbName); - - await db.open(); - - print('Getting schema changes of ${db.name}'); - print('========================='); - - var diff = await getSchemaDiff(db, schema); - - if (diff.hasChanges) { - printDiff(diff); - print('========================='); - - if (dryRun) { - print('DATABASE SCHEME HAS CHANGES, EXITING'); - await db.close(); - exit(1); - } else { - if (output == null) { - await db.startTransaction(); - - String? answerApplyChanges; - if (!applyChanges) { - stdout.write('Do you want to apply these changes? (yes/no): '); - answerApplyChanges = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); - } - - if (applyChanges || answerApplyChanges == 'yes') { - print('Database schema changed, applying updates now:'); - - try { - db.debugPrint = true; - await patchSchema(db, diff); - } catch (_) { - db.cancelTransaction(); - } - } else { - db.cancelTransaction(); - } - - var updateWasSuccessFull = await db.finishTransaction(); - - print('========================'); - if (updateWasSuccessFull) { - print('---\nDATABASE UPDATE SUCCESSFUL'); - } else { - print('---\nALL CHANGES REVERTED, EXITING'); - } - - await db.close(); - exit(updateWasSuccessFull ? 0 : 1); - } else { - await db.close(); - var dir = Directory(output); - - String? answerApplyChanges; - if (!applyChanges) { - stdout.write('Do you want to write these migrations to ${dir.path}? (yes/no): '); - answerApplyChanges = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); - } - - if (applyChanges || answerApplyChanges == 'yes') { - - if (!dir.existsSync()) { - dir.createSync(recursive: true); - } - - await outputSchema(dir, diff); - } - } - } - } else { - print('NO CHANGES, ALL DONE'); - } } diff --git a/bin/src/differentiator.dart b/lib/src/cli/differentiator.dart similarity index 97% rename from bin/src/differentiator.dart rename to lib/src/cli/differentiator.dart index 2e88504..e72c30c 100644 --- a/bin/src/differentiator.dart +++ b/lib/src/cli/differentiator.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:stormberry/stormberry.dart'; +import '../../stormberry.dart'; import 'inspector.dart'; import 'schema.dart'; @@ -162,8 +162,7 @@ class TableSchemaDiff { TableSchemaDiff(this.name); - bool get hasChanges => - columns.hasChanges() || constraints.hasChanges() || indexes.hasChanges(); + bool get hasChanges => columns.hasChanges() || constraints.hasChanges() || indexes.hasChanges(); } class Diff { diff --git a/bin/src/inspector.dart b/lib/src/cli/inspector.dart similarity index 99% rename from bin/src/inspector.dart rename to lib/src/cli/inspector.dart index 8f699ce..a54971e 100644 --- a/bin/src/inspector.dart +++ b/lib/src/cli/inspector.dart @@ -1,4 +1,4 @@ -import 'package:stormberry/stormberry.dart'; +import '../../stormberry.dart'; import 'schema.dart'; diff --git a/bin/src/output.dart b/lib/src/cli/output.dart similarity index 100% rename from bin/src/output.dart rename to lib/src/cli/output.dart diff --git a/bin/src/patcher.dart b/lib/src/cli/patcher.dart similarity index 99% rename from bin/src/patcher.dart rename to lib/src/cli/patcher.dart index 42b2921..59aff35 100644 --- a/bin/src/patcher.dart +++ b/lib/src/cli/patcher.dart @@ -1,4 +1,4 @@ -import 'package:stormberry/stormberry.dart'; +import '../../stormberry.dart'; import 'differentiator.dart'; import 'schema.dart'; diff --git a/lib/src/cli/runner.dart b/lib/src/cli/runner.dart new file mode 100644 index 0000000..0db5c34 --- /dev/null +++ b/lib/src/cli/runner.dart @@ -0,0 +1,187 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:args/command_runner.dart'; +import 'package:yaml/yaml.dart'; + +import '../../stormberry.dart'; +import 'differentiator.dart'; +import 'output.dart'; +import 'patcher.dart'; +import 'schema.dart'; + +class MigrateCommand extends Command { + MigrateCommand() { + argParser.addFlag( + 'dry-run', + negatable: false, + help: 'Returns exit code 1 if there are any pending migrations. ' + 'Does not apply any changes to the database.', + ); + argParser.addOption('db', help: 'Set the database name.'); + + argParser.addOption( + 'output', + abbr: 'o', + help: 'Specify an output directory. This will write all migrations into .sql ' + 'files instead of writing to the database.', + ); + + argParser.addFlag( + 'apply-changes', + negatable: false, + help: 'Applies all changes to the database without asking for confirmation.', + ); + } + + @override + String get description => 'Migrates the database to the generated schema.'; + + @override + String get name => 'migrate'; + + @override + Future run() async { + bool dryRun = argResults!['dry-run'] as bool; + String? dbName = argResults!['db'] as String?; + String? output = argResults!['output'] as String?; + bool applyChanges = argResults!['apply-changes'] as bool; + + var pubspecYaml = File('pubspec.yaml'); + + if (!pubspecYaml.existsSync()) { + print('Cannot find pubspec.yaml file in current directory.'); + exit(1); + } + + var packageName = loadYaml(await pubspecYaml.readAsString())['name'] as String; + + var buildYaml = File('build.yaml'); + + if (!buildYaml.existsSync()) { + print('Cannot find build.yaml file in current directory.'); + exit(1); + } + + var content = loadYaml(await buildYaml.readAsString()); + + List? generateTargets = (content['targets'] as YamlMap?) + ?.values + .map((t) => t['builders']?['stormberry']) + .where((b) => b != null) + .expand((b) => b['generate_for'] as List) + .map((d) => d as String) + .toList(); + + if (generateTargets == null || generateTargets.isEmpty) { + print('Cannot find stormberry generation targets in build.yaml. ' + 'Make sure you have the stormberry builder configured with at least one generation target.'); + exit(1); + } + + var schema = DatabaseSchema.empty(); + + for (var target in generateTargets) { + var schemaPath = target.replaceFirst('.dart', '.runner.g.dart'); + var file = File('.dart_tool/build/generated/$packageName/$schemaPath'); + if (!file.existsSync()) { + print('Could not run migration for target $target. Did you run the build script?'); + exit(1); + } + + var port = ReceivePort(); + await Isolate.spawnUri( + file.absolute.uri, + [], + port.sendPort, + packageConfig: Uri.parse('.dart_tool/package_config.json'), + //automaticPackageResolution: true, + ); + + var schemaMap = jsonDecode(await port.first as String); + var targetSchema = DatabaseSchema.fromMap(schemaMap as Map); + + schema = schema.mergeWith(targetSchema); + } + + if (dbName == null && Platform.environment['DB_NAME'] == null) { + stdout.write('Select a database to update: '); + dbName = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); + } + + var db = Database(database: dbName); + + await db.open(); + + print('Getting schema changes of ${db.name}'); + print('========================='); + + var diff = await getSchemaDiff(db, schema); + + if (diff.hasChanges) { + printDiff(diff); + print('========================='); + + if (dryRun) { + print('DATABASE SCHEME HAS CHANGES, EXITING'); + await db.close(); + exit(1); + } else { + if (output == null) { + await db.startTransaction(); + + String? answerApplyChanges; + if (!applyChanges) { + stdout.write('Do you want to apply these changes? (yes/no): '); + answerApplyChanges = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); + } + + if (applyChanges || answerApplyChanges == 'yes') { + print('Database schema changed, applying updates now:'); + + try { + db.debugPrint = true; + await patchSchema(db, diff); + } catch (_) { + db.cancelTransaction(); + } + } else { + db.cancelTransaction(); + } + + var updateWasSuccessFull = await db.finishTransaction(); + + print('========================'); + if (updateWasSuccessFull) { + print('---\nDATABASE UPDATE SUCCESSFUL'); + } else { + print('---\nALL CHANGES REVERTED, EXITING'); + } + + await db.close(); + exit(updateWasSuccessFull ? 0 : 1); + } else { + await db.close(); + var dir = Directory(output); + + String? answerApplyChanges; + if (!applyChanges) { + stdout.write('Do you want to write these migrations to ${dir.path}? (yes/no): '); + answerApplyChanges = stdin.readLineSync(encoding: Encoding.getByName('utf-8')!); + } + + if (applyChanges || answerApplyChanges == 'yes') { + if (!dir.existsSync()) { + dir.createSync(recursive: true); + } + + await outputSchema(dir, diff); + } + } + } + } else { + print('NO CHANGES, ALL DONE'); + } + } +} diff --git a/bin/src/schema.dart b/lib/src/cli/schema.dart similarity index 100% rename from bin/src/schema.dart rename to lib/src/cli/schema.dart diff --git a/pubspec.yaml b/pubspec.yaml index 647dc94..ee39c1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,6 +6,7 @@ issue_tracker: https://github.com/schultek/stormberry/issues dependencies: analyzer: ^3.3.1 + args: ^2.3.1 build: ^2.2.1 collection: ^1.16.0 crypto: ^3.0.1 From 469b62aabd193315dbca1133ca6df10181277a25 Mon Sep 17 00:00:00 2001 From: Tim Whiting Date: Wed, 14 Sep 2022 14:14:24 -0600 Subject: [PATCH 12/14] update to latest packages (except analyzer), add .vscode folder to gitignore --- .gitignore | 1 + example/pubspec.yaml | 4 ++-- pubspec.yaml | 20 +++++++++---------- .../multi_schema/lib/modelsA.schema.g.dart | 2 +- .../multi_schema/lib/modelsB.schema.g.dart | 2 +- test/packages/multi_schema/pubspec.yaml | 4 ++-- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 589a652..ee5a63c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .dart_tool/ .packages build/ +.vscode/ pubspec.lock diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bec7009..353ed68 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -8,5 +8,5 @@ dependencies: path: ../ dev_dependencies: - build_runner: ^2.1.7 - lints: ^1.0.1 \ No newline at end of file + build_runner: ^2.2.1 + lints: ^2.0.0 \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index ee39c1f..784e58d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,21 +5,21 @@ repository: https://github.com/schultek/stormberry issue_tracker: https://github.com/schultek/stormberry/issues dependencies: - analyzer: ^3.3.1 + analyzer: ">=4.2.0 <5.0.0" args: ^2.3.1 - build: ^2.2.1 + build: ^2.3.1 collection: ^1.16.0 - crypto: ^3.0.1 - dart_style: ^2.2.2 + crypto: ^3.0.2 + dart_style: ^2.2.4 path: ^1.8.2 - postgres: ^2.4.3 - source_gen: ^1.2.1 - yaml: ^3.1.0 + postgres: ^2.5.1 + source_gen: ^1.2.3 + yaml: ^3.1.1 dev_dependencies: build_runner: ^2.2.0 - lints: ^1.0.1 - test: ^1.21.1 + lints: ^2.0.0 + test: ^1.21.6 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.16.0 <3.0.0' diff --git a/test/packages/multi_schema/lib/modelsA.schema.g.dart b/test/packages/multi_schema/lib/modelsA.schema.g.dart index 4f2ab3d..af2df4f 100644 --- a/test/packages/multi_schema/lib/modelsA.schema.g.dart +++ b/test/packages/multi_schema/lib/modelsA.schema.g.dart @@ -28,10 +28,10 @@ class _ModelARepository extends BaseRepository @override Future insert(Database db, List requests) async { if (requests.isEmpty) return; + await db.query(""" INSERT INTO "model_as" ( "data" ) VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} - """); } diff --git a/test/packages/multi_schema/lib/modelsB.schema.g.dart b/test/packages/multi_schema/lib/modelsB.schema.g.dart index 553195a..8a276db 100644 --- a/test/packages/multi_schema/lib/modelsB.schema.g.dart +++ b/test/packages/multi_schema/lib/modelsB.schema.g.dart @@ -28,10 +28,10 @@ class _ModelBRepository extends BaseRepository @override Future insert(Database db, List requests) async { if (requests.isEmpty) return; + await db.query(""" INSERT INTO "model_bs" ( "data" ) VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} - """); } diff --git a/test/packages/multi_schema/pubspec.yaml b/test/packages/multi_schema/pubspec.yaml index 7529fcc..df95241 100644 --- a/test/packages/multi_schema/pubspec.yaml +++ b/test/packages/multi_schema/pubspec.yaml @@ -8,5 +8,5 @@ dependencies: path: ../../../ dev_dependencies: - build_runner: ^2.1.7 - lints: ^1.0.1 \ No newline at end of file + build_runner: ^2.2.1 + lints: ^2.0.0 \ No newline at end of file From 682ef262124a7d427e89f1a75f23946f6593c824 Mon Sep 17 00:00:00 2001 From: Tim Whiting Date: Wed, 14 Sep 2022 18:19:41 -0600 Subject: [PATCH 13/14] update generated files --- .../multi_schema/lib/modelsA.schema.g.dart | 41 +++++----- .../multi_schema/lib/modelsB.schema.g.dart | 41 +++++----- .../serialization/lib/models.mapper.g.dart | 28 +++---- .../serialization/lib/models.schema.g.dart | 77 +++++++++---------- 4 files changed, 94 insertions(+), 93 deletions(-) diff --git a/test/packages/multi_schema/lib/modelsA.schema.g.dart b/test/packages/multi_schema/lib/modelsA.schema.g.dart index af2df4f..5ce47b5 100644 --- a/test/packages/multi_schema/lib/modelsA.schema.g.dart +++ b/test/packages/multi_schema/lib/modelsA.schema.g.dart @@ -1,6 +1,7 @@ // ignore_for_file: prefer_relative_imports import 'package:stormberry/internals.dart'; -import 'package:multi_schema_test/modelsA.dart'; + +import 'modelsA.dart'; extension Repositories on Database { ModelARepository get modelAs => ModelARepository._(this); @@ -12,7 +13,7 @@ abstract class ModelARepository implements ModelRepository, ModelRepositoryInsert, ModelRepositoryUpdate { factory ModelARepository._(Database db) = _ModelARepository; - Future> queryViewaViews([QueryParams? params]); + Future> queryViewAViews([QueryParams? params]); } class _ModelARepository extends BaseRepository @@ -21,30 +22,30 @@ class _ModelARepository extends BaseRepository _ModelARepository(Database db) : super(db: db); @override - Future> queryViewaViews([QueryParams? params]) { - return queryMany(ViewaModelAViewQueryable(), params); + Future> queryViewAViews([QueryParams? params]) { + return queryMany(ViewAModelAViewQueryable(), params); } @override Future insert(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - INSERT INTO "model_as" ( "data" ) - VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} - """); + await db.query( + 'INSERT INTO "model_as" ( "data" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')}\n', + ); } @override Future update(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - UPDATE "model_as" - SET "data" = COALESCE(UPDATED."data"::text, "model_as"."data") - FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} ) - AS UPDATED("data") - WHERE - """); + await db.query( + 'UPDATE "model_as"\n' + 'SET "data" = COALESCE(UPDATED."data"::text, "model_as"."data")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} )\n' + 'AS UPDATED("data")\n' + 'WHERE ', + ); } } @@ -58,17 +59,17 @@ class ModelAUpdateRequest { String? data; } -class ViewaModelAViewQueryable extends ViewQueryable { +class ViewAModelAViewQueryable extends ViewQueryable { @override - String get tableName => 'viewa_model_as_view'; + String get tableName => 'view_a_model_as_view'; @override String get tableAlias => 'model_as'; @override - ViewaModelAView decode(TypedMap map) => ViewaModelAView(); + ViewAModelAView decode(TypedMap map) => ViewAModelAView(); } -class ViewaModelAView { - ViewaModelAView(); +class ViewAModelAView { + ViewAModelAView(); } diff --git a/test/packages/multi_schema/lib/modelsB.schema.g.dart b/test/packages/multi_schema/lib/modelsB.schema.g.dart index 8a276db..21f47f5 100644 --- a/test/packages/multi_schema/lib/modelsB.schema.g.dart +++ b/test/packages/multi_schema/lib/modelsB.schema.g.dart @@ -1,6 +1,7 @@ // ignore_for_file: prefer_relative_imports import 'package:stormberry/internals.dart'; -import 'package:multi_schema_test/modelsB.dart'; + +import 'modelsB.dart'; extension Repositories on Database { ModelBRepository get modelBs => ModelBRepository._(this); @@ -12,7 +13,7 @@ abstract class ModelBRepository implements ModelRepository, ModelRepositoryInsert, ModelRepositoryUpdate { factory ModelBRepository._(Database db) = _ModelBRepository; - Future> queryViewbViews([QueryParams? params]); + Future> queryViewBViews([QueryParams? params]); } class _ModelBRepository extends BaseRepository @@ -21,30 +22,30 @@ class _ModelBRepository extends BaseRepository _ModelBRepository(Database db) : super(db: db); @override - Future> queryViewbViews([QueryParams? params]) { - return queryMany(ViewbModelBViewQueryable(), params); + Future> queryViewBViews([QueryParams? params]) { + return queryMany(ViewBModelBViewQueryable(), params); } @override Future insert(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - INSERT INTO "model_bs" ( "data" ) - VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} - """); + await db.query( + 'INSERT INTO "model_bs" ( "data" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')}\n', + ); } @override Future update(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - UPDATE "model_bs" - SET "data" = COALESCE(UPDATED."data"::text, "model_bs"."data") - FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} ) - AS UPDATED("data") - WHERE - """); + await db.query( + 'UPDATE "model_bs"\n' + 'SET "data" = COALESCE(UPDATED."data"::text, "model_bs"."data")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.data)} )').join(', ')} )\n' + 'AS UPDATED("data")\n' + 'WHERE ', + ); } } @@ -58,17 +59,17 @@ class ModelBUpdateRequest { String? data; } -class ViewbModelBViewQueryable extends ViewQueryable { +class ViewBModelBViewQueryable extends ViewQueryable { @override - String get tableName => 'viewb_model_bs_view'; + String get tableName => 'view_b_model_bs_view'; @override String get tableAlias => 'model_bs'; @override - ViewbModelBView decode(TypedMap map) => ViewbModelBView(); + ViewBModelBView decode(TypedMap map) => ViewBModelBView(); } -class ViewbModelBView { - ViewbModelBView(); +class ViewBModelBView { + ViewBModelBView(); } diff --git a/test/packages/serialization/lib/models.mapper.g.dart b/test/packages/serialization/lib/models.mapper.g.dart index e1f8875..d89f90a 100644 --- a/test/packages/serialization/lib/models.mapper.g.dart +++ b/test/packages/serialization/lib/models.mapper.g.dart @@ -25,15 +25,15 @@ class UserInsertRequestMapper extends BaseMapper { @override Function get decoder => decode; UserInsertRequest decode(dynamic v) => checked(v, (Map map) => fromMap(map)); - UserInsertRequest fromMap(Map map) => UserInsertRequest(companyId: Mapper.i.$getOpt(map, 'companyId'), id: Mapper.i.$get(map, 'id'), name: Mapper.i.$get(map, 'name'), securityNumber: Mapper.i.$get(map, 'securityNumber')); + UserInsertRequest fromMap(Map map) => UserInsertRequest(id: Mapper.i.$get(map, 'id'), name: Mapper.i.$get(map, 'name'), securityNumber: Mapper.i.$get(map, 'securityNumber')); @override Function get encoder => (UserInsertRequest v) => encode(v); dynamic encode(UserInsertRequest v) => toMap(v); - Map toMap(UserInsertRequest u) => {'companyId': Mapper.i.$enc(u.companyId, 'companyId'), 'id': Mapper.i.$enc(u.id, 'id'), 'name': Mapper.i.$enc(u.name, 'name'), 'securityNumber': Mapper.i.$enc(u.securityNumber, 'securityNumber')}; + Map toMap(UserInsertRequest u) => {'id': Mapper.i.$enc(u.id, 'id'), 'name': Mapper.i.$enc(u.name, 'name'), 'securityNumber': Mapper.i.$enc(u.securityNumber, 'securityNumber')}; - @override String stringify(UserInsertRequest self) => 'UserInsertRequest(companyId: ${Mapper.asString(self.companyId)}, id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)}, securityNumber: ${Mapper.asString(self.securityNumber)})'; - @override int hash(UserInsertRequest self) => Mapper.hash(self.companyId) ^ Mapper.hash(self.id) ^ Mapper.hash(self.name) ^ Mapper.hash(self.securityNumber); - @override bool equals(UserInsertRequest self, UserInsertRequest other) => Mapper.isEqual(self.companyId, other.companyId) && Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name) && Mapper.isEqual(self.securityNumber, other.securityNumber); + @override String stringify(UserInsertRequest self) => 'UserInsertRequest(id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)}, securityNumber: ${Mapper.asString(self.securityNumber)})'; + @override int hash(UserInsertRequest self) => Mapper.hash(self.id) ^ Mapper.hash(self.name) ^ Mapper.hash(self.securityNumber); + @override bool equals(UserInsertRequest self, UserInsertRequest other) => Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name) && Mapper.isEqual(self.securityNumber, other.securityNumber); @override Function get typeFactory => (f) => f(); } @@ -46,14 +46,14 @@ extension UserInsertRequestMapperExtension on UserInsertRequest { abstract class UserInsertRequestCopyWith<$R> { factory UserInsertRequestCopyWith(UserInsertRequest value, Then then) = _UserInsertRequestCopyWithImpl<$R>; - $R call({String? companyId, String? id, String? name, String? securityNumber}); + $R call({String? id, String? name, String? securityNumber}); $R apply(UserInsertRequest Function(UserInsertRequest) transform); } class _UserInsertRequestCopyWithImpl<$R> extends BaseCopyWith implements UserInsertRequestCopyWith<$R> { _UserInsertRequestCopyWithImpl(UserInsertRequest value, Then then) : super(value, then); - @override $R call({Object? companyId = $none, String? id, String? name, String? securityNumber}) => $then(UserInsertRequest(companyId: or(companyId, $value.companyId), id: id ?? $value.id, name: name ?? $value.name, securityNumber: securityNumber ?? $value.securityNumber)); + @override $R call({String? id, String? name, String? securityNumber}) => $then(UserInsertRequest(id: id ?? $value.id, name: name ?? $value.name, securityNumber: securityNumber ?? $value.securityNumber)); } class UserUpdateRequestMapper extends BaseMapper { @@ -61,15 +61,15 @@ class UserUpdateRequestMapper extends BaseMapper { @override Function get decoder => decode; UserUpdateRequest decode(dynamic v) => checked(v, (Map map) => fromMap(map)); - UserUpdateRequest fromMap(Map map) => UserUpdateRequest(companyId: Mapper.i.$getOpt(map, 'companyId'), id: Mapper.i.$get(map, 'id'), name: Mapper.i.$getOpt(map, 'name'), securityNumber: Mapper.i.$getOpt(map, 'securityNumber')); + UserUpdateRequest fromMap(Map map) => UserUpdateRequest(id: Mapper.i.$get(map, 'id'), name: Mapper.i.$getOpt(map, 'name'), securityNumber: Mapper.i.$getOpt(map, 'securityNumber')); @override Function get encoder => (UserUpdateRequest v) => encode(v); dynamic encode(UserUpdateRequest v) => toMap(v); - Map toMap(UserUpdateRequest u) => {'companyId': Mapper.i.$enc(u.companyId, 'companyId'), 'id': Mapper.i.$enc(u.id, 'id'), 'name': Mapper.i.$enc(u.name, 'name'), 'securityNumber': Mapper.i.$enc(u.securityNumber, 'securityNumber')}; + Map toMap(UserUpdateRequest u) => {'id': Mapper.i.$enc(u.id, 'id'), 'name': Mapper.i.$enc(u.name, 'name'), 'securityNumber': Mapper.i.$enc(u.securityNumber, 'securityNumber')}; - @override String stringify(UserUpdateRequest self) => 'UserUpdateRequest(companyId: ${Mapper.asString(self.companyId)}, id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)}, securityNumber: ${Mapper.asString(self.securityNumber)})'; - @override int hash(UserUpdateRequest self) => Mapper.hash(self.companyId) ^ Mapper.hash(self.id) ^ Mapper.hash(self.name) ^ Mapper.hash(self.securityNumber); - @override bool equals(UserUpdateRequest self, UserUpdateRequest other) => Mapper.isEqual(self.companyId, other.companyId) && Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name) && Mapper.isEqual(self.securityNumber, other.securityNumber); + @override String stringify(UserUpdateRequest self) => 'UserUpdateRequest(id: ${Mapper.asString(self.id)}, name: ${Mapper.asString(self.name)}, securityNumber: ${Mapper.asString(self.securityNumber)})'; + @override int hash(UserUpdateRequest self) => Mapper.hash(self.id) ^ Mapper.hash(self.name) ^ Mapper.hash(self.securityNumber); + @override bool equals(UserUpdateRequest self, UserUpdateRequest other) => Mapper.isEqual(self.id, other.id) && Mapper.isEqual(self.name, other.name) && Mapper.isEqual(self.securityNumber, other.securityNumber); @override Function get typeFactory => (f) => f(); } @@ -82,14 +82,14 @@ extension UserUpdateRequestMapperExtension on UserUpdateRequest { abstract class UserUpdateRequestCopyWith<$R> { factory UserUpdateRequestCopyWith(UserUpdateRequest value, Then then) = _UserUpdateRequestCopyWithImpl<$R>; - $R call({String? companyId, String? id, String? name, String? securityNumber}); + $R call({String? id, String? name, String? securityNumber}); $R apply(UserUpdateRequest Function(UserUpdateRequest) transform); } class _UserUpdateRequestCopyWithImpl<$R> extends BaseCopyWith implements UserUpdateRequestCopyWith<$R> { _UserUpdateRequestCopyWithImpl(UserUpdateRequest value, Then then) : super(value, then); - @override $R call({Object? companyId = $none, String? id, Object? name = $none, Object? securityNumber = $none}) => $then(UserUpdateRequest(companyId: or(companyId, $value.companyId), id: id ?? $value.id, name: or(name, $value.name), securityNumber: or(securityNumber, $value.securityNumber))); + @override $R call({String? id, Object? name = $none, Object? securityNumber = $none}) => $then(UserUpdateRequest(id: id ?? $value.id, name: or(name, $value.name), securityNumber: or(securityNumber, $value.securityNumber))); } class DefaultUserViewMapper extends BaseMapper { diff --git a/test/packages/serialization/lib/models.schema.g.dart b/test/packages/serialization/lib/models.schema.g.dart index 4da80ca..9ccaa35 100644 --- a/test/packages/serialization/lib/models.schema.g.dart +++ b/test/packages/serialization/lib/models.schema.g.dart @@ -1,6 +1,7 @@ // ignore_for_file: prefer_relative_imports import 'package:stormberry/internals.dart'; -import 'package:serialization_test/models.dart'; + +import 'models.dart'; extension Repositories on Database { UserRepository get users => UserRepository._(this); @@ -55,32 +56,32 @@ class _UserRepository extends BaseRepository Future insert(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - INSERT INTO "users" ( "company_id", "id", "name", "security_number" ) - VALUES ${requests.map((r) => '( ${registry.encode(r.companyId)}, ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.securityNumber)} )').join(', ')} -ON CONFLICT ( "id" ) DO UPDATE SET "company_id" = EXCLUDED."company_id", "name" = EXCLUDED."name", "security_number" = EXCLUDED."security_number" - """); + await db.query( + 'INSERT INTO "users" ( "id", "name", "security_number" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.securityNumber)} )').join(', ')}\n' + 'ON CONFLICT ( "id" ) DO UPDATE SET "name" = EXCLUDED."name", "security_number" = EXCLUDED."security_number"', + ); } @override Future update(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - UPDATE "users" - SET "company_id" = COALESCE(UPDATED."company_id"::text, "users"."company_id"), "name" = COALESCE(UPDATED."name"::text, "users"."name"), "security_number" = COALESCE(UPDATED."security_number"::text, "users"."security_number") - FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.companyId)}, ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.securityNumber)} )').join(', ')} ) - AS UPDATED("company_id", "id", "name", "security_number") - WHERE "users"."id" = UPDATED."id" - """); + await db.query( + 'UPDATE "users"\n' + 'SET "name" = COALESCE(UPDATED."name"::text, "users"."name"), "security_number" = COALESCE(UPDATED."security_number"::text, "users"."security_number")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.name)}, ${registry.encode(r.securityNumber)} )').join(', ')} )\n' + 'AS UPDATED("id", "name", "security_number")\n' + 'WHERE "users"."id" = UPDATED."id"', + ); } @override Future delete(Database db, List keys) async { if (keys.isEmpty) return; - await db.query(""" - DELETE FROM "users" - WHERE "users"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} ) - """); + await db.query( + 'DELETE FROM "users"\n' + 'WHERE "users"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} )', + ); } } @@ -118,39 +119,38 @@ class _CompanyRepository extends BaseRepository Future insert(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - INSERT INTO "companies" ( "id", "member_id" ) - VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.memberId)} )').join(', ')} -ON CONFLICT ( "id" ) DO UPDATE SET "member_id" = EXCLUDED."member_id" - """); + await db.query( + 'INSERT INTO "companies" ( "id", "member_id" )\n' + 'VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.memberId)} )').join(', ')}\n' + 'ON CONFLICT ( "id" ) DO UPDATE SET "member_id" = EXCLUDED."member_id"', + ); } @override Future update(Database db, List requests) async { if (requests.isEmpty) return; - await db.query(""" - UPDATE "companies" - SET "member_id" = COALESCE(UPDATED."member_id"::text, "companies"."member_id") - FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.memberId)} )').join(', ')} ) - AS UPDATED("id", "member_id") - WHERE "companies"."id" = UPDATED."id" - """); + await db.query( + 'UPDATE "companies"\n' + 'SET "member_id" = COALESCE(UPDATED."member_id"::text, "companies"."member_id")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${registry.encode(r.id)}, ${registry.encode(r.memberId)} )').join(', ')} )\n' + 'AS UPDATED("id", "member_id")\n' + 'WHERE "companies"."id" = UPDATED."id"', + ); } @override Future delete(Database db, List keys) async { if (keys.isEmpty) return; - await db.query(""" - DELETE FROM "companies" - WHERE "companies"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} ) - """); + await db.query( + 'DELETE FROM "companies"\n' + 'WHERE "companies"."id" IN ( ${keys.map((k) => registry.encode(k)).join(',')} )', + ); } } @MappableClass() class UserInsertRequest { - UserInsertRequest({this.companyId, required this.id, required this.name, required this.securityNumber}); - String? companyId; + UserInsertRequest({required this.id, required this.name, required this.securityNumber}); String id; String name; String securityNumber; @@ -164,17 +164,16 @@ class CompanyInsertRequest { @MappableClass() class UserUpdateRequest { - UserUpdateRequest({this.companyId, required this.id, this.name, this.securityNumber}); - String? companyId; + UserUpdateRequest({required this.id, this.name, this.securityNumber}); String id; String? name; String? securityNumber; } class CompanyUpdateRequest { - CompanyUpdateRequest({required this.id, required this.memberId}); + CompanyUpdateRequest({required this.id, this.memberId}); String id; - String memberId; + String? memberId; } class DefaultUserViewQueryable extends KeyedViewQueryable { From cf69f4a996514ce2f3a1c642a35306705b93ccaa Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Thu, 15 Sep 2022 11:49:57 +0200 Subject: [PATCH 14/14] [bump] version 0.8.0 --- CHANGELOG.md | 2 +- README.md | 7 ++++--- pubspec.yaml | 4 ++-- test/multi_schema_test.dart | 2 +- test/serialization_test.dart | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e721e39..ef74ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # 0.8.0 - Added support for serialization through custom annotations -- Added support for manual migration output to .sql files +- Improved migration cli and added support for manual migration output to .sql files - Fixed bug with default views and value decoding # 0.7.0 diff --git a/README.md b/README.md index 206ccf9..634aed2 100644 --- a/README.md +++ b/README.md @@ -392,7 +392,7 @@ await db.users.updateOne(UserUpdateRequest(id: 'abc', name: 'Tom')); You can specify a custom query with custom sql by extending the `Query` class. You will then need to implement the `Future apply(Database db, U params)` method. -Additionally to the model tabels, you can query the model views to automatically get all resolved +Additionally to the model tables, you can query the model views to automatically get all resolved relations without needing to do manual joins. Table names are always plural, e.g. `users` and view names are in the format as `complete_user_view`. @@ -408,7 +408,7 @@ Stormberry comes with a database migration tool, to create or update the schema To use this run the following command from the root folder of your project. ``` -dart pub run stormberry +dart pub run stormberry migrate ``` In order to connect to your database, provide the following environment variables: @@ -419,7 +419,8 @@ confirmation before applying the changes or aborting. The tool supported the following options: -- `-db=`: Specify the database name. Tool will ask if not specified. +- `-h`: Shows the available options. +- `--db=`: Specify the database name. Tool will ask if not specified. - `--dry-run`: Logs any changes to the schema without writing to the database, and exists with code 1 if there are any. - `--apply-changes`: Apply any changes without asking for confirmation. diff --git a/pubspec.yaml b/pubspec.yaml index 784e58d..6e5198c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: stormberry description: Access your postgres database effortlessly from dart code. -version: 0.7.0 +version: 0.8.0 repository: https://github.com/schultek/stormberry issue_tracker: https://github.com/schultek/stormberry/issues @@ -19,7 +19,7 @@ dependencies: dev_dependencies: build_runner: ^2.2.0 lints: ^2.0.0 - test: ^1.21.6 + test: ^1.21.0 environment: sdk: '>=2.16.0 <3.0.0' diff --git a/test/multi_schema_test.dart b/test/multi_schema_test.dart index e04f4d4..faf011f 100644 --- a/test/multi_schema_test.dart +++ b/test/multi_schema_test.dart @@ -25,7 +25,7 @@ void main() { test('Migrating schemas', () async { var proc = await Process.start( 'dart', - 'run stormberry --apply-changes'.split(' '), + 'run stormberry migrate --apply-changes'.split(' '), workingDirectory: 'test/packages/multi_schema', environment: { 'DB_HOST': 'localhost', diff --git a/test/serialization_test.dart b/test/serialization_test.dart index c8c6efb..55662dd 100644 --- a/test/serialization_test.dart +++ b/test/serialization_test.dart @@ -38,7 +38,7 @@ void main() { expect(lines[0], equals('{"id":"abc","name":"Tom","securityNumber":"12345"}')); expect(lines[1], equals('DefaultUserView(id: abc, name: Alex, securityNumber: 12345)')); expect(lines[2], equals('{"id":"01","member":{"id":"def","name":"Susan"}}')); - expect(lines[3], equals('{"companyId":null,"id":"abc","name":null,"securityNumber":"007"}')); + expect(lines[3], equals('{"id":"abc","name":null,"securityNumber":"007"}')); expect(lines[4], equals('')); }, timeout: Timeout(Duration(seconds: 60)));