diff --git a/wrestling_scoreboard_common/lib/src/data/data_object.dart b/wrestling_scoreboard_common/lib/src/data/data_object.dart index d7b75114..a2a99bab 100644 --- a/wrestling_scoreboard_common/lib/src/data/data_object.dart +++ b/wrestling_scoreboard_common/lib/src/data/data_object.dart @@ -115,7 +115,7 @@ abstract class DataObject { } } -abstract class Organizational { +abstract class Organizational extends DataObject { String? get orgSyncId => null; Organization? get organization => null; @@ -124,5 +124,6 @@ abstract class Organizational { class DataUnimplementedError extends UnimplementedError { DataUnimplementedError(CRUD operationType, Type type, [DataObject? filterObject]) : super( - 'Data ${operationType.toString().substring(5).toUpperCase()}-request for "${type.toString()}" ${filterObject == null ? '' : 'in "${filterObject.runtimeType.toString()}"'} not found.'); + 'Data ${operationType.toString().substring(5).toUpperCase()}-request for "${type.toString()}" ${filterObject == + null ? '' : 'in "${filterObject.runtimeType.toString()}"'} not found.'); } diff --git a/wrestling_scoreboard_common/lib/src/services/api.dart b/wrestling_scoreboard_common/lib/src/services/api.dart index b98aceab..e53904ac 100644 --- a/wrestling_scoreboard_common/lib/src/services/api.dart +++ b/wrestling_scoreboard_common/lib/src/services/api.dart @@ -1,7 +1,7 @@ import '../../common.dart'; import 'apis/germany_by.dart'; -typedef GetSingleOfOrg = Future Function(String orgSyncId, {required int orgId}); +typedef GetSingleOfOrg = Future Function(String orgSyncId, {required int orgId}); enum WrestlingApiProvider { deNwRingenApi, diff --git a/wrestling_scoreboard_common/lib/src/services/apis/germany_by.dart b/wrestling_scoreboard_common/lib/src/services/apis/germany_by.dart index ff69fccc..933a0b8c 100644 --- a/wrestling_scoreboard_common/lib/src/services/apis/germany_by.dart +++ b/wrestling_scoreboard_common/lib/src/services/apis/germany_by.dart @@ -31,7 +31,7 @@ class ByGermanyWrestlingApi extends WrestlingApi { @override GetSingleOfOrg getSingleOfOrg; - late Future Function(String orgSyncId) _getSingleBySyncId; + late Future Function(String orgSyncId) _getSingleBySyncId; ByGermanyWrestlingApi( this.organization, { @@ -40,7 +40,7 @@ class ByGermanyWrestlingApi extends WrestlingApi { this.authService, }) { _getSingleBySyncId = - (String orgSyncId) => getSingleOfOrg(orgSyncId, orgId: organization.id!); + (String orgSyncId) => getSingleOfOrg(orgSyncId, orgId: organization.id!); } @override diff --git a/wrestling_scoreboard_common/test/services/apis_test.dart b/wrestling_scoreboard_common/test/services/apis_test.dart index c1c2c376..7fd64598 100644 --- a/wrestling_scoreboard_common/test/services/apis_test.dart +++ b/wrestling_scoreboard_common/test/services/apis_test.dart @@ -117,7 +117,7 @@ void main() { wrestlingApi = WrestlingApiProvider.deNwRingenApi.getApi( organizationNRW, authService: BasicAuthService(username: '', password: ''), - getSingleOfOrg: (String orgSyncId, {required int orgId}) async { + getSingleOfOrg: (String orgSyncId, {required int orgId}) async { switch (T) { case const (Club): return (await wrestlingApi.importClubs()).singleWhere((club) => club.orgSyncId == orgSyncId) as T; diff --git a/wrestling_scoreboard_server/lib/controllers/club_controller.dart b/wrestling_scoreboard_server/lib/controllers/club_controller.dart index 5a5cce10..82566a5d 100644 --- a/wrestling_scoreboard_server/lib/controllers/club_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/club_controller.dart @@ -2,10 +2,11 @@ import 'package:shelf/shelf.dart'; import 'package:wrestling_scoreboard_common/common.dart'; import 'package:wrestling_scoreboard_server/controllers/entity_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/membership_controller.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/team_controller.dart'; import 'package:wrestling_scoreboard_server/request.dart'; -class ClubController extends EntityController { +class ClubController extends OrganizationalController { static final ClubController _singleton = ClubController._internal(); factory ClubController() { diff --git a/wrestling_scoreboard_server/lib/controllers/database_controller.dart b/wrestling_scoreboard_server/lib/controllers/database_controller.dart index 27eb3fb3..7d1774a3 100644 --- a/wrestling_scoreboard_server/lib/controllers/database_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/database_controller.dart @@ -77,9 +77,8 @@ class DatabaseController { Future _restore(String dumpPath) async { final db = PostgresDb(); - final conn = db.connection; { - await conn.execute('DROP SCHEMA IF EXISTS public CASCADE;'); + await db.connection.execute('DROP SCHEMA IF EXISTS public CASCADE;'); await db.close(); } @@ -97,6 +96,9 @@ class DatabaseController { final processResult = await Process.run('psql', args, environment: {'PGPASSWORD': db.dbPW}); await db.open(); + Iterable entityControllers = dataTypes.map((t) => EntityController.getControllerFromDataType(t)); + await Future.forEach(entityControllers, (e) => e.init()); + if (processResult.exitCode != 0) { throw processResult.stderr; } diff --git a/wrestling_scoreboard_server/lib/controllers/division_controller.dart b/wrestling_scoreboard_server/lib/controllers/division_controller.dart index 92a15069..8f25073d 100644 --- a/wrestling_scoreboard_server/lib/controllers/division_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/division_controller.dart @@ -3,10 +3,11 @@ import 'package:wrestling_scoreboard_common/common.dart'; import 'package:wrestling_scoreboard_server/controllers/division_weight_class_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/entity_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/league_controller.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/weight_class_controller.dart'; import 'package:wrestling_scoreboard_server/request.dart'; -class DivisionController extends EntityController { +class DivisionController extends OrganizationalController { static final DivisionController _singleton = DivisionController._internal(); factory DivisionController() { diff --git a/wrestling_scoreboard_server/lib/controllers/entity_controller.dart b/wrestling_scoreboard_server/lib/controllers/entity_controller.dart index 93022061..caa4249a 100644 --- a/wrestling_scoreboard_server/lib/controllers/entity_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/entity_controller.dart @@ -57,7 +57,20 @@ abstract class EntityController { String tableName; String primaryKeyName; - EntityController({required this.tableName, this.primaryKeyName = 'id'}); + late Future getSingleRawStmt; + late Future deleteSingleStmt; + + EntityController({required this.tableName, this.primaryKeyName = 'id'}) { + init(); + } + + void init() { + getSingleRawStmt = + PostgresDb().connection.prepare(psql.Sql.named('SELECT * FROM $tableName WHERE $primaryKeyName = @id;')); + + deleteSingleStmt = + PostgresDb().connection.prepare(psql.Sql.named('DELETE FROM $tableName WHERE $primaryKeyName = @id;')); + } Future postSingle(Request request) async { final message = await request.readAsString(); @@ -80,15 +93,6 @@ abstract class EntityController { return DataObject.fromRaw(single, getSingleFromDataType); } - /// Get a single data object via a foreign id (sync id), given by an organization. - Future getSingleOfOrg(String orgSyncId, {required int orgId}) async { - final single = await getSingleOfOrgRaw(orgSyncId, orgId: orgId); - return DataObject.fromRaw(single, getSingleFromDataType); - } - - late final getSingleRawStmt = - PostgresDb().connection.prepare(psql.Sql.named('SELECT * FROM $tableName WHERE $primaryKeyName = @id;')); - Future> getSingleRaw(int id) async { final resStream = (await getSingleRawStmt).bind({'id': id}); final many = await resStream.toColumnMap().toList(); @@ -96,21 +100,6 @@ abstract class EntityController { return many.first; } - late final getSingleOfOrgRawStmt = PostgresDb() - .connection - .prepare(psql.Sql.named('SELECT * FROM $tableName WHERE organization_id = @orgId AND org_sync_id = @orgSyncId;')); - - Future> getSingleOfOrgRaw(String orgSyncId, {required int orgId}) async { - if (orgSyncId != orgSyncId.trim()) { - orgSyncId = orgSyncId.trim(); - print('$T with orgSyncId "$orgSyncId" was trimmed'); - } - final resStream = (await getSingleOfOrgRawStmt).bind({'orgSyncId': orgSyncId, 'orgId': orgId}); - final many = await resStream.toColumnMap().toList(); - if (many.isEmpty) throw InvalidParameterException('$T with orgSyncId "$orgSyncId" not found'); - return many.first; - } - Future createSingle(T dataObject) async { return await createSingleRaw(dataObject.toRaw()); } @@ -140,25 +129,6 @@ abstract class EntityController { return dataObject.copyWithId(await createSingle(dataObject)) as T; } - Future getOrCreateSingleOfOrg(T dataObject) async { - if (dataObject.id != null) { - throw Exception('Data object already has an id: $dataObject'); - } - if (dataObject is! Organizational) { - throw Exception('Data object is not Organizational: $dataObject'); - } - final organizational = (dataObject as Organizational); - if (organizational.organization?.id == null || organizational.orgSyncId == null) { - throw Exception('Organization id and sync id must not be null: $dataObject'); - } - try { - final single = await getSingleOfOrg(organizational.orgSyncId!, orgId: organizational.organization!.id!); - return single; - } on InvalidParameterException catch (_) { - return createSingleReturn(dataObject); - } - } - Future> createMany(List dataObjects) async { return await Future.wait(dataObjects.map((element) => createSingle(element))); } @@ -167,10 +137,6 @@ abstract class EntityController { return await Future.wait(dataObjects.map((element) => createSingleReturn(element))); } - Future> getOrCreateManyOfOrg(List dataObjects) async { - return await Future.wait(dataObjects.map((element) => getOrCreateSingleOfOrg(element))); - } - Future updateSingle(T dataObject) async { return updateSingleRaw(dataObject.toRaw()); } @@ -211,9 +177,6 @@ abstract class EntityController { }); } - late final deleteSingleStmt = - PostgresDb().connection.prepare(psql.Sql.named('DELETE FROM $tableName WHERE $primaryKeyName = @id;')); - Future deleteSingle(int id) async { try { await (await deleteSingleStmt).bind({'id': id}).toList(); @@ -435,10 +398,6 @@ abstract class EntityController { return getControllerFromDataType(T).getSingle(id) as Future; } - static Future getSingleFromDataTypeOfOrg(String orgSyncId, {required int orgId}) { - return getControllerFromDataType(T).getSingleOfOrg(orgSyncId, orgId: orgId) as Future; - } - static Future> getManyFromDataType( {List? conditions, Conjunction conjunction = Conjunction.and, Map? substitutionValues}) { return getControllerFromDataType(T).getMany( @@ -507,7 +466,7 @@ class InvalidParameterException implements Exception { } } -extension on Map { +extension ToPsqlParser on Map { /// Parse custom postgres types /// https://github.com/isoos/postgresql-dart/issues/276 Map parse() { @@ -515,7 +474,7 @@ extension on Map { } } -extension on psql.ResultStream { +extension FromPsqlParser on psql.ResultStream { /// Parse custom postgres types /// https://github.com/isoos/postgresql-dart/issues/276 // ignore: unused_element diff --git a/wrestling_scoreboard_server/lib/controllers/league_controller.dart b/wrestling_scoreboard_server/lib/controllers/league_controller.dart index 8da03d28..571b4f9b 100644 --- a/wrestling_scoreboard_server/lib/controllers/league_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/league_controller.dart @@ -6,6 +6,7 @@ import 'package:wrestling_scoreboard_server/controllers/league_team_participatio import 'package:wrestling_scoreboard_server/controllers/lineup_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/membership_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/organization_controller.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/participant_state_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/participation_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/person_controller.dart'; @@ -16,7 +17,7 @@ import 'package:wrestling_scoreboard_server/request.dart'; import 'bout_controller.dart'; -class LeagueController extends EntityController { +class LeagueController extends OrganizationalController { static final LeagueController _singleton = LeagueController._internal(); factory LeagueController() { diff --git a/wrestling_scoreboard_server/lib/controllers/membership_controller.dart b/wrestling_scoreboard_server/lib/controllers/membership_controller.dart index 4a4f18b2..204880bf 100644 --- a/wrestling_scoreboard_server/lib/controllers/membership_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/membership_controller.dart @@ -1,8 +1,7 @@ import 'package:wrestling_scoreboard_common/common.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; -import 'entity_controller.dart'; - -class MembershipController extends EntityController { +class MembershipController extends OrganizationalController { static final MembershipController _singleton = MembershipController._internal(); factory MembershipController() { diff --git a/wrestling_scoreboard_server/lib/controllers/organization_controller.dart b/wrestling_scoreboard_server/lib/controllers/organization_controller.dart index ecb67713..0ea84191 100644 --- a/wrestling_scoreboard_server/lib/controllers/organization_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/organization_controller.dart @@ -7,6 +7,7 @@ import 'package:wrestling_scoreboard_server/controllers/competition_controller.d import 'package:wrestling_scoreboard_server/controllers/division_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/division_weight_class_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/entity_controller.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/team_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/weight_class_controller.dart'; import 'package:wrestling_scoreboard_server/request.dart'; @@ -55,7 +56,7 @@ class OrganizationController extends EntityController { } final organization = await getSingle(organizationId); - return organization.getApi(EntityController.getSingleFromDataTypeOfOrg, authService: authService); + return organization.getApi(OrganizationalController.getSingleFromDataTypeOfOrg, authService: authService); } Future import(Request request, String organizationId) async { diff --git a/wrestling_scoreboard_server/lib/controllers/organizational_controller.dart b/wrestling_scoreboard_server/lib/controllers/organizational_controller.dart new file mode 100644 index 00000000..4bd7f793 --- /dev/null +++ b/wrestling_scoreboard_server/lib/controllers/organizational_controller.dart @@ -0,0 +1,59 @@ +import 'package:postgres/postgres.dart' as psql; +import 'package:wrestling_scoreboard_common/common.dart'; +import 'package:wrestling_scoreboard_server/controllers/entity_controller.dart'; +import 'package:wrestling_scoreboard_server/services/postgres_db.dart'; + +abstract class OrganizationalController extends EntityController { + OrganizationalController({required super.tableName}); + + late Future getSingleOfOrgRawStmt; + + @override + void init() { + getSingleOfOrgRawStmt = PostgresDb().connection.prepare( + psql.Sql.named('SELECT * FROM $tableName WHERE organization_id = @orgId AND org_sync_id = @orgSyncId;')); + super.init(); + } + + /// Get a single data object via a foreign id (sync id), given by an organization. + Future getSingleOfOrg(String orgSyncId, {required int orgId}) async { + final single = await getSingleOfOrgRaw(orgSyncId, orgId: orgId); + return DataObject.fromRaw(single, EntityController.getSingleFromDataType); + } + + Future> getSingleOfOrgRaw(String orgSyncId, {required int orgId}) async { + if (orgSyncId != orgSyncId.trim()) { + orgSyncId = orgSyncId.trim(); + print('$T with orgSyncId "$orgSyncId" was trimmed'); + } + final resStream = (await getSingleOfOrgRawStmt).bind({'orgSyncId': orgSyncId, 'orgId': orgId}); + final many = await resStream.toColumnMap().toList(); + if (many.isEmpty) throw InvalidParameterException('$T with orgSyncId "$orgSyncId" not found'); + return many.first; + } + + Future getOrCreateSingleOfOrg(T dataObject) async { + if (dataObject.id != null) { + throw Exception('Data object already has an id: $dataObject'); + } + final organizational = (dataObject as Organizational); + if (organizational.organization?.id == null || organizational.orgSyncId == null) { + throw Exception('Organization id and sync id must not be null: $dataObject'); + } + try { + final single = await getSingleOfOrg(organizational.orgSyncId!, orgId: organizational.organization!.id!); + return single; + } on InvalidParameterException catch (_) { + return createSingleReturn(dataObject); + } + } + + Future> getOrCreateManyOfOrg(List dataObjects) async { + return await Future.wait(dataObjects.map((element) => getOrCreateSingleOfOrg(element))); + } + + static Future getSingleFromDataTypeOfOrg(String orgSyncId, {required int orgId}) { + return (EntityController.getControllerFromDataType(T) as OrganizationalController) + .getSingleOfOrg(orgSyncId, orgId: orgId); + } +} diff --git a/wrestling_scoreboard_server/lib/controllers/person_controller.dart b/wrestling_scoreboard_server/lib/controllers/person_controller.dart index 3426be8d..51ec82d1 100644 --- a/wrestling_scoreboard_server/lib/controllers/person_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/person_controller.dart @@ -2,11 +2,12 @@ import 'package:postgres/postgres.dart' as psql; import 'package:shelf/shelf.dart'; import 'package:wrestling_scoreboard_common/common.dart'; import 'package:wrestling_scoreboard_server/controllers/membership_controller.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/request.dart'; import 'entity_controller.dart'; -class PersonController extends EntityController { +class PersonController extends OrganizationalController { static final PersonController _singleton = PersonController._internal(); factory PersonController() { diff --git a/wrestling_scoreboard_server/lib/controllers/team_controller.dart b/wrestling_scoreboard_server/lib/controllers/team_controller.dart index 30b95a47..2dd41bb1 100644 --- a/wrestling_scoreboard_server/lib/controllers/team_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/team_controller.dart @@ -1,11 +1,12 @@ import 'package:shelf/shelf.dart'; import 'package:wrestling_scoreboard_common/common.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/team_match_controller.dart'; import 'package:wrestling_scoreboard_server/request.dart'; import 'entity_controller.dart'; -class TeamController extends EntityController { +class TeamController extends OrganizationalController { static final TeamController _singleton = TeamController._internal(); factory TeamController() { diff --git a/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart b/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart index 161b76ac..0a9fffe2 100644 --- a/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart +++ b/wrestling_scoreboard_server/lib/controllers/team_match_controller.dart @@ -4,6 +4,7 @@ import 'package:postgres/postgres.dart' as psql; import 'package:shelf/shelf.dart'; import 'package:wrestling_scoreboard_common/common.dart'; import 'package:wrestling_scoreboard_server/controllers/division_controller.dart'; +import 'package:wrestling_scoreboard_server/controllers/organizational_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/participant_state_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/participation_controller.dart'; import 'package:wrestling_scoreboard_server/controllers/team_match_bout_controller.dart'; @@ -14,7 +15,7 @@ import 'package:wrestling_scoreboard_server/services/postgres_db.dart'; import 'bout_controller.dart'; import 'entity_controller.dart'; -class TeamMatchController extends EntityController { +class TeamMatchController extends OrganizationalController { static final TeamMatchController _singleton = TeamMatchController._internal(); factory TeamMatchController() { diff --git a/wrestling_scoreboard_server/lib/server.dart b/wrestling_scoreboard_server/lib/server.dart index ec111a50..c6149a30 100644 --- a/wrestling_scoreboard_server/lib/server.dart +++ b/wrestling_scoreboard_server/lib/server.dart @@ -53,12 +53,44 @@ Future init() async { // https://cloud.google.com/run/docs/reference/container-contract#port final port = int.parse(env['PORT'] ?? '8080'); + // Must open the database before initializing any routes. + await PostgresDb().open(); + + // Router instance to handler requests. + final router = shelf_router.Router() + ..mount('/api', ApiRoute().pipeline) + ..mount('/ws', (Request request) { + try { + return websocketHandler(request); + } on HijackException catch (error, _) { + // A HijackException should bypass the response-writing logic entirely. + print('Warning: HijackException thrown on WebsocketHandler.\n$error'); + // TODO hide stack trace or handle better + // Exception is handled here: https://pub.dev/documentation/shelf/latest/shelf_io/handleRequest.html + rethrow; + } catch (error, _) { + print('Error thrown by handler.\n$error'); + return Response.internalServerError(); + } + }) + ..mount('/about', (Request request) async { + final pubspec = await _parsePubspec(); + return Response.ok(''' + Name: ${pubspec.name} + Description: ${pubspec.description} + Version: ${pubspec.version} + '''); + }); + + // Serve files from the file system. + final staticHandler = shelf_static.createStaticHandler('public', defaultDocument: 'index.html'); + // See https://pub.dev/documentation/shelf/latest/shelf/Cascade-class.html final cascade = Cascade() // First, serve files from the 'public' directory - .add(_staticHandler) + .add(staticHandler) // If a corresponding file is not found, send requests to a `Router` - .add(_router.call); + .add(router.call); // See https://pub.dev/documentation/shelf/latest/shelf/Pipeline-class.html final pipeline = Pipeline() @@ -74,39 +106,8 @@ Future init() async { port, ); - await PostgresDb().open(); - final serverUrl = 'http://${server.address.host}:${server.port}'; print('Serving at $serverUrl'); print('Serving API at $serverUrl/api'); print('Serving Websocket at $serverUrl/ws'); } - -// Serve files from the file system. -final _staticHandler = shelf_static.createStaticHandler('public', defaultDocument: 'index.html'); - -// Router instance to handler requests. -final _router = shelf_router.Router() - ..mount('/api', ApiRoute().pipeline) - ..mount('/ws', (Request request) { - try { - return websocketHandler(request); - } on HijackException catch (error, _) { - // A HijackException should bypass the response-writing logic entirely. - print('Warning: HijackException thrown on WebsocketHandler.\n$error'); - // TODO hide stack trace or handle better - // Exception is handled here: https://pub.dev/documentation/shelf/latest/shelf_io/handleRequest.html - rethrow; - } catch (error, _) { - print('Error thrown by handler.\n$error'); - return Response.internalServerError(); - } - }) - ..mount('/about', (Request request) async { - final pubspec = await _parsePubspec(); - return Response.ok(''' - Name: ${pubspec.name} - Description: ${pubspec.description} - Version: ${pubspec.version} - '''); - });