Skip to content

Commit

Permalink
Merge pull request #16 from schultek/develop
Browse files Browse the repository at this point in the history
version 0.8.0
  • Loading branch information
schultek authored Sep 15, 2022
2 parents da1241f + cf69f4a commit 0b04aa9
Show file tree
Hide file tree
Showing 45 changed files with 2,008 additions and 404 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.dart_tool/
.packages
build/
.vscode/

pubspec.lock

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.8.0

- Added support for serialization through custom annotations
- Improved migration cli and 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
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -373,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<T, U>` class.
You will then need to implement the `Future<T> 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`.

Expand All @@ -389,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:
Expand All @@ -400,7 +419,10 @@ confirmation before applying the changes or aborting.

The tool supported the following options:

- `-db=<db_name>`: Specify the database name. Tool will ask if not specified.
- `-h`: Shows the available options.
- `--db=<db_name>`: 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.
- `-o=<folder>`: 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.
140 changes: 14 additions & 126 deletions bin/stormberry.dart
Original file line number Diff line number Diff line change
@@ -1,133 +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/patcher.dart';
import 'src/schema.dart';
import 'package:args/command_runner.dart';
import 'package:stormberry/src/cli/runner.dart';

Future<void> main(List<String> args) async {
bool dryRun = args.contains('--dry-run');
String? dbName = args.where((a) => a.startsWith('-db=')).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<void>(
'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<String>? 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<String, dynamic>);

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 {
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 {
print('NO CHANGES, ALL DONE');
}
}
8 changes: 7 additions & 1 deletion build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ builders:
post_process_builders:
output_cleanup:
import: "package:stormberry/builder.dart"
builder_factory: "outputCleanup"
builder_factory: "outputCleanup"
targets:
$default:
builders:
stormberry:
generate_for:
- test/generation_test.dart
42 changes: 42 additions & 0 deletions example/migrations/0_create_tables.sql
Original file line number Diff line number Diff line change
@@ -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
);
35 changes: 35 additions & 0 deletions example/migrations/1_alter_tables.sql
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 0b04aa9

Please sign in to comment.