From 4dc673ba72c82ba977c04875c88c7899f1bace7d Mon Sep 17 00:00:00 2001
From: Andri Joos <andri@joos.io>
Date: Fri, 13 Sep 2024 16:36:05 +0200
Subject: [PATCH] wip

---
 api/bin/api.dart                              |  4 +
 .../converters/angel3/point_converter.dart    |  2 +-
 api/lib/converters/angel3/user_converter.dart |  2 +-
 .../angel3/angel3_point_database.dart         |  5 +-
 .../angel3/angel3_user_database.dart          |  8 +-
 .../models/angel3/angel3_user_factory.dart    | 16 +++
 .../models/interfaces/user_factory.dart       |  5 +
 .../postgres/migration_scripts/0001.sql       |  8 +-
 .../angel3/abstract/angel3_base_model.dart    | 13 +--
 api/lib/models/databases/angel3/models.dart   | 99 +++++++++++++++++++
 api/lib/models/databases/angel3/point.dart    | 25 -----
 api/lib/models/databases/angel3/user.dart     | 29 ------
 .../domain/interfaces/identifiable_model.dart |  4 +-
 api/lib/models/domain/point.dart              | 14 +--
 api/lib/models/domain/user.dart               | 14 +--
 15 files changed, 154 insertions(+), 94 deletions(-)
 create mode 100644 api/lib/factories/models/angel3/angel3_user_factory.dart
 create mode 100644 api/lib/factories/models/interfaces/user_factory.dart
 create mode 100644 api/lib/models/databases/angel3/models.dart
 delete mode 100644 api/lib/models/databases/angel3/point.dart
 delete mode 100644 api/lib/models/databases/angel3/user.dart

diff --git a/api/bin/api.dart b/api/bin/api.dart
index c027002..c3a10e0 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 6600c65..a8dba51 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 6f979e9..f328eb1 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 4b83c4c..9f2be38 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 1442286..3a3dc49 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 0000000..262a13b
--- /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 0000000..2315dec
--- /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 98e391d..90b80a9 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 d8981ae..eb110fd 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 0000000..b21d9bb
--- /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 e52469f..0000000
--- 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 d4d65fc..0000000
--- 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 b96f20e..a0fe238 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 8ebdab0..8598cfc 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 e46b24d..7cecae2 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;
 }
-- 
GitLab