made use of first class citizen objects as json validator functions

This commit is contained in:
Jean Jacques Avril 2025-01-03 13:32:42 +00:00
parent 2210fe4bb1
commit aac9c8af4f
No known key found for this signature in database
21 changed files with 122 additions and 92 deletions

View File

@ -27,7 +27,7 @@ class ProjectRepositoryImpl implements ProjectRepository {
} }
@override @override
TaskEither<IError, void> delete(String id) { TaskEither<IError, Project> delete(String id) {
return database.projects.delete(id); return database.projects.delete(id);
} }

View File

@ -32,7 +32,7 @@ class ProjectTaskRepositoryImpl implements ProjectTaskRepository {
} }
@override @override
TaskEither<IError, void> delete(String id) { TaskEither<IError, ProjectTask> delete(String id) {
return database.tasks.delete(id); return database.tasks.delete(id);
} }

View File

@ -37,7 +37,7 @@ class TimeEntryRepositoryImpl implements TimeEntryRepository {
} }
@override @override
TaskEither<IError, void> delete(String id) { TaskEither<IError, TimeEntry> delete(String id) {
return database.timeEntries.delete(id); return database.timeEntries.delete(id);
} }

View File

@ -34,7 +34,7 @@ class UserRepositoryImpl implements UserRepository {
} }
@override @override
TaskEither<IError, void> delete(String id) { TaskEither<IError, User> delete(String id) {
return database.users.delete(id); return database.users.delete(id);
} }

View File

@ -14,7 +14,8 @@ class UserDto with _$UserDto {
}) = _UserDto; }) = _UserDto;
/// JSON-Serialisierung /// JSON-Serialisierung
factory UserDto.fromJson(Map<String, dynamic> json) => _$UserDtoFromJson(json); factory UserDto.fromJson(Map<String, dynamic> json) =>
_$UserDtoFromJson(json);
} }
@freezed @freezed

View File

@ -22,8 +22,8 @@ class ProjectService {
.findAll() .findAll()
.flatMap(mapper.listTo) .flatMap(mapper.listTo)
.map((projects) => projects.map((project) => project.toJson()).toList()) .map((projects) => projects.map((project) => project.toJson()).toList())
.match((left) => ResponseHelpers.fromError(left), .map(jsonEncode)
(right) => Response.ok(jsonEncode(right))) .toResponse()
.run(); .run();
} }
@ -32,35 +32,34 @@ class ProjectService {
return projects return projects
.findById(projectId) .findById(projectId)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.post('/') @Route.post('/')
Future<Response> createProject(Request request) async { Future<Response> createProject(Request request) async {
return requestToJson(request) return requestToJson(request)
.flatMap((json) => .flatMap(validateJsonKeys(['name', 'userId'])) // Add required fields
validateJsonKeys(json, ['name', 'userId'])) // Add required fields
.flatMap((json) => decodeJson(json, ProjectCreateDto.fromJson)) .flatMap((json) => decodeJson(json, ProjectCreateDto.fromJson))
.flatMap(mapper.fromCreateTo) .flatMap(mapper.fromCreateTo)
.flatMap(projects.create) .flatMap(projects.create)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.put('/<projectId>') @Route.put('/<projectId>')
Future<Response> updateProject(Request request, String projectId) async { Future<Response> updateProject(Request request, String projectId) async {
return requestToJson(request) return requestToJson(request)
.flatMap((json) => validateJsonKeys(json, [])) .flatMap(validateJsonKeys([]))
.flatMap((json) => decodeJson(json, ProjectUpdateDto.fromJson)) .flatMap((json) => decodeJson(json, ProjectUpdateDto.fromJson))
.flatMap((dto) => mapper.fromUpdateTo(dto, projectId)) .flatMap((dto) => mapper.fromUpdateTo(dto, projectId))
.flatMap(projects.update) .flatMap(projects.update)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@ -68,8 +67,9 @@ class ProjectService {
Future<Response> deleteProject(Request request, String projectId) async { Future<Response> deleteProject(Request request, String projectId) async {
return projects return projects
.delete(projectId) .delete(projectId)
.match((left) => ResponseHelpers.fromError(left), .flatMap(mapper.to)
(right) => Response.ok('project deleted')) .map((dto) => dto.toJson())
.toResponse()
.run(); .run();
} }

View File

@ -22,8 +22,8 @@ class ProjectTaskService {
.findAll() .findAll()
.flatMap(mapper.listTo) .flatMap(mapper.listTo)
.map((tasks) => tasks.map((task) => task.toJson()).toList()) .map((tasks) => tasks.map((task) => task.toJson()).toList())
.match((left) => ResponseHelpers.fromError(left), .map(jsonEncode)
(right) => Response.ok(jsonEncode(right))) .toResponse()
.run(); .run();
} }
@ -32,35 +32,34 @@ class ProjectTaskService {
return tasks return tasks
.findById(taskId) .findById(taskId)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.post('/') @Route.post('/')
Future<Response> createTask(Request request) async { Future<Response> createTask(Request request) async {
return requestToJson(request) return requestToJson(request)
.flatMap((json) => .flatMap(validateJsonKeys(['name', 'projectId'])) // Add required fields
validateJsonKeys(json, ['name', 'projectId'])) // Add required fields
.flatMap((json) => decodeJson(json, ProjectTaskCreateDto.fromJson)) .flatMap((json) => decodeJson(json, ProjectTaskCreateDto.fromJson))
.flatMap(mapper.fromCreateTo) .flatMap(mapper.fromCreateTo)
.flatMap(tasks.create) .flatMap(tasks.create)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.put('/<taskId>') @Route.put('/<taskId>')
Future<Response> updateTask(Request request, String taskId) async { Future<Response> updateTask(Request request, String taskId) async {
return requestToJson(request) return requestToJson(request)
.flatMap((json) => validateJsonKeys(json, [])) .flatMap(validateJsonKeys([]))
.flatMap((json) => decodeJson(json, ProjectTaskUpdateDto.fromJson)) .flatMap((json) => decodeJson(json, ProjectTaskUpdateDto.fromJson))
.flatMap((dto) => mapper.fromUpdateTo(dto, taskId)) .flatMap((dto) => mapper.fromUpdateTo(dto, taskId))
.flatMap(tasks.update) .flatMap(tasks.update)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@ -68,8 +67,9 @@ class ProjectTaskService {
Future<Response> deleteTask(Request request, String taskId) async { Future<Response> deleteTask(Request request, String taskId) async {
return tasks return tasks
.delete(taskId) .delete(taskId)
.match((left) => ResponseHelpers.fromError(left), .flatMap(mapper.to)
(right) => Response.ok('Task deleted')) .map((dto) => dto.toJson())
.toResponse()
.run(); .run();
} }

View File

@ -22,8 +22,8 @@ class TimeEntryService {
.findAll() .findAll()
.flatMap(mapper.listTo) .flatMap(mapper.listTo)
.map((entries) => entries.map((entry) => entry.toJson()).toList()) .map((entries) => entries.map((entry) => entry.toJson()).toList())
.match((left) => ResponseHelpers.fromError(left), .map(jsonEncode)
(right) => Response.ok(jsonEncode(right))) .toResponse()
.run(); .run();
} }
@ -32,35 +32,35 @@ class TimeEntryService {
return entries return entries
.findById(entryId) .findById(entryId)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.post('/') @Route.post('/')
Future<Response> createEntry(Request request) async { Future<Response> createEntry(Request request) async {
return requestToJson(request) return requestToJson(request)
.flatMap((json) => validateJsonKeys( .flatMap(
json, ['startTime', 'endTime', 'userId', 'projectId'])) validateJsonKeys(['startTime', 'endTime', 'userId', 'projectId']))
.flatMap((json) => decodeJson(json, TimeEntryCreateDto.fromJson)) .flatMap((json) => decodeJson(json, TimeEntryCreateDto.fromJson))
.flatMap(mapper.fromCreateTo) .flatMap(mapper.fromCreateTo)
.flatMap(entries.create) .flatMap(entries.create)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.put('/<entryId>') @Route.put('/<entryId>')
Future<Response> updateEntry(Request request, String entryId) async { Future<Response> updateEntry(Request request, String entryId) async {
return requestToJson(request) return requestToJson(request)
.flatMap((json) => validateJsonKeys(json, [])) .flatMap(validateJsonKeys([]))
.flatMap((json) => decodeJson(json, TimeEntryUpdateDto.fromJson)) .flatMap((json) => decodeJson(json, TimeEntryUpdateDto.fromJson))
.flatMap((dto) => mapper.fromUpdateTo(dto, entryId)) .flatMap((dto) => mapper.fromUpdateTo(dto, entryId))
.flatMap(entries.update) .flatMap(entries.update)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@ -68,8 +68,8 @@ class TimeEntryService {
Future<Response> deleteEntry(Request request, String entryId) async { Future<Response> deleteEntry(Request request, String entryId) async {
return entries return entries
.delete(entryId) .delete(entryId)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => Response.ok('Entry deleted')) .toResponse()
.run(); .run();
} }

View File

@ -22,8 +22,8 @@ class UserService {
.findAll() .findAll()
.flatMap(mapper.listTo) .flatMap(mapper.listTo)
.map((users) => users.map((user) => user.toJson()).toList()) .map((users) => users.map((user) => user.toJson()).toList())
.match((left) => ResponseHelpers.fromError(left), .map(jsonEncode)
(right) => Response.ok(jsonEncode(right))) .toResponse()
.run(); .run();
} }
@ -32,35 +32,34 @@ class UserService {
return users return users
.findById(userId) .findById(userId)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.post('/') @Route.post('/')
Future<Response> createUser(Request request) async { Future<Response> createUser(Request request) async {
return requestToJson(request) return requestToJson(request)
.flatMap( .flatMap(validateJsonKeys(['name', 'email', 'password']))
(json) => validateJsonKeys(json, ['name', 'email', 'password']))
.flatMap((json) => decodeJson(json, UserCreateDto.fromJson)) .flatMap((json) => decodeJson(json, UserCreateDto.fromJson))
.flatMap(mapper.fromCreateTo) .flatMap(mapper.fromCreateTo)
.flatMap(users.create) .flatMap(users.create)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@Route.put('/<userId>') @Route.put('/<userId>')
Future<Response> updateUser(Request request, String userId) async { Future<Response> updateUser(Request request, String userId) async {
return requestToJson(request) return requestToJson(request)
.flatMap((json) => validateJsonKeys(json, [])) .flatMap(validateJsonKeys([]))
.flatMap((json) => decodeJson(json, UserUpdateDto.fromJson)) .flatMap((json) => decodeJson(json, UserUpdateDto.fromJson))
.flatMap((dto) => mapper.fromUpdateTo(dto, userId)) .flatMap((dto) => mapper.fromUpdateTo(dto, userId))
.flatMap(users.update) .flatMap(users.update)
.flatMap(mapper.to) .flatMap(mapper.to)
.match((left) => ResponseHelpers.fromError(left), .map((dto) => dto.toJson())
(right) => ResponseHelpers.jsonOk(right.toJson())) .toResponse()
.run(); .run();
} }
@ -68,8 +67,9 @@ class UserService {
Future<Response> deleteUser(Request request, String userId) async { Future<Response> deleteUser(Request request, String userId) async {
return users return users
.delete(userId) .delete(userId)
.match((left) => ResponseHelpers.fromError(left), .flatMap(mapper.to)
(right) => Response.ok('user deleted')) .map((dto) => dto.toJson())
.toResponse()
.run(); .run();
} }

View File

@ -1,8 +1,24 @@
import 'dart:convert'; import 'dart:convert';
import 'package:backend_dart/domain/errors/error_code.dart'; import 'package:backend_dart/domain/errors/error_code.dart';
import 'package:backend_dart/domain/interface/error.dart'; import 'package:backend_dart/domain/interface/error.dart';
import 'package:fpdart/fpdart.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
extension TaskEitherResponseExtensions
on TaskEither<IError, Map<String, dynamic>> {
Task<Response> toResponse() => match(
(left) => ResponseHelpers.fromError(left),
(right) => ResponseHelpers.jsonOk(right),
);
}
extension TaskEitherResponseExtensionsFromString on TaskEither<IError, String> {
Task<Response> toResponse() => match(
(left) => ResponseHelpers.fromError(left),
(right) => Response.ok(right),
);
}
class ResponseHelpers { class ResponseHelpers {
/// Sendet eine JSON-Antwort mit einem 200-Statuscode /// Sendet eine JSON-Antwort mit einem 200-Statuscode
static Response jsonOk(Map<String, dynamic> data) { static Response jsonOk(Map<String, dynamic> data) {

View File

@ -1,22 +1,18 @@
import 'package:backend_dart/domain/errors/app_error.dart'; import 'package:backend_dart/domain/errors/app_error.dart';
import 'package:backend_dart/domain/interface/error.dart';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
TaskEither<IError, Map<String, dynamic>> validateJsonKeys( TaskEither<AppError, Map<String, dynamic>> Function(Map<String, dynamic>)
Map<String, dynamic> json, List<String> requiredKeys) { validateJsonKeys(List<String> requiredKeys) {
return TaskEither.tryCatch( return (json) {
() async {
final missingKeys = final missingKeys =
requiredKeys.where((key) => !json.containsKey(key)).toList(); requiredKeys.where((key) => !json.containsKey(key)).toList();
if (missingKeys.isNotEmpty) { if (missingKeys.isNotEmpty) {
throw Exception('Missing required keys: ${missingKeys.join(', ')}'); return TaskEither.left(AppError.validationError(
message: 'Missing required keys: ${missingKeys.join(', ')}',
));
} }
return json; return TaskEither.right(json);
}, };
(error, _) => AppError.validationError(
message: 'Failed to validate JSON keys: ${error.toString()}',
),
);
} }

View File

@ -11,7 +11,7 @@ abstract class UserDataSource {
TaskEither<IError, User> update(UserUpdate user); TaskEither<IError, User> update(UserUpdate user);
TaskEither<IError, void> delete(String id); TaskEither<IError, User> delete(String id);
TaskEither<IError, List<User>> findAll(); TaskEither<IError, List<User>> findAll();

View File

@ -13,7 +13,7 @@ abstract class ProjectRepository {
TaskEither<IError, Project> update(ProjectUpdate project); TaskEither<IError, Project> update(ProjectUpdate project);
/// Deletes a project by its unique ID. /// Deletes a project by its unique ID.
TaskEither<IError, void> delete(String id); TaskEither<IError, Project> delete(String id);
/// Finds all projects. /// Finds all projects.
TaskEither<IError, List<Project>> findAll(); TaskEither<IError, List<Project>> findAll();

View File

@ -16,7 +16,7 @@ abstract class ProjectTaskRepository {
TaskEither<IError, ProjectTask> update(ProjectTaskUpdate task); TaskEither<IError, ProjectTask> update(ProjectTaskUpdate task);
/// Deletes a project task by its unique ID. /// Deletes a project task by its unique ID.
TaskEither<IError, void> delete(String id); TaskEither<IError, ProjectTask> delete(String id);
/// Finds all project tasks. /// Finds all project tasks.
TaskEither<IError, List<ProjectTask>> findAll(); TaskEither<IError, List<ProjectTask>> findAll();

View File

@ -19,7 +19,7 @@ abstract class TimeEntryRepository {
TaskEither<IError, TimeEntry> update(TimeEntryUpdate timeEntry); TaskEither<IError, TimeEntry> update(TimeEntryUpdate timeEntry);
/// Deletes a time entry by its unique ID. /// Deletes a time entry by its unique ID.
TaskEither<IError, void> delete(String id); TaskEither<IError, TimeEntry> delete(String id);
/// Finds all time entries. /// Finds all time entries.
TaskEither<IError, List<TimeEntry>> findAll(); TaskEither<IError, List<TimeEntry>> findAll();

View File

@ -11,7 +11,7 @@ abstract class UserRepository {
TaskEither<IError, User> update(UserUpdate user); TaskEither<IError, User> update(UserUpdate user);
TaskEither<IError, void> delete(String id); TaskEither<IError, User> delete(String id);
TaskEither<IError, List<User>> findAll(); TaskEither<IError, List<User>> findAll();
} }

View File

@ -45,9 +45,13 @@ class PrismaUserDataSource implements UserDataSource {
message: 'Failed to find user by email: ${error.toString()}', message: 'Failed to find user by email: ${error.toString()}',
), ),
) )
.flatMap(errorOnNull(AppError.notFound( .flatMap(
errorOnNull(
AppError.notFound(
'User with email $email found', 'User with email $email found',
))) ),
),
)
.flatMap(mapper.from); .flatMap(mapper.from);
} }
@ -63,9 +67,13 @@ class PrismaUserDataSource implements UserDataSource {
message: 'Failed to find user by ID: ${error.toString()}', message: 'Failed to find user by ID: ${error.toString()}',
), ),
) )
.flatMap(errorOnNull(AppError.notFound( .flatMap(
errorOnNull(
AppError.notFound(
"User with id $id not found", "User with id $id not found",
))) ),
),
)
.flatMap(mapper.from); .flatMap(mapper.from);
} }
@ -104,15 +112,24 @@ class PrismaUserDataSource implements UserDataSource {
} }
@override @override
TaskEither<IError, void> delete(String id) { TaskEither<IError, User> delete(String id) {
return TaskEither.tryCatch( return TaskEither<IError, UserDbo?>.tryCatch(
() async { () async {
await prisma.userDbo.delete(where: UserDboWhereUniqueInput(id: id)); return await prisma.userDbo
.delete(where: UserDboWhereUniqueInput(id: id));
}, },
(error, _) => AppError.databaseError( (error, _) => AppError.databaseError(
message: 'Failed to delete user: ${error.toString()}', message: 'Failed to delete user: ${error.toString()}',
), ),
); )
.flatMap(
errorOnNull(
AppError.notFound(
'User not found',
),
),
)
.flatMap(mapper.from);
} }
@override @override