From aac9c8af4f2af849a858fe438aeea11233cbbb6a Mon Sep 17 00:00:00 2001 From: Jean Jacques Avril Date: Fri, 3 Jan 2025 13:32:42 +0000 Subject: [PATCH] made use of first class citizen objects as json validator functions --- .../repository/project_repository_impl.dart | 2 +- .../project_task_repository_impl.dart | 2 +- .../time_entry_repository_impl.dart | 2 +- .../repository/user_repository_impl.dart | 2 +- .../lib/application/service/dto/user_dto.dart | 3 +- .../mapper/project_task_dto_mapper.dart | 2 +- .../application/service/project_service.dart | 26 ++++++------- .../service/project_task_service.dart | 26 ++++++------- .../application/service/service_provider.dart | 2 +- .../service/time_entry_service.dart | 26 ++++++------- .../lib/application/service/user_service.dart | 26 ++++++------- backend-dart/lib/common/request_helper.dart | 2 +- backend-dart/lib/common/response_helpers.dart | 16 ++++++++ backend-dart/lib/common/validation.dart | 28 ++++++-------- .../lib/domain/data/user_data_source.dart | 2 +- .../lib/domain/entities/time_entry.dart | 2 +- .../domain/repository/project_repository.dart | 2 +- .../repository/project_task_repository.dart | 2 +- .../repository/time_entry_repository.dart | 2 +- .../domain/repository/user_repository.dart | 2 +- .../persistence/prisma_user_data_source.dart | 37 ++++++++++++++----- 21 files changed, 122 insertions(+), 92 deletions(-) diff --git a/backend-dart/lib/application/repository/project_repository_impl.dart b/backend-dart/lib/application/repository/project_repository_impl.dart index 28b8311..5532a64 100644 --- a/backend-dart/lib/application/repository/project_repository_impl.dart +++ b/backend-dart/lib/application/repository/project_repository_impl.dart @@ -27,7 +27,7 @@ class ProjectRepositoryImpl implements ProjectRepository { } @override - TaskEither delete(String id) { + TaskEither delete(String id) { return database.projects.delete(id); } diff --git a/backend-dart/lib/application/repository/project_task_repository_impl.dart b/backend-dart/lib/application/repository/project_task_repository_impl.dart index 7af4537..2e762ac 100644 --- a/backend-dart/lib/application/repository/project_task_repository_impl.dart +++ b/backend-dart/lib/application/repository/project_task_repository_impl.dart @@ -32,7 +32,7 @@ class ProjectTaskRepositoryImpl implements ProjectTaskRepository { } @override - TaskEither delete(String id) { + TaskEither delete(String id) { return database.tasks.delete(id); } diff --git a/backend-dart/lib/application/repository/time_entry_repository_impl.dart b/backend-dart/lib/application/repository/time_entry_repository_impl.dart index 5ab497a..929c909 100644 --- a/backend-dart/lib/application/repository/time_entry_repository_impl.dart +++ b/backend-dart/lib/application/repository/time_entry_repository_impl.dart @@ -37,7 +37,7 @@ class TimeEntryRepositoryImpl implements TimeEntryRepository { } @override - TaskEither delete(String id) { + TaskEither delete(String id) { return database.timeEntries.delete(id); } diff --git a/backend-dart/lib/application/repository/user_repository_impl.dart b/backend-dart/lib/application/repository/user_repository_impl.dart index 4e8a76f..8554ec1 100755 --- a/backend-dart/lib/application/repository/user_repository_impl.dart +++ b/backend-dart/lib/application/repository/user_repository_impl.dart @@ -34,7 +34,7 @@ class UserRepositoryImpl implements UserRepository { } @override - TaskEither delete(String id) { + TaskEither delete(String id) { return database.users.delete(id); } diff --git a/backend-dart/lib/application/service/dto/user_dto.dart b/backend-dart/lib/application/service/dto/user_dto.dart index e1dfb7a..485ba28 100644 --- a/backend-dart/lib/application/service/dto/user_dto.dart +++ b/backend-dart/lib/application/service/dto/user_dto.dart @@ -14,7 +14,8 @@ class UserDto with _$UserDto { }) = _UserDto; /// JSON-Serialisierung - factory UserDto.fromJson(Map json) => _$UserDtoFromJson(json); + factory UserDto.fromJson(Map json) => + _$UserDtoFromJson(json); } @freezed diff --git a/backend-dart/lib/application/service/mapper/project_task_dto_mapper.dart b/backend-dart/lib/application/service/mapper/project_task_dto_mapper.dart index 749d69a..1c1db96 100644 --- a/backend-dart/lib/application/service/mapper/project_task_dto_mapper.dart +++ b/backend-dart/lib/application/service/mapper/project_task_dto_mapper.dart @@ -28,7 +28,7 @@ class ProjectTaskDtoMapper { )); TaskEither fromUpdateTo( - ProjectTaskUpdateDto origin, String id) => + ProjectTaskUpdateDto origin, String id) => TaskEither.of(ProjectTaskUpdate( id: id, name: origin.name, diff --git a/backend-dart/lib/application/service/project_service.dart b/backend-dart/lib/application/service/project_service.dart index 5c2f682..6654887 100644 --- a/backend-dart/lib/application/service/project_service.dart +++ b/backend-dart/lib/application/service/project_service.dart @@ -22,8 +22,8 @@ class ProjectService { .findAll() .flatMap(mapper.listTo) .map((projects) => projects.map((project) => project.toJson()).toList()) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok(jsonEncode(right))) + .map(jsonEncode) + .toResponse() .run(); } @@ -32,35 +32,34 @@ class ProjectService { return projects .findById(projectId) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.post('/') Future createProject(Request request) async { return requestToJson(request) - .flatMap((json) => - validateJsonKeys(json, ['name', 'userId'])) // Add required fields + .flatMap(validateJsonKeys(['name', 'userId'])) // Add required fields .flatMap((json) => decodeJson(json, ProjectCreateDto.fromJson)) .flatMap(mapper.fromCreateTo) .flatMap(projects.create) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.put('/') Future updateProject(Request request, String projectId) async { return requestToJson(request) - .flatMap((json) => validateJsonKeys(json, [])) + .flatMap(validateJsonKeys([])) .flatMap((json) => decodeJson(json, ProjectUpdateDto.fromJson)) .flatMap((dto) => mapper.fromUpdateTo(dto, projectId)) .flatMap(projects.update) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @@ -68,8 +67,9 @@ class ProjectService { Future deleteProject(Request request, String projectId) async { return projects .delete(projectId) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok('project deleted')) + .flatMap(mapper.to) + .map((dto) => dto.toJson()) + .toResponse() .run(); } diff --git a/backend-dart/lib/application/service/project_task_service.dart b/backend-dart/lib/application/service/project_task_service.dart index 9ba4c8b..abf41a4 100644 --- a/backend-dart/lib/application/service/project_task_service.dart +++ b/backend-dart/lib/application/service/project_task_service.dart @@ -22,8 +22,8 @@ class ProjectTaskService { .findAll() .flatMap(mapper.listTo) .map((tasks) => tasks.map((task) => task.toJson()).toList()) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok(jsonEncode(right))) + .map(jsonEncode) + .toResponse() .run(); } @@ -32,35 +32,34 @@ class ProjectTaskService { return tasks .findById(taskId) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.post('/') Future createTask(Request request) async { return requestToJson(request) - .flatMap((json) => - validateJsonKeys(json, ['name', 'projectId'])) // Add required fields + .flatMap(validateJsonKeys(['name', 'projectId'])) // Add required fields .flatMap((json) => decodeJson(json, ProjectTaskCreateDto.fromJson)) .flatMap(mapper.fromCreateTo) .flatMap(tasks.create) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.put('/') Future updateTask(Request request, String taskId) async { return requestToJson(request) - .flatMap((json) => validateJsonKeys(json, [])) + .flatMap(validateJsonKeys([])) .flatMap((json) => decodeJson(json, ProjectTaskUpdateDto.fromJson)) .flatMap((dto) => mapper.fromUpdateTo(dto, taskId)) .flatMap(tasks.update) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @@ -68,8 +67,9 @@ class ProjectTaskService { Future deleteTask(Request request, String taskId) async { return tasks .delete(taskId) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok('Task deleted')) + .flatMap(mapper.to) + .map((dto) => dto.toJson()) + .toResponse() .run(); } diff --git a/backend-dart/lib/application/service/service_provider.dart b/backend-dart/lib/application/service/service_provider.dart index 6aa0605..25115e5 100644 --- a/backend-dart/lib/application/service/service_provider.dart +++ b/backend-dart/lib/application/service/service_provider.dart @@ -23,4 +23,4 @@ final projectTaskServiceProvider = Provider((ref) { final timeEntryServiceProvider = Provider((ref) { final database = ref.read(timeEntryProvider); return TimeEntryService(database); -}); \ No newline at end of file +}); diff --git a/backend-dart/lib/application/service/time_entry_service.dart b/backend-dart/lib/application/service/time_entry_service.dart index b36650f..2864fe6 100644 --- a/backend-dart/lib/application/service/time_entry_service.dart +++ b/backend-dart/lib/application/service/time_entry_service.dart @@ -22,8 +22,8 @@ class TimeEntryService { .findAll() .flatMap(mapper.listTo) .map((entries) => entries.map((entry) => entry.toJson()).toList()) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok(jsonEncode(right))) + .map(jsonEncode) + .toResponse() .run(); } @@ -32,35 +32,35 @@ class TimeEntryService { return entries .findById(entryId) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.post('/') Future createEntry(Request request) async { return requestToJson(request) - .flatMap((json) => validateJsonKeys( - json, ['startTime', 'endTime', 'userId', 'projectId'])) + .flatMap( + validateJsonKeys(['startTime', 'endTime', 'userId', 'projectId'])) .flatMap((json) => decodeJson(json, TimeEntryCreateDto.fromJson)) .flatMap(mapper.fromCreateTo) .flatMap(entries.create) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.put('/') Future updateEntry(Request request, String entryId) async { return requestToJson(request) - .flatMap((json) => validateJsonKeys(json, [])) + .flatMap(validateJsonKeys([])) .flatMap((json) => decodeJson(json, TimeEntryUpdateDto.fromJson)) .flatMap((dto) => mapper.fromUpdateTo(dto, entryId)) .flatMap(entries.update) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @@ -68,8 +68,8 @@ class TimeEntryService { Future deleteEntry(Request request, String entryId) async { return entries .delete(entryId) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok('Entry deleted')) + .map((dto) => dto.toJson()) + .toResponse() .run(); } diff --git a/backend-dart/lib/application/service/user_service.dart b/backend-dart/lib/application/service/user_service.dart index c0cd0a8..4596bdf 100644 --- a/backend-dart/lib/application/service/user_service.dart +++ b/backend-dart/lib/application/service/user_service.dart @@ -22,8 +22,8 @@ class UserService { .findAll() .flatMap(mapper.listTo) .map((users) => users.map((user) => user.toJson()).toList()) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok(jsonEncode(right))) + .map(jsonEncode) + .toResponse() .run(); } @@ -32,35 +32,34 @@ class UserService { return users .findById(userId) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.post('/') Future createUser(Request request) async { return requestToJson(request) - .flatMap( - (json) => validateJsonKeys(json, ['name', 'email', 'password'])) + .flatMap(validateJsonKeys(['name', 'email', 'password'])) .flatMap((json) => decodeJson(json, UserCreateDto.fromJson)) .flatMap(mapper.fromCreateTo) .flatMap(users.create) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @Route.put('/') Future updateUser(Request request, String userId) async { return requestToJson(request) - .flatMap((json) => validateJsonKeys(json, [])) + .flatMap(validateJsonKeys([])) .flatMap((json) => decodeJson(json, UserUpdateDto.fromJson)) .flatMap((dto) => mapper.fromUpdateTo(dto, userId)) .flatMap(users.update) .flatMap(mapper.to) - .match((left) => ResponseHelpers.fromError(left), - (right) => ResponseHelpers.jsonOk(right.toJson())) + .map((dto) => dto.toJson()) + .toResponse() .run(); } @@ -68,8 +67,9 @@ class UserService { Future deleteUser(Request request, String userId) async { return users .delete(userId) - .match((left) => ResponseHelpers.fromError(left), - (right) => Response.ok('user deleted')) + .flatMap(mapper.to) + .map((dto) => dto.toJson()) + .toResponse() .run(); } diff --git a/backend-dart/lib/common/request_helper.dart b/backend-dart/lib/common/request_helper.dart index 5689a5f..36ed344 100644 --- a/backend-dart/lib/common/request_helper.dart +++ b/backend-dart/lib/common/request_helper.dart @@ -28,4 +28,4 @@ TaskEither decodeJson( message: 'Failed to decode JSON: ${error.toString()}', ), ); -} \ No newline at end of file +} diff --git a/backend-dart/lib/common/response_helpers.dart b/backend-dart/lib/common/response_helpers.dart index 895dafe..358556c 100644 --- a/backend-dart/lib/common/response_helpers.dart +++ b/backend-dart/lib/common/response_helpers.dart @@ -1,8 +1,24 @@ import 'dart:convert'; import 'package:backend_dart/domain/errors/error_code.dart'; import 'package:backend_dart/domain/interface/error.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:shelf/shelf.dart'; +extension TaskEitherResponseExtensions + on TaskEither> { + Task toResponse() => match( + (left) => ResponseHelpers.fromError(left), + (right) => ResponseHelpers.jsonOk(right), + ); +} + +extension TaskEitherResponseExtensionsFromString on TaskEither { + Task toResponse() => match( + (left) => ResponseHelpers.fromError(left), + (right) => Response.ok(right), + ); +} + class ResponseHelpers { /// Sendet eine JSON-Antwort mit einem 200-Statuscode static Response jsonOk(Map data) { diff --git a/backend-dart/lib/common/validation.dart b/backend-dart/lib/common/validation.dart index 1124e70..40f96c8 100644 --- a/backend-dart/lib/common/validation.dart +++ b/backend-dart/lib/common/validation.dart @@ -1,22 +1,18 @@ import 'package:backend_dart/domain/errors/app_error.dart'; -import 'package:backend_dart/domain/interface/error.dart'; import 'package:fpdart/fpdart.dart'; -TaskEither> validateJsonKeys( - Map json, List requiredKeys) { - return TaskEither.tryCatch( - () async { - final missingKeys = - requiredKeys.where((key) => !json.containsKey(key)).toList(); +TaskEither> Function(Map) + validateJsonKeys(List requiredKeys) { + return (json) { + final missingKeys = + requiredKeys.where((key) => !json.containsKey(key)).toList(); - if (missingKeys.isNotEmpty) { - throw Exception('Missing required keys: ${missingKeys.join(', ')}'); - } + if (missingKeys.isNotEmpty) { + return TaskEither.left(AppError.validationError( + message: 'Missing required keys: ${missingKeys.join(', ')}', + )); + } - return json; - }, - (error, _) => AppError.validationError( - message: 'Failed to validate JSON keys: ${error.toString()}', - ), - ); + return TaskEither.right(json); + }; } diff --git a/backend-dart/lib/domain/data/user_data_source.dart b/backend-dart/lib/domain/data/user_data_source.dart index cb05b07..b497746 100644 --- a/backend-dart/lib/domain/data/user_data_source.dart +++ b/backend-dart/lib/domain/data/user_data_source.dart @@ -11,7 +11,7 @@ abstract class UserDataSource { TaskEither update(UserUpdate user); - TaskEither delete(String id); + TaskEither delete(String id); TaskEither> findAll(); diff --git a/backend-dart/lib/domain/entities/time_entry.dart b/backend-dart/lib/domain/entities/time_entry.dart index dc83e63..61bfd2d 100644 --- a/backend-dart/lib/domain/entities/time_entry.dart +++ b/backend-dart/lib/domain/entities/time_entry.dart @@ -25,7 +25,7 @@ class TimeEntryCreate with _$TimeEntryCreate { const factory TimeEntryCreate({ String? id, required DateTime startTime, - DateTime? endTime, + DateTime? endTime, String? description, required String userId, required String projectId, diff --git a/backend-dart/lib/domain/repository/project_repository.dart b/backend-dart/lib/domain/repository/project_repository.dart index eaeed75..7a0721e 100644 --- a/backend-dart/lib/domain/repository/project_repository.dart +++ b/backend-dart/lib/domain/repository/project_repository.dart @@ -13,7 +13,7 @@ abstract class ProjectRepository { TaskEither update(ProjectUpdate project); /// Deletes a project by its unique ID. - TaskEither delete(String id); + TaskEither delete(String id); /// Finds all projects. TaskEither> findAll(); diff --git a/backend-dart/lib/domain/repository/project_task_repository.dart b/backend-dart/lib/domain/repository/project_task_repository.dart index e834d7e..df534ad 100644 --- a/backend-dart/lib/domain/repository/project_task_repository.dart +++ b/backend-dart/lib/domain/repository/project_task_repository.dart @@ -16,7 +16,7 @@ abstract class ProjectTaskRepository { TaskEither update(ProjectTaskUpdate task); /// Deletes a project task by its unique ID. - TaskEither delete(String id); + TaskEither delete(String id); /// Finds all project tasks. TaskEither> findAll(); diff --git a/backend-dart/lib/domain/repository/time_entry_repository.dart b/backend-dart/lib/domain/repository/time_entry_repository.dart index c138fa0..cb1279e 100644 --- a/backend-dart/lib/domain/repository/time_entry_repository.dart +++ b/backend-dart/lib/domain/repository/time_entry_repository.dart @@ -19,7 +19,7 @@ abstract class TimeEntryRepository { TaskEither update(TimeEntryUpdate timeEntry); /// Deletes a time entry by its unique ID. - TaskEither delete(String id); + TaskEither delete(String id); /// Finds all time entries. TaskEither> findAll(); diff --git a/backend-dart/lib/domain/repository/user_repository.dart b/backend-dart/lib/domain/repository/user_repository.dart index af94616..083e181 100755 --- a/backend-dart/lib/domain/repository/user_repository.dart +++ b/backend-dart/lib/domain/repository/user_repository.dart @@ -11,7 +11,7 @@ abstract class UserRepository { TaskEither update(UserUpdate user); - TaskEither delete(String id); + TaskEither delete(String id); TaskEither> findAll(); } diff --git a/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart b/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart index 37c6423..ff4f404 100755 --- a/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart +++ b/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart @@ -45,9 +45,13 @@ class PrismaUserDataSource implements UserDataSource { message: 'Failed to find user by email: ${error.toString()}', ), ) - .flatMap(errorOnNull(AppError.notFound( - 'User with email $email found', - ))) + .flatMap( + errorOnNull( + AppError.notFound( + 'User with email $email found', + ), + ), + ) .flatMap(mapper.from); } @@ -63,9 +67,13 @@ class PrismaUserDataSource implements UserDataSource { message: 'Failed to find user by ID: ${error.toString()}', ), ) - .flatMap(errorOnNull(AppError.notFound( - "User with id $id not found", - ))) + .flatMap( + errorOnNull( + AppError.notFound( + "User with id $id not found", + ), + ), + ) .flatMap(mapper.from); } @@ -104,15 +112,24 @@ class PrismaUserDataSource implements UserDataSource { } @override - TaskEither delete(String id) { - return TaskEither.tryCatch( + TaskEither delete(String id) { + return TaskEither.tryCatch( () async { - await prisma.userDbo.delete(where: UserDboWhereUniqueInput(id: id)); + return await prisma.userDbo + .delete(where: UserDboWhereUniqueInput(id: id)); }, (error, _) => AppError.databaseError( message: 'Failed to delete user: ${error.toString()}', ), - ); + ) + .flatMap( + errorOnNull( + AppError.notFound( + 'User not found', + ), + ), + ) + .flatMap(mapper.from); } @override