diff --git a/api/bin/api.dart b/api/bin/api.dart index c02700253cef26f432a6ab7c67b70bd6389421db..c3a10e0dccf78f953ffedea14cec59bee4220381 100644 --- a/api/bin/api.dart +++ b/api/bin/api.dart @@ -107,12 +107,16 @@ void main(List<String> arguments) async { }); // Migrations + migrationsLogger.info("Starting migrations"); + await migratePostgres( postgresEndpoint, migrationsLogger, sslMode: databaseSslMode ); + migrationsLogger.info("Successfully migrated"); + // Factories var userAuthenticationResponseFactory = JwtAuthenticationResponseFactory(); diff --git a/api/lib/converters/angel3/point_converter.dart b/api/lib/converters/angel3/point_converter.dart index 6600c65a5e89bd89e368cfbb49d86e00d2a25439..a8dba513a06834c9a68a0a84c507d034667550d1 100644 --- a/api/lib/converters/angel3/point_converter.dart +++ b/api/lib/converters/angel3/point_converter.dart @@ -1,4 +1,4 @@ -import 'package:api/models/databases/angel3/point.dart'; +import 'package:api/models/databases/angel3/models.dart'; import 'package:api/models/domain/point.dart' as dm; extension Angel3PointConverter on Point { diff --git a/api/lib/converters/angel3/user_converter.dart b/api/lib/converters/angel3/user_converter.dart index 6f979e9d8f64f1e163fb705c0f6dc46e93006766..f328eb12e88e51f182aa74026265dbf63d0df2be 100644 --- a/api/lib/converters/angel3/user_converter.dart +++ b/api/lib/converters/angel3/user_converter.dart @@ -1,4 +1,4 @@ -import 'package:api/models/databases/angel3/user.dart'; +import 'package:api/models/databases/angel3/models.dart'; import 'package:api/models/domain/user.dart' as dm; extension Angel3UserConverter on User { diff --git a/api/lib/databases/angel3/angel3_point_database.dart b/api/lib/databases/angel3/angel3_point_database.dart index 4b83c4c53221a4bc3271700930238dbfe72a6275..9f2be38961fcfd7165766b696110999f282798e9 100644 --- a/api/lib/databases/angel3/angel3_point_database.dart +++ b/api/lib/databases/angel3/angel3_point_database.dart @@ -1,7 +1,7 @@ import 'package:angel3_orm/angel3_orm.dart'; import 'package:api/converters/angel3/point_converter.dart'; import 'package:api/databases/interfaces/point_database.dart'; -import 'package:api/models/databases/angel3/point.dart'; +import 'package:api/models/databases/angel3/models.dart'; import 'package:api/models/domain/point.dart' as dm; import 'package:uuid/uuid.dart'; @@ -23,6 +23,9 @@ class Angel3PointDatabase implements PointDatabase<String> { ..createdAt = currentTime ..updatedAt = currentTime; + // var t = CarQuery(); + // t.where!.model + var insertedPoint = await query.insert(_executor); return insertedPoint.value.toDomainPoint(); } diff --git a/api/lib/databases/angel3/angel3_user_database.dart b/api/lib/databases/angel3/angel3_user_database.dart index 1442286a82df3ed2a22fe378ede51d45fae65c0f..3a3dc49cccb31006860ad60420bcbea43fd866e4 100644 --- a/api/lib/databases/angel3/angel3_user_database.dart +++ b/api/lib/databases/angel3/angel3_user_database.dart @@ -2,8 +2,8 @@ import 'package:angel3_orm/angel3_orm.dart'; import 'package:api/converters/angel3/user_converter.dart'; import 'package:api/databases/interfaces/user_database.dart'; import 'package:api/exceptions/not_in_database_exception.dart'; +import 'package:api/models/databases/angel3/models.dart'; import 'package:api/models/domain/user.dart' as dm; -import 'package:api/models/databases/angel3/user.dart'; import 'package:uuid/uuid.dart'; class Angel3UserDatabase implements UserDatabase<String> { @@ -60,11 +60,11 @@ class Angel3UserDatabase implements UserDatabase<String> { @override Future<bool> isUsernameExisting(String username) async { var query = await _executor.query( - tableName, + usersTableName, ''' SELECT COUNT(*) - FROM $tableName - WHERE $usernameRowName = @username + FROM $usersTableName + WHERE $userUsernameColumnName = @username ''', // table/column is not possible to substitute { 'username': username, diff --git a/api/lib/factories/models/angel3/angel3_user_factory.dart b/api/lib/factories/models/angel3/angel3_user_factory.dart new file mode 100644 index 0000000000000000000000000000000000000000..262a13bb920541fb64eb2e1fb44f7f0fd0906e50 --- /dev/null +++ b/api/lib/factories/models/angel3/angel3_user_factory.dart @@ -0,0 +1,16 @@ +import 'package:api/factories/models/interfaces/user_factory.dart'; +import 'package:api/models/databases/angel3/models.dart'; + +class Angel3UserFactory implements UserFactory<String, Angel3User> { + @override + Angel3User call(String id, String username, String passwordHash, {String? name}) { + return Angel3User( + id: id, + createdAt: DateTime.now().toUtc(), + updatedAt: DateTime.now().toUtc(), + username: username, + passwordHash: passwordHash, + isEnabled: false, + ); + } +} diff --git a/api/lib/factories/models/interfaces/user_factory.dart b/api/lib/factories/models/interfaces/user_factory.dart new file mode 100644 index 0000000000000000000000000000000000000000..2315dec8c7516c20d2ba5618a92a020d56374467 --- /dev/null +++ b/api/lib/factories/models/interfaces/user_factory.dart @@ -0,0 +1,5 @@ +import 'package:api/models/domain/user.dart'; + +abstract interface class UserFactory<TUserId, TUser extends User<TUserId>> { + TUser call(TUserId id, String username, String passwordHash, { String? name }); +} diff --git a/api/lib/migrations/database/postgres/migration_scripts/0001.sql b/api/lib/migrations/database/postgres/migration_scripts/0001.sql index 98e391da111c57c2896338443867399b0cfdbe87..90b80a9abe5a048dcf7b615f667e22f24d0ef1a8 100644 --- a/api/lib/migrations/database/postgres/migration_scripts/0001.sql +++ b/api/lib/migrations/database/postgres/migration_scripts/0001.sql @@ -2,11 +2,10 @@ CREATE TABLE users( id uuid PRIMARY KEY, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - username text NOT NULL, + username text UNIQUE NOT NULL, password_hash text NOT NULL, "name" text, - is_enabled boolean NOT NULL, - UNIQUE(username) + is_enabled boolean NOT NULL ); CREATE TABLE points( @@ -15,5 +14,6 @@ CREATE TABLE points( longitude decimal NOT NULL, recorded_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + user_id uuid REFERENCES users (id) NOT NULL ); diff --git a/api/lib/models/databases/angel3/abstract/angel3_base_model.dart b/api/lib/models/databases/angel3/abstract/angel3_base_model.dart index d8981ae473ddd7e94f051a39f344a44a76dab4ea..eb110fda37db02ec981a05c38d7a077a37642ef0 100644 --- a/api/lib/models/databases/angel3/abstract/angel3_base_model.dart +++ b/api/lib/models/databases/angel3/abstract/angel3_base_model.dart @@ -1,16 +1,13 @@ import 'package:angel3_orm/angel3_orm.dart'; -const idRowName = 'id'; -const createdAtRowName = 'created_at'; -const updatedAtRowName = 'updated_at'; +const idColumnName = 'id'; +const createdAtColumnName = 'created_at'; +const updatedAtColumnName = 'updated_at'; abstract class Angel3BaseModel { - @Column(name: idRowName, indexType: IndexType.primaryKey, type: ColumnType.nText) - String get id; - - @Column(name: createdAtRowName) + @Column(name: createdAtColumnName) DateTime get createdAt; - @Column(name: updatedAtRowName) + @Column(name: updatedAtColumnName) DateTime get updatedAt; } diff --git a/api/lib/models/databases/angel3/models.dart b/api/lib/models/databases/angel3/models.dart new file mode 100644 index 0000000000000000000000000000000000000000..b21d9bbd8e38457c2ec81346a9955be7fac24bd3 --- /dev/null +++ b/api/lib/models/databases/angel3/models.dart @@ -0,0 +1,99 @@ +// All database models in one file is quite ugly, this is the explanation why it is necessary. +// +// Explanation of angel3 touch points: +// - @serialize generates a (model) class, which implements the annotated class. +// This generated class is instantiated e.g.. when making database requests. +// - @orm generates classes to query the database. +// +// As angel3_serialize (@serialize) requires the class name to start with an underscore, which makes the class private/file-scoped. +// To allow dependencies (~foreign keys etc.), the class, which defines the relation, must have access to the @orm annotated class +// (which is private because of the @serialize annotation). +// The build_runner is failing when the generated model is referenced and not the class annotated with @orm. +// This is what i've tried but doesn't work: +// - Omitting @serialize annotation and omitting leading underscore -> generated code tries to instantiate abstract class. +// - Omitting @serialize annotation and implement Model -> relation can't be created by build_runner. +// - Omitting @serialize annotation, implement and annotate it with @serialize -> multiple classes with the same name. +// - Making @orm an instantiable class -> code generation for fields does not work, e.g. Query classes don't have fields in values. +// - Changing prefix -> No possible, underscore is hardcoded in https://github.com/dart-backend/angel/blob/master/packages/serialize/angel_serialize_generator/lib/serialize.dart#L197. +// - Prefixing the class with a custom, non-private prefix, something like $_, has it's own implications such as the generated code +// instantiates an abstract or non-existent class. +// +// Currently, I see two options, either create one file for all models, or fork angel3_serialize and change this single character. +// From my point of view, it would be cleaner to fork the project, which in turn implies a huge additional expenditure in maintaining and +// continuously updating the new library which i'm not really willing to take at this point. +// Therefore, all angel3 models will be placed in this file at the moment. +// +// Feel free to experiment and maybe come up with better solution, where the classes can be placed in separate files. + +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:api/models/databases/angel3/abstract/angel3_base_model.dart'; +import 'package:api/models/domain/point.dart'; +import 'package:api/models/domain/user.dart'; +import 'package:optional/optional.dart'; + +part 'models.g.dart'; + +const usersTableName = 'users'; + +const userUsernameColumnName = 'username'; +const userPasswordHashColumnName = 'password_hash'; +const userNameColumnName = 'name'; +const userIsEnabledColumnName = 'is_enabled'; + +@serializable +@Orm(generateMigrations: false, tableName: usersTableName) +abstract class _Angel3User extends Angel3BaseModel implements User<String> { + @override + @Column(name: idColumnName, indexType: IndexType.primaryKey, type: ColumnType.nText) + String get id; + + @override + @Column(name: userUsernameColumnName, indexType: IndexType.unique) + String get username; + + @override + @Column(name: userPasswordHashColumnName) + String get passwordHash; + + @override + @Column(name: userNameColumnName) + String? get name; + + @override + @Column(name: userIsEnabledColumnName) + bool get isEnabled; + + @HasMany(localKey: idColumnName, foreignKey: pointUserFkName) + List<_Angel3Point> get points; +} + +const String pointsTableName = 'points'; + +const String pointLatitudeColumnName = 'latitude'; +const String pointLongitudeColumnName = 'longitude'; +const String pointRecordedAtColumnName = 'recorded_at'; +const String pointUserFkName = 'user_id'; + +@serializable +@Orm(generateMigrations: false, tableName: pointsTableName) +abstract class _Angel3Point extends Angel3BaseModel implements Point<String> { + @override + @Column(name: idColumnName, indexType: IndexType.primaryKey, type: ColumnType.nText) + String get id; + + @override + @Column(name: pointLatitudeColumnName, type: ColumnType.decimal) + double get latitude; + + @override + @Column(name: pointLongitudeColumnName, type: ColumnType.decimal) + double get longitude; + + @override + @Column(name: pointRecordedAtColumnName) + DateTime get recordedAt; + + @BelongsTo(localKey: pointUserFkName, foreignKey: idColumnName) + _Angel3User get user; +} diff --git a/api/lib/models/databases/angel3/point.dart b/api/lib/models/databases/angel3/point.dart deleted file mode 100644 index e52469f119db85771d777708ec1e8a9c1cafccfa..0000000000000000000000000000000000000000 --- a/api/lib/models/databases/angel3/point.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:angel3_orm/angel3_orm.dart'; -import 'package:angel3_serialize/angel3_serialize.dart'; -import 'package:api/models/databases/angel3/abstract/angel3_base_model.dart'; -import 'package:optional/optional.dart'; - -part 'point.g.dart'; - -const String tableName = 'points'; - -const String latitudeColumnName = 'latitude'; -const String longitudeColumnName = 'longitude'; -const String recordedAtColumnName = 'recorded_at'; - -@serializable -@Orm(generateMigrations: false, tableName: tableName) -abstract class _Point extends Angel3BaseModel { - @Column(name: latitudeColumnName, type: ColumnType.decimal) - double get latitude; - - @Column(name: longitudeColumnName, type: ColumnType.decimal) - double get longitude; - - @Column(name: recordedAtColumnName) - DateTime get recordedAt; -} diff --git a/api/lib/models/databases/angel3/user.dart b/api/lib/models/databases/angel3/user.dart deleted file mode 100644 index d4d65fcf3da5b14f173f9391946d38c8e442ac26..0000000000000000000000000000000000000000 --- a/api/lib/models/databases/angel3/user.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:angel3_orm/angel3_orm.dart'; -import 'package:angel3_serialize/angel3_serialize.dart'; -import 'package:api/models/databases/angel3/abstract/angel3_base_model.dart'; -import 'package:optional/optional.dart'; - -part 'user.g.dart'; - -const tableName = 'users'; - -const usernameRowName = 'username'; -const passwordHashRowName = 'password_hash'; -const nameRowName = 'name'; -const isEnabledRowName = 'is_enabled'; - -@serializable -@Orm(generateMigrations: false, tableName: tableName) -abstract class _User extends Angel3BaseModel { - @Column(name: usernameRowName, indexType: IndexType.unique) - String get username; - - @Column(name: passwordHashRowName) - String get passwordHash; - - @Column(name: nameRowName) - String? get name; - - @Column(name: isEnabledRowName) - bool get isEnabled; -} diff --git a/api/lib/models/domain/interfaces/identifiable_model.dart b/api/lib/models/domain/interfaces/identifiable_model.dart index b96f20eefa5de173ae34554f0c67409ca80b38c6..a0fe238fe8bc1e36da59cf876aa32567864808a4 100644 --- a/api/lib/models/domain/interfaces/identifiable_model.dart +++ b/api/lib/models/domain/interfaces/identifiable_model.dart @@ -1,3 +1,3 @@ -abstract interface class IdentifiableModel<T> { - T get id; +abstract interface class IdentifiableModel<TId> { + TId get id; } diff --git a/api/lib/models/domain/point.dart b/api/lib/models/domain/point.dart index 8ebdab00c2722f3b18a3c7a63ffe1ab0251ca1c1..8598cfc7c2ec30e7ad90436c615df55166576d8f 100644 --- a/api/lib/models/domain/point.dart +++ b/api/lib/models/domain/point.dart @@ -1,13 +1,7 @@ import 'package:api/models/domain/interfaces/identifiable_model.dart'; -class Point<TPointId> implements IdentifiableModel<TPointId> { - final num latitude; - final num longitude; - final DateTime recordedAt; - - @override - final TPointId id; - - Point(this.id, this.latitude, this.longitude, DateTime recordedAt) : - recordedAt = recordedAt.toUtc(); +abstract interface class Point<TPointId> implements IdentifiableModel<TPointId> { + num get latitude; + num get longitude; + DateTime get recordedAt; } diff --git a/api/lib/models/domain/user.dart b/api/lib/models/domain/user.dart index e46b24d07052cf5b816a0870bec7e7f006e3a8ae..7cecae2da053f19b1baef2001f22ee2812b71387 100644 --- a/api/lib/models/domain/user.dart +++ b/api/lib/models/domain/user.dart @@ -1,12 +1,8 @@ import 'package:api/models/domain/interfaces/identifiable_model.dart'; -class User<T> implements IdentifiableModel<T> { - @override - final T id; - final String username; - final String passwordHash; - final String? name; - final bool isEnabled; - - User(this.id, this.username, this.passwordHash, this.name, this.isEnabled); +abstract interface class User<TUserId> implements IdentifiableModel<TUserId> { + String get username; + String get passwordHash; + String? get name; + bool get isEnabled; }