From 8559b1c44e1a614f3ee0da9d7e8925a02400bf15 Mon Sep 17 00:00:00 2001 From: Jean Jacques Avril Date: Wed, 1 Jan 2025 18:33:53 +0000 Subject: [PATCH] implemented user service (no auth!) --- .../lib/application/dto/user_dto.dart | 29 -- .../lib/application/repository/provider.dart | 9 + .../repository/user_repository_impl.dart | 45 +++ .../lib/application/service/dto/user_dto.dart | 20 ++ .../service/dto/user_dto.freezed.dart | 268 ++++++++++++++++++ .../{ => service}/dto/user_dto.g.dart | 14 +- .../service/mapper/user_dto_mapper.dart | 35 +++ .../lib/application/service/user_service.dart | 88 ++++++ .../user_service.g.dart | 15 + .../service/user_service_provider.dart | 8 + .../user_service/user_service.dart | 27 -- .../user_service/user_service_provider.dart | 8 - backend-dart/lib/common/error_on_null.dart | 8 + backend-dart/lib/common/response_helpers.dart | 84 ++++++ .../lib/domain/data/user_data_source.dart | 18 ++ backend-dart/lib/domain/entities/user.dart | 7 +- .../lib/domain/entities/user.freezed.dart | 247 ++++++++++++++++ backend-dart/lib/domain/errors/app_error.dart | 51 ++++ .../lib/domain/errors/app_error.freezed.dart | 239 ++++++++++++++++ .../lib/domain/errors/error_code.dart | 15 + .../lib/domain/interface/database.dart | 7 + backend-dart/lib/domain/interface/error.dart | 8 + backend-dart/lib/domain/interface/mapper.dart | 16 ++ .../domain/repositories/user_repository.dart | 7 - .../domain/repository/user_repository.dart | 17 ++ .../infrastructure/persistence/database.dart | 13 - .../persistence/database_provider.dart | 8 +- .../persistence/mapper/user_dbo_mapper.dart | 37 +++ .../persistence/prisma_database.dart | 19 ++ .../persistence/prisma_user_data_source.dart | 162 +++++++++++ .../persistence/prisma_user_repository.dart | 57 ---- backend-dart/lib/interfaces/http/router.dart | 6 +- backend-dart/pubspec.lock | 2 +- backend-dart/pubspec.yaml | 1 + 34 files changed, 1437 insertions(+), 158 deletions(-) delete mode 100644 backend-dart/lib/application/dto/user_dto.dart create mode 100644 backend-dart/lib/application/repository/provider.dart create mode 100755 backend-dart/lib/application/repository/user_repository_impl.dart create mode 100644 backend-dart/lib/application/service/dto/user_dto.dart create mode 100644 backend-dart/lib/application/service/dto/user_dto.freezed.dart rename backend-dart/lib/application/{ => service}/dto/user_dto.g.dart (65%) create mode 100644 backend-dart/lib/application/service/mapper/user_dto_mapper.dart create mode 100644 backend-dart/lib/application/service/user_service.dart rename backend-dart/lib/application/{user_service => service}/user_service.g.dart (69%) create mode 100644 backend-dart/lib/application/service/user_service_provider.dart delete mode 100644 backend-dart/lib/application/user_service/user_service.dart delete mode 100644 backend-dart/lib/application/user_service/user_service_provider.dart create mode 100644 backend-dart/lib/common/error_on_null.dart create mode 100644 backend-dart/lib/common/response_helpers.dart create mode 100644 backend-dart/lib/domain/data/user_data_source.dart create mode 100644 backend-dart/lib/domain/entities/user.freezed.dart create mode 100644 backend-dart/lib/domain/errors/app_error.dart create mode 100644 backend-dart/lib/domain/errors/app_error.freezed.dart create mode 100644 backend-dart/lib/domain/errors/error_code.dart create mode 100644 backend-dart/lib/domain/interface/database.dart create mode 100644 backend-dart/lib/domain/interface/error.dart create mode 100644 backend-dart/lib/domain/interface/mapper.dart delete mode 100755 backend-dart/lib/domain/repositories/user_repository.dart create mode 100755 backend-dart/lib/domain/repository/user_repository.dart delete mode 100644 backend-dart/lib/infrastructure/persistence/database.dart create mode 100644 backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart create mode 100644 backend-dart/lib/infrastructure/persistence/prisma_database.dart create mode 100755 backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart delete mode 100755 backend-dart/lib/infrastructure/persistence/prisma_user_repository.dart diff --git a/backend-dart/lib/application/dto/user_dto.dart b/backend-dart/lib/application/dto/user_dto.dart deleted file mode 100644 index 91de950..0000000 --- a/backend-dart/lib/application/dto/user_dto.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'user_dto.g.dart'; - -@JsonSerializable() -class UserDto { - final String id; - final String name; - final String email; - - // Optional: Füge zusätzliche Felder wie Timestamps hinzu - final DateTime? createdAt; - final DateTime? updatedAt; - - UserDto({ - required this.id, - required this.name, - required this.email, - this.createdAt, - this.updatedAt, - }); - - /// Factory-Methode zur Deserialisierung von JSON - factory UserDto.fromJson(Map json) => - _$UserDtoFromJson(json); - - /// Methode zur Serialisierung nach JSON - Map toJson() => _$UserDtoToJson(this); -} diff --git a/backend-dart/lib/application/repository/provider.dart b/backend-dart/lib/application/repository/provider.dart new file mode 100644 index 0000000..13706b3 --- /dev/null +++ b/backend-dart/lib/application/repository/provider.dart @@ -0,0 +1,9 @@ +import 'package:backend_dart/application/repository/user_repository_impl.dart'; +import 'package:backend_dart/domain/repository/user_repository.dart'; +import 'package:backend_dart/infrastructure/persistence/database_provider.dart'; +import 'package:riverpod/riverpod.dart'; + +final userRepoProvider = Provider((ref) { + final database = ref.read(databaseProvider); + return UserRepositoryImpl(database); +}); diff --git a/backend-dart/lib/application/repository/user_repository_impl.dart b/backend-dart/lib/application/repository/user_repository_impl.dart new file mode 100755 index 0000000..7635f98 --- /dev/null +++ b/backend-dart/lib/application/repository/user_repository_impl.dart @@ -0,0 +1,45 @@ +import 'package:backend_dart/domain/entities/user.dart'; +import 'package:backend_dart/domain/interface/database.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:backend_dart/domain/repository/user_repository.dart'; +import 'package:fpdart/fpdart.dart'; + +class UserRepositoryImpl implements UserRepository { + final IDatabase database; + UserRepositoryImpl(this.database); + + @override + TaskEither create(User user) { + return database.users + .generateId() + .map( + (id) => user.copyWith(id: id), + ) + .flatMap(database.users.create); + } + + @override + TaskEither findByEmail(String email) { + return database.users.findByEmail(email); + } + + @override + TaskEither findById(String id) { + return database.users.findById(id); + } + + @override + TaskEither update(User user) { + return database.users.update(user); + } + + @override + TaskEither delete(String id) { + return database.users.delete(id); + } + + @override + TaskEither> findAll() { + return database.users.findAll(); + } +} diff --git a/backend-dart/lib/application/service/dto/user_dto.dart b/backend-dart/lib/application/service/dto/user_dto.dart new file mode 100644 index 0000000..27ca162 --- /dev/null +++ b/backend-dart/lib/application/service/dto/user_dto.dart @@ -0,0 +1,20 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_dto.freezed.dart'; +part 'user_dto.g.dart'; + +@freezed +class UserDto with _$UserDto { + const factory UserDto({ + String? id, + String? name, + String? email, + String? password, + DateTime? createdAt, + DateTime? updatedAt, + }) = _UserDto; + + /// JSON-Serialisierung + factory UserDto.fromJson(Map json) => + _$UserDtoFromJson(json); +} diff --git a/backend-dart/lib/application/service/dto/user_dto.freezed.dart b/backend-dart/lib/application/service/dto/user_dto.freezed.dart new file mode 100644 index 0000000..867047b --- /dev/null +++ b/backend-dart/lib/application/service/dto/user_dto.freezed.dart @@ -0,0 +1,268 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +UserDto _$UserDtoFromJson(Map json) { + return _UserDto.fromJson(json); +} + +/// @nodoc +mixin _$UserDto { + String? get id => throw _privateConstructorUsedError; + String? get name => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; + String? get password => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this UserDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserDtoCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserDtoCopyWith<$Res> { + factory $UserDtoCopyWith(UserDto value, $Res Function(UserDto) then) = + _$UserDtoCopyWithImpl<$Res, UserDto>; + @useResult + $Res call( + {String? id, + String? name, + String? email, + String? password, + DateTime? createdAt, + DateTime? updatedAt}); +} + +/// @nodoc +class _$UserDtoCopyWithImpl<$Res, $Val extends UserDto> + implements $UserDtoCopyWith<$Res> { + _$UserDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then(_value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserDtoImplCopyWith<$Res> implements $UserDtoCopyWith<$Res> { + factory _$$UserDtoImplCopyWith( + _$UserDtoImpl value, $Res Function(_$UserDtoImpl) then) = + __$$UserDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? id, + String? name, + String? email, + String? password, + DateTime? createdAt, + DateTime? updatedAt}); +} + +/// @nodoc +class __$$UserDtoImplCopyWithImpl<$Res> + extends _$UserDtoCopyWithImpl<$Res, _$UserDtoImpl> + implements _$$UserDtoImplCopyWith<$Res> { + __$$UserDtoImplCopyWithImpl( + _$UserDtoImpl _value, $Res Function(_$UserDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then(_$UserDtoImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserDtoImpl implements _UserDto { + const _$UserDtoImpl( + {this.id, + this.name, + this.email, + this.password, + this.createdAt, + this.updatedAt}); + + factory _$UserDtoImpl.fromJson(Map json) => + _$$UserDtoImplFromJson(json); + + @override + final String? id; + @override + final String? name; + @override + final String? email; + @override + final String? password; + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'UserDto(id: $id, name: $name, email: $email, password: $password, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserDtoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, name, email, password, createdAt, updatedAt); + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserDtoImplCopyWith<_$UserDtoImpl> get copyWith => + __$$UserDtoImplCopyWithImpl<_$UserDtoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserDtoImplToJson( + this, + ); + } +} + +abstract class _UserDto implements UserDto { + const factory _UserDto( + {final String? id, + final String? name, + final String? email, + final String? password, + final DateTime? createdAt, + final DateTime? updatedAt}) = _$UserDtoImpl; + + factory _UserDto.fromJson(Map json) = _$UserDtoImpl.fromJson; + + @override + String? get id; + @override + String? get name; + @override + String? get email; + @override + String? get password; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserDtoImplCopyWith<_$UserDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/backend-dart/lib/application/dto/user_dto.g.dart b/backend-dart/lib/application/service/dto/user_dto.g.dart similarity index 65% rename from backend-dart/lib/application/dto/user_dto.g.dart rename to backend-dart/lib/application/service/dto/user_dto.g.dart index bbaa65b..a71206c 100644 --- a/backend-dart/lib/application/dto/user_dto.g.dart +++ b/backend-dart/lib/application/service/dto/user_dto.g.dart @@ -6,10 +6,12 @@ part of 'user_dto.dart'; // JsonSerializableGenerator // ************************************************************************** -UserDto _$UserDtoFromJson(Map json) => UserDto( - id: json['id'] as String, - name: json['name'] as String, - email: json['email'] as String, +_$UserDtoImpl _$$UserDtoImplFromJson(Map json) => + _$UserDtoImpl( + id: json['id'] as String?, + name: json['name'] as String?, + email: json['email'] as String?, + password: json['password'] as String?, createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String), @@ -18,10 +20,12 @@ UserDto _$UserDtoFromJson(Map json) => UserDto( : DateTime.parse(json['updatedAt'] as String), ); -Map _$UserDtoToJson(UserDto instance) => { +Map _$$UserDtoImplToJson(_$UserDtoImpl instance) => + { 'id': instance.id, 'name': instance.name, 'email': instance.email, + 'password': instance.password, 'createdAt': instance.createdAt?.toIso8601String(), 'updatedAt': instance.updatedAt?.toIso8601String(), }; diff --git a/backend-dart/lib/application/service/mapper/user_dto_mapper.dart b/backend-dart/lib/application/service/mapper/user_dto_mapper.dart new file mode 100644 index 0000000..30a8d00 --- /dev/null +++ b/backend-dart/lib/application/service/mapper/user_dto_mapper.dart @@ -0,0 +1,35 @@ +import 'package:backend_dart/application/service/dto/user_dto.dart'; +import 'package:backend_dart/domain/entities/user.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:backend_dart/domain/interface/mapper.dart'; +import 'package:fpdart/fpdart.dart'; + +class UserDtoMapper implements IMapper { + @override + TaskEither to(User entity) => TaskEither.of(UserDto( + id: entity.id, + name: entity.name, + email: entity.email, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + )); + @override + TaskEither from(UserDto dto) => TaskEither.of(User( + id: dto.id, + name: dto.name, + email: dto.email, + password: dto.password, + createdAt: dto.createdAt, + updatedAt: dto.updatedAt, + )); + + @override + TaskEither> listFrom(Iterable targets) { + return TaskEither.traverseList(targets.toList(), from); + } + + @override + TaskEither> listTo(Iterable origins) { + return TaskEither.traverseList(origins.toList(), to); + } +} diff --git a/backend-dart/lib/application/service/user_service.dart b/backend-dart/lib/application/service/user_service.dart new file mode 100644 index 0000000..b825d97 --- /dev/null +++ b/backend-dart/lib/application/service/user_service.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +import 'package:backend_dart/application/service/dto/user_dto.dart'; +import 'package:backend_dart/application/service/mapper/user_dto_mapper.dart'; +import 'package:backend_dart/common/response_helpers.dart'; +import 'package:backend_dart/domain/errors/app_error.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:backend_dart/domain/repository/user_repository.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +part 'user_service.g.dart'; // generated with 'pub run build_runner build' + +class UserService { + final UserRepository users; + final UserDtoMapper mapper = UserDtoMapper(); + UserService(this.users); + + @Route.get('/') + Future listUsers(Request request) async { + return users + .findAll() + .flatMap(mapper.listTo) + .map((users) => users.map((user) => user.toJson()).toList()) + .match((left) => ResponseHelpers.fromError(left), + (right) => Response.ok(jsonEncode(right))) + .run(); + } + + @Route.get('/') + Future fetchUser(Request request, String userId) async { + return users + .findById(userId) + .flatMap(mapper.to) + .match((left) => ResponseHelpers.fromError(left), + (right) => ResponseHelpers.jsonOk(right.toJson())) + .run(); + } + + @Route.post('/') + Future createUser(Request request) async { + return TaskEither.tryCatch(() async { + final json = jsonDecode(await request.readAsString()); + final user = UserDto.fromJson(json); + return user; + }, (error, stack) { + return AppError.inputError( + message: 'Failed to create user: ${error.toString()}'); + }) + .flatMap(mapper.from) + .flatMap(users.create) + .flatMap(mapper.to) + .match((left) => ResponseHelpers.fromError(left), + (right) => ResponseHelpers.jsonOk(right.toJson())) + .run(); + } + + @Route.put('/') + Future updateUser(Request request, String userId) async { + return TaskEither.tryCatch(() async { + final json = jsonDecode(await request.readAsString()); + final user = UserDto.fromJson(json); + return user; + }, (error, stack) { + return AppError.inputError( + message: 'Failed to update user: ${error.toString()}'); + }) + .flatMap(mapper.from) + .flatMap(users.update) + .flatMap(mapper.to) + .match((left) => ResponseHelpers.fromError(left), + (right) => ResponseHelpers.jsonOk(right.toJson())) + .run(); + } + + @Route.delete('/') + Future deleteUser(Request request, String userId) async { + return users + .delete(userId) + .match((left) => ResponseHelpers.fromError(left), + (right) => Response.ok('user deleted')) + .run(); + } + + // Create router using the generate function defined in 'userservice.g.dart'. + Router get router => _$UserServiceRouter(this); +} diff --git a/backend-dart/lib/application/user_service/user_service.g.dart b/backend-dart/lib/application/service/user_service.g.dart similarity index 69% rename from backend-dart/lib/application/user_service/user_service.g.dart rename to backend-dart/lib/application/service/user_service.g.dart index d9b81e3..9081f58 100644 --- a/backend-dart/lib/application/user_service/user_service.g.dart +++ b/backend-dart/lib/application/service/user_service.g.dart @@ -18,5 +18,20 @@ Router _$UserServiceRouter(UserService service) { r'/', service.fetchUser, ); + router.add( + 'POST', + r'/', + service.createUser, + ); + router.add( + 'PUT', + r'/', + service.updateUser, + ); + router.add( + 'DELETE', + r'/', + service.deleteUser, + ); return router; } diff --git a/backend-dart/lib/application/service/user_service_provider.dart b/backend-dart/lib/application/service/user_service_provider.dart new file mode 100644 index 0000000..a248727 --- /dev/null +++ b/backend-dart/lib/application/service/user_service_provider.dart @@ -0,0 +1,8 @@ +import 'package:backend_dart/application/repository/provider.dart'; +import 'package:backend_dart/application/service/user_service.dart'; +import 'package:riverpod/riverpod.dart'; + +final userServiceProvider = Provider((ref) { + final database = ref.read(userRepoProvider); + return UserService(database); +}); diff --git a/backend-dart/lib/application/user_service/user_service.dart b/backend-dart/lib/application/user_service/user_service.dart deleted file mode 100644 index d13e7ab..0000000 --- a/backend-dart/lib/application/user_service/user_service.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:backend_dart/infrastructure/persistence/database.dart'; -import 'package:shelf/shelf.dart'; -import 'package:shelf_router/shelf_router.dart'; - -part 'user_service.g.dart'; // generated with 'pub run build_runner build' - -class UserService { - final Database database; - UserService(this.database); - - @Route.get('/') - Future listUsers(Request request) async { - return Response.ok('["user1"]'); - } - - @Route.get('/') - Future fetchUser(Request request, String userId) async { - final result = await database.users.findById(userId); - if (result != null) { - return Response.ok(result); - } - return Response.notFound('no such user'); - } - - // Create router using the generate function defined in 'userservice.g.dart'. - Router get router => _$UserServiceRouter(this); -} diff --git a/backend-dart/lib/application/user_service/user_service_provider.dart b/backend-dart/lib/application/user_service/user_service_provider.dart deleted file mode 100644 index 29c363d..0000000 --- a/backend-dart/lib/application/user_service/user_service_provider.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:backend_dart/application/user_service/user_service.dart'; -import 'package:backend_dart/infrastructure/persistence/database_provider.dart'; -import 'package:riverpod/riverpod.dart'; - -final userServiceProvider = Provider((ref) { - final database = ref.read(databaseProvider); - return UserService(database); -}); diff --git a/backend-dart/lib/common/error_on_null.dart b/backend-dart/lib/common/error_on_null.dart new file mode 100644 index 0000000..c8ddfd9 --- /dev/null +++ b/backend-dart/lib/common/error_on_null.dart @@ -0,0 +1,8 @@ +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:fpdart/fpdart.dart'; + +TaskEither Function(T?) errorOnNull(IError error) { + return (T? value) { + return value != null ? TaskEither.of(value) : TaskEither.left(error); + }; +} diff --git a/backend-dart/lib/common/response_helpers.dart b/backend-dart/lib/common/response_helpers.dart new file mode 100644 index 0000000..895dafe --- /dev/null +++ b/backend-dart/lib/common/response_helpers.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; +import 'package:backend_dart/domain/errors/error_code.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:shelf/shelf.dart'; + +class ResponseHelpers { + /// Sendet eine JSON-Antwort mit einem 200-Statuscode + static Response jsonOk(Map data) { + return Response.ok( + jsonEncode(data), + headers: {'Content-Type': 'application/json'}, + ); + } + + /// Sendet eine JSON-Antwort mit einem spezifischen Statuscode + static Response jsonWithStatus(Map data, int statusCode) { + return Response( + statusCode, + body: jsonEncode(data), + headers: {'Content-Type': 'application/json'}, + ); + } + + /// Sendet eine JSON-Antwort für einen Fehler (z. B. 404 oder 500) + static Response jsonError(String message, {int statusCode = 500}) { + return Response( + statusCode, + body: jsonEncode({'error': message}), + headers: {'Content-Type': 'application/json'}, + ); + } + + /// Sendet eine einfache Textantwort + static Response textResponse(String message, {int statusCode = 200}) { + return Response( + statusCode, + body: message, + headers: {'Content-Type': 'text/plain'}, + ); + } + + static Response fromError(IError error) { + // Print error to console + print('Error: ${error.code.name} - ${error.message}'); + //print(error.stackTrace); + return Response( + mapErrorCodeToHttpStatus(error.code), + body: jsonEncode({ + 'code': error.code.name, + 'error': error.message, + 'details': error.details, + }), + headers: {'Content-Type': 'application/json'}, + ); + } +} + +int mapErrorCodeToHttpStatus(ErrorCode errorCode) { + switch (errorCode) { + case ErrorCode.networkError: + return 503; // Service Unavailable + case ErrorCode.validationError: + case ErrorCode.inputError: + return 400; // Bad Request + case ErrorCode.unexpectedError: + case ErrorCode.internalError: + case ErrorCode.databaseError: + return 500; // Internal Server Error + case ErrorCode.authenticationError: + return 401; // Unauthorized + case ErrorCode.permissionDenied: + return 403; // Forbidden + case ErrorCode.notFound: + return 404; // Not Found + case ErrorCode.mappingError: + case ErrorCode.outputError: + return 422; // Unprocessable Entity + case ErrorCode.exception: + case ErrorCode.unknownError: + return 520; // Unknown Error (Custom) + default: + return 500; // Fallback: Internal Server Error + } +} diff --git a/backend-dart/lib/domain/data/user_data_source.dart b/backend-dart/lib/domain/data/user_data_source.dart new file mode 100644 index 0000000..f14aaa7 --- /dev/null +++ b/backend-dart/lib/domain/data/user_data_source.dart @@ -0,0 +1,18 @@ +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:fpdart/fpdart.dart'; + +abstract class UserDataSource { + TaskEither create(T user); + + TaskEither findByEmail(String email); + + TaskEither findById(String id); + + TaskEither update(T user); + + TaskEither delete(String id); + + TaskEither> findAll(); + + TaskEither generateId(); +} diff --git a/backend-dart/lib/domain/entities/user.dart b/backend-dart/lib/domain/entities/user.dart index 5d2a605..8e268c1 100755 --- a/backend-dart/lib/domain/entities/user.dart +++ b/backend-dart/lib/domain/entities/user.dart @@ -1,14 +1,13 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'user.freezed.dart'; -part 'user.g.dart'; @freezed class User with _$User { const factory User({ - required String id, - required String name, - required String email, + String? id, + String? name, + String? email, String? password, DateTime? createdAt, DateTime? updatedAt, diff --git a/backend-dart/lib/domain/entities/user.freezed.dart b/backend-dart/lib/domain/entities/user.freezed.dart new file mode 100644 index 0000000..e262253 --- /dev/null +++ b/backend-dart/lib/domain/entities/user.freezed.dart @@ -0,0 +1,247 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$User { + String? get id => throw _privateConstructorUsedError; + String? get name => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; + String? get password => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCopyWith<$Res> { + factory $UserCopyWith(User value, $Res Function(User) then) = + _$UserCopyWithImpl<$Res, User>; + @useResult + $Res call( + {String? id, + String? name, + String? email, + String? password, + DateTime? createdAt, + DateTime? updatedAt}); +} + +/// @nodoc +class _$UserCopyWithImpl<$Res, $Val extends User> + implements $UserCopyWith<$Res> { + _$UserCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then(_value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { + factory _$$UserImplCopyWith( + _$UserImpl value, $Res Function(_$UserImpl) then) = + __$$UserImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? id, + String? name, + String? email, + String? password, + DateTime? createdAt, + DateTime? updatedAt}); +} + +/// @nodoc +class __$$UserImplCopyWithImpl<$Res> + extends _$UserCopyWithImpl<$Res, _$UserImpl> + implements _$$UserImplCopyWith<$Res> { + __$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then) + : super(_value, _then); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then(_$UserImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + )); + } +} + +/// @nodoc + +class _$UserImpl implements _User { + const _$UserImpl( + {this.id, + this.name, + this.email, + this.password, + this.createdAt, + this.updatedAt}); + + @override + final String? id; + @override + final String? name; + @override + final String? email; + @override + final String? password; + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'User(id: $id, name: $name, email: $email, password: $password, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @override + int get hashCode => + Object.hash(runtimeType, id, name, email, password, createdAt, updatedAt); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserImplCopyWith<_$UserImpl> get copyWith => + __$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity); +} + +abstract class _User implements User { + const factory _User( + {final String? id, + final String? name, + final String? email, + final String? password, + final DateTime? createdAt, + final DateTime? updatedAt}) = _$UserImpl; + + @override + String? get id; + @override + String? get name; + @override + String? get email; + @override + String? get password; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserImplCopyWith<_$UserImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/backend-dart/lib/domain/errors/app_error.dart b/backend-dart/lib/domain/errors/app_error.dart new file mode 100644 index 0000000..0ea4fc6 --- /dev/null +++ b/backend-dart/lib/domain/errors/app_error.dart @@ -0,0 +1,51 @@ +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'error_code.dart'; + +part 'app_error.freezed.dart'; + +@freezed +class AppError with _$AppError implements IError { + const factory AppError({ + required ErrorCode code, + required String message, + Map? details, + StackTrace? stackTrace, + Object? originalError, + }) = _AppError; + + factory AppError.fromException(Exception e) { + return AppError( + code: ErrorCode.exception, + message: e.toString(), + originalError: e, + ); + } + + factory AppError.fromErrorCode(ErrorCode code, {String? message}) { + return AppError( + code: code, + message: message ?? "No message", + stackTrace: StackTrace.current); + } + + factory AppError.notFound(String? message) { + return AppError.fromErrorCode(ErrorCode.notFound, message: message); + } + + factory AppError.mappingError({String? message}) { + return AppError.fromErrorCode(ErrorCode.mappingError, message: message); + } + + factory AppError.databaseError({String? message}) { + return AppError.fromErrorCode(ErrorCode.databaseError, message: message); + } + + factory AppError.validationError({String? message}) { + return AppError.fromErrorCode(ErrorCode.validationError, message: message); + } + + factory AppError.inputError({String? message}) { + return AppError.fromErrorCode(ErrorCode.inputError, message: message); + } +} diff --git a/backend-dart/lib/domain/errors/app_error.freezed.dart b/backend-dart/lib/domain/errors/app_error.freezed.dart new file mode 100644 index 0000000..2c68322 --- /dev/null +++ b/backend-dart/lib/domain/errors/app_error.freezed.dart @@ -0,0 +1,239 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'app_error.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$AppError { + ErrorCode get code => throw _privateConstructorUsedError; + String get message => throw _privateConstructorUsedError; + Map? get details => throw _privateConstructorUsedError; + StackTrace? get stackTrace => throw _privateConstructorUsedError; + Object? get originalError => throw _privateConstructorUsedError; + + /// Create a copy of AppError + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AppErrorCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AppErrorCopyWith<$Res> { + factory $AppErrorCopyWith(AppError value, $Res Function(AppError) then) = + _$AppErrorCopyWithImpl<$Res, AppError>; + @useResult + $Res call( + {ErrorCode code, + String message, + Map? details, + StackTrace? stackTrace, + Object? originalError}); +} + +/// @nodoc +class _$AppErrorCopyWithImpl<$Res, $Val extends AppError> + implements $AppErrorCopyWith<$Res> { + _$AppErrorCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AppError + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? code = null, + Object? message = null, + Object? details = freezed, + Object? stackTrace = freezed, + Object? originalError = freezed, + }) { + return _then(_value.copyWith( + code: null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as ErrorCode, + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + details: freezed == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Map?, + stackTrace: freezed == stackTrace + ? _value.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as StackTrace?, + originalError: + freezed == originalError ? _value.originalError : originalError, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AppErrorImplCopyWith<$Res> + implements $AppErrorCopyWith<$Res> { + factory _$$AppErrorImplCopyWith( + _$AppErrorImpl value, $Res Function(_$AppErrorImpl) then) = + __$$AppErrorImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {ErrorCode code, + String message, + Map? details, + StackTrace? stackTrace, + Object? originalError}); +} + +/// @nodoc +class __$$AppErrorImplCopyWithImpl<$Res> + extends _$AppErrorCopyWithImpl<$Res, _$AppErrorImpl> + implements _$$AppErrorImplCopyWith<$Res> { + __$$AppErrorImplCopyWithImpl( + _$AppErrorImpl _value, $Res Function(_$AppErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of AppError + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? code = null, + Object? message = null, + Object? details = freezed, + Object? stackTrace = freezed, + Object? originalError = freezed, + }) { + return _then(_$AppErrorImpl( + code: null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as ErrorCode, + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + details: freezed == details + ? _value._details + : details // ignore: cast_nullable_to_non_nullable + as Map?, + stackTrace: freezed == stackTrace + ? _value.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as StackTrace?, + originalError: + freezed == originalError ? _value.originalError : originalError, + )); + } +} + +/// @nodoc + +class _$AppErrorImpl implements _AppError { + const _$AppErrorImpl( + {required this.code, + required this.message, + final Map? details, + this.stackTrace, + this.originalError}) + : _details = details; + + @override + final ErrorCode code; + @override + final String message; + final Map? _details; + @override + Map? get details { + final value = _details; + if (value == null) return null; + if (_details is EqualUnmodifiableMapView) return _details; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final StackTrace? stackTrace; + @override + final Object? originalError; + + @override + String toString() { + return 'AppError(code: $code, message: $message, details: $details, stackTrace: $stackTrace, originalError: $originalError)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AppErrorImpl && + (identical(other.code, code) || other.code == code) && + (identical(other.message, message) || other.message == message) && + const DeepCollectionEquality().equals(other._details, _details) && + (identical(other.stackTrace, stackTrace) || + other.stackTrace == stackTrace) && + const DeepCollectionEquality() + .equals(other.originalError, originalError)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + code, + message, + const DeepCollectionEquality().hash(_details), + stackTrace, + const DeepCollectionEquality().hash(originalError)); + + /// Create a copy of AppError + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AppErrorImplCopyWith<_$AppErrorImpl> get copyWith => + __$$AppErrorImplCopyWithImpl<_$AppErrorImpl>(this, _$identity); +} + +abstract class _AppError implements AppError { + const factory _AppError( + {required final ErrorCode code, + required final String message, + final Map? details, + final StackTrace? stackTrace, + final Object? originalError}) = _$AppErrorImpl; + + @override + ErrorCode get code; + @override + String get message; + @override + Map? get details; + @override + StackTrace? get stackTrace; + @override + Object? get originalError; + + /// Create a copy of AppError + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AppErrorImplCopyWith<_$AppErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/backend-dart/lib/domain/errors/error_code.dart b/backend-dart/lib/domain/errors/error_code.dart new file mode 100644 index 0000000..fa75d72 --- /dev/null +++ b/backend-dart/lib/domain/errors/error_code.dart @@ -0,0 +1,15 @@ +enum ErrorCode { + networkError, // Fehler bei der Netzwerkkommunikation + validationError, // Eingabevalidierungsfehler + unexpectedError, // Unerwarteter Fehler + authenticationError, // Authentifizierungsfehler + permissionDenied, // Berechtigungsfehler + notFound, // Ressource nicht gefunden + exception, // Ausnahme + mappingError, // Fehler beim Zuordnen von Daten + databaseError, // Datenbankfehler + inputError, // Eingabefehler + outputError, // Ausgabefehler + internalError, // Interner Fehler + unknownError, // Unbekannter Fehler +} diff --git a/backend-dart/lib/domain/interface/database.dart b/backend-dart/lib/domain/interface/database.dart new file mode 100644 index 0000000..9e4781c --- /dev/null +++ b/backend-dart/lib/domain/interface/database.dart @@ -0,0 +1,7 @@ +import 'package:backend_dart/domain/data/user_data_source.dart'; +import 'package:backend_dart/domain/entities/user.dart'; + +abstract class IDatabase { + UserDataSource get users; + Future close(); +} diff --git a/backend-dart/lib/domain/interface/error.dart b/backend-dart/lib/domain/interface/error.dart new file mode 100644 index 0000000..6cf5eab --- /dev/null +++ b/backend-dart/lib/domain/interface/error.dart @@ -0,0 +1,8 @@ +import 'package:backend_dart/domain/errors/error_code.dart'; + +abstract class IError { + ErrorCode get code; // Fehlercode + String get message; // Fehlermeldung + StackTrace? get stackTrace; // Stacktrace + Map? get details; // Zusätzliche Details +} diff --git a/backend-dart/lib/domain/interface/mapper.dart b/backend-dart/lib/domain/interface/mapper.dart new file mode 100644 index 0000000..8cffc2a --- /dev/null +++ b/backend-dart/lib/domain/interface/mapper.dart @@ -0,0 +1,16 @@ +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:fpdart/fpdart.dart'; + +abstract class IMapper { + /// Konvertiert von Typ U (Origin) zu Typ V (Target) + TaskEither to(U origin); + + /// Konvertiert von Typ V (Target) zu Typ U (Origin) + TaskEither from(V target); + + /// Konvertiert eine Liste von Typ U (Origin) zu einer Liste von Typ V (Target) + TaskEither> listTo(Iterable origins); + + /// Konvertiert eine Liste von Typ V (Target) zu einer Liste von Typ U (Origin) + TaskEither> listFrom(Iterable targets); +} diff --git a/backend-dart/lib/domain/repositories/user_repository.dart b/backend-dart/lib/domain/repositories/user_repository.dart deleted file mode 100755 index 4dfc622..0000000 --- a/backend-dart/lib/domain/repositories/user_repository.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:backend_dart/domain/entities/user.dart'; - -abstract class UserRepository { - Future create(User user); - Future findByEmail(String email); - Future findById(String id); -} diff --git a/backend-dart/lib/domain/repository/user_repository.dart b/backend-dart/lib/domain/repository/user_repository.dart new file mode 100755 index 0000000..1c599b1 --- /dev/null +++ b/backend-dart/lib/domain/repository/user_repository.dart @@ -0,0 +1,17 @@ +import 'package:backend_dart/domain/entities/user.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:fpdart/fpdart.dart'; + +abstract class UserRepository { + TaskEither create(User user); + + TaskEither findByEmail(String email); + + TaskEither findById(String id); + + TaskEither update(User user); + + TaskEither delete(String id); + + TaskEither> findAll(); +} diff --git a/backend-dart/lib/infrastructure/persistence/database.dart b/backend-dart/lib/infrastructure/persistence/database.dart deleted file mode 100644 index faf1e62..0000000 --- a/backend-dart/lib/infrastructure/persistence/database.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:backend_dart/infrastructure/persistence/db/client.dart'; -import 'package:backend_dart/infrastructure/persistence/prisma_user_repository.dart'; - -class Database { - final prisma = PrismaClient(); - late final PrismaUserRepository users; - Database() { - users = PrismaUserRepository(prisma); - print('Database initialized'); - } - - -} \ No newline at end of file diff --git a/backend-dart/lib/infrastructure/persistence/database_provider.dart b/backend-dart/lib/infrastructure/persistence/database_provider.dart index 3fe65fb..5737c5e 100644 --- a/backend-dart/lib/infrastructure/persistence/database_provider.dart +++ b/backend-dart/lib/infrastructure/persistence/database_provider.dart @@ -1,8 +1,8 @@ -import 'package:backend_dart/infrastructure/persistence/database.dart'; +import 'package:backend_dart/infrastructure/persistence/prisma_database.dart'; import 'package:riverpod/riverpod.dart'; -final databaseProvider = Provider((ref) { +final databaseProvider = Provider((ref) { // Hier die Datenbankverbindung initialisieren - final database = Database(); + final database = PrismaDatabase(); return database; -}); \ No newline at end of file +}); diff --git a/backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart b/backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart new file mode 100644 index 0000000..c37f70f --- /dev/null +++ b/backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart @@ -0,0 +1,37 @@ +import 'package:backend_dart/domain/entities/user.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:backend_dart/domain/interface/mapper.dart'; +import 'package:backend_dart/infrastructure/persistence/db/model.dart'; +import 'package:fpdart/fpdart.dart'; + +class UserDboMapper implements IMapper { + @override + TaskEither from(UserDbo target) => TaskEither.of(User( + id: target.id!, + name: target.name!, + email: target.email!, + password: target.password, + createdAt: target.createdAt, + updatedAt: target.updatedAt, + )); + + @override + TaskEither> listFrom(Iterable targets) { + return TaskEither.traverseList(targets.toList(), from); + } + + @override + TaskEither> listTo(Iterable origins) { + return TaskEither.traverseList(origins.toList(), to); + } + + @override + TaskEither to(User origin) => TaskEither.of(UserDbo( + id: origin.id, + name: origin.name, + email: origin.email, + password: origin.password, + createdAt: origin.createdAt, + updatedAt: origin.updatedAt, + )); +} diff --git a/backend-dart/lib/infrastructure/persistence/prisma_database.dart b/backend-dart/lib/infrastructure/persistence/prisma_database.dart new file mode 100644 index 0000000..8276b9d --- /dev/null +++ b/backend-dart/lib/infrastructure/persistence/prisma_database.dart @@ -0,0 +1,19 @@ +import 'package:backend_dart/domain/interface/database.dart'; +import 'package:backend_dart/infrastructure/persistence/db/client.dart'; +import 'package:backend_dart/infrastructure/persistence/prisma_user_data_source.dart'; + +class PrismaDatabase implements IDatabase { + final prisma = PrismaClient(); + late final PrismaUserDataSource _users; + PrismaDatabase() { + _users = PrismaUserDataSource(prisma); + print('Database initialized'); + } + + @override + get users => _users; + @override + Future close() { + return Future.value(); + } +} diff --git a/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart b/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart new file mode 100755 index 0000000..82fe335 --- /dev/null +++ b/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart @@ -0,0 +1,162 @@ +import 'package:backend_dart/common/error_on_null.dart'; +import 'package:backend_dart/domain/data/user_data_source.dart'; +import 'package:backend_dart/domain/entities/user.dart'; +import 'package:backend_dart/domain/errors/app_error.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:backend_dart/infrastructure/persistence/db/model.dart'; +import 'package:backend_dart/infrastructure/persistence/db/prisma.dart'; +import 'package:backend_dart/infrastructure/persistence/mapper/user_dbo_mapper.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:orm/orm.dart'; +import 'package:uuid/uuid.dart'; + +import 'db/client.dart'; + +class PrismaUserDataSource implements UserDataSource { + final PrismaClient prisma; + final UserDboMapper mapper = UserDboMapper(); + PrismaUserDataSource(this.prisma); + + @override + TaskEither create(User user) { + return TaskEither.tryCatch( + () async { + if (user.password == null) { + throw Exception('Password is required'); + } + if (user.email == null) { + throw Exception('Email is required'); + } + final createdUser = await prisma.userDbo.create( + data: PrismaUnion.$1(UserDboCreateInput( + id: user.id, + name: user.name!, + email: user.email!, + password: user.password!, + )), + ); + return createdUser; + }, + (error, _) => AppError.databaseError( + message: 'Failed to create user: ${error.toString()}', + ), + ).flatMap(mapper.from); + } + + @override + TaskEither findByEmail(String email) { + return TaskEither.tryCatch( + () async { + final user = await prisma.userDbo + .findUnique(where: UserDboWhereUniqueInput(email: email)); + if (user == null) return null; + return user; + }, + (error, _) => AppError.databaseError( + message: 'Failed to find user by email: ${error.toString()}', + ), + ) + .flatMap(errorOnNull(AppError.notFound( + 'User with email $email found', + ))) + .flatMap(mapper.from); + } + + @override + TaskEither findById(String id) { + return TaskEither.tryCatch( + () async { + final user = await prisma.userDbo + .findUnique(where: UserDboWhereUniqueInput(id: id)); + return user; + }, + (error, _) => AppError.databaseError( + message: 'Failed to find user by ID: ${error.toString()}', + ), + ) + .flatMap(errorOnNull(AppError.notFound( + "User with id $id not found", + ))) + .flatMap(mapper.from); + } + + @override + TaskEither update(User user) { + return mapper + .to(user) + .flatMap((userDbo) => TaskEither.tryCatch(() async { + // Führe das Update durch + if (userDbo.id == null) { + throw Exception('User ID is required'); + } + + final updatedUser = await prisma.userDbo.update( + data: PrismaUnion.$2( + UserDboUncheckedUpdateInput( + id: PrismaUnion.$1(userDbo.id!), + name: userDbo.name != null + ? PrismaUnion.$1(userDbo.name!) + : null, + email: userDbo.email != null + ? PrismaUnion.$1(userDbo.email!) + : null, + password: userDbo.password != null + ? PrismaUnion.$1(userDbo.password!) + : null, + ), + ), + where: UserDboWhereUniqueInput(id: userDbo.id), + ); + + return updatedUser; + }, + (error, _) => AppError.databaseError( + message: 'Failed to update user: ${error.toString()}', + ))) + .flatMap(errorOnNull(AppError.notFound( + 'User not found', + ))) + .flatMap(mapper.from); + } + + @override + TaskEither delete(String id) { + return TaskEither.tryCatch( + () async { + await prisma.userDbo.delete(where: UserDboWhereUniqueInput(id: id)); + }, + (error, _) => AppError.databaseError( + message: 'Failed to delete user: ${error.toString()}', + ), + ); + } + + @override + TaskEither> findAll() { + return TaskEither>.tryCatch( + () async => await prisma.userDbo.findMany(), + (error, _) => AppError.databaseError( + message: 'Failed to fetch all users: ${error.toString()}', + ), + ).flatMap(mapper.listFrom); + } + + @override + TaskEither generateId() { + return TaskEither.tryCatch( + () async { + var uuid = Uuid(); + do { + final id = uuid.v4(); + final user = await prisma.userDbo.findUnique( + where: UserDboWhereUniqueInput(id: id), + ); + if (user == null) return id; + } while (true); + }, + (error, _) => AppError.databaseError( + message: 'Failed to generate ID: ${error.toString()}', + ), + ); + } +} diff --git a/backend-dart/lib/infrastructure/persistence/prisma_user_repository.dart b/backend-dart/lib/infrastructure/persistence/prisma_user_repository.dart deleted file mode 100755 index 83b09b8..0000000 --- a/backend-dart/lib/infrastructure/persistence/prisma_user_repository.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:backend_dart/domain/entities/user.dart'; -import 'package:backend_dart/domain/repositories/user_repository.dart'; -import 'package:backend_dart/infrastructure/persistence/db/prisma.dart'; -import 'package:orm/orm.dart'; - -import 'db/client.dart'; - -class PrismaUserRepository implements UserRepository { - final PrismaClient prisma; - PrismaUserRepository(this.prisma); - - @override - Future create(User user) async { - if (user.password == null) { - throw Exception('Password is required'); - } - prisma.userDbo.create( - data: PrismaUnion.$1(UserDboCreateInput( - id: user.id, - name: user.name, - email: user.email, - password: user.password!, - ))); - } - - @override - Future findByEmail(String email) async { - final user = await prisma.userDbo - .findUnique(where: UserDboWhereUniqueInput(email: email)); - if (user == null) { - return null; - } - - return User( - id: user.id!, - name: user.name!, - email: user.email!, - password: user.password, - ); - } - - @override - Future findById(String id) async { - final user = - await prisma.userDbo.findUnique(where: UserDboWhereUniqueInput(id: id)); - if (user == null) { - return null; - } - - return User( - id: user.id!, - name: user.name!, - email: user.email!, - password: user.password, - ); - } -} diff --git a/backend-dart/lib/interfaces/http/router.dart b/backend-dart/lib/interfaces/http/router.dart index e671414..20f4f53 100644 --- a/backend-dart/lib/interfaces/http/router.dart +++ b/backend-dart/lib/interfaces/http/router.dart @@ -1,4 +1,4 @@ -import 'package:backend_dart/application/user_service/user_service_provider.dart'; +import 'package:backend_dart/application/service/user_service_provider.dart'; import 'package:riverpod/riverpod.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -15,10 +15,10 @@ Router getRouter(ProviderContainer container) { return Response.ok('Server is running'); }); - // UserService instanzieren + // Services final userService = container.read(userServiceProvider); - // UserService-Router an Haupt-Router binden + // UserService-Router router.mount('/users/', userService.router.call); return router; diff --git a/backend-dart/pubspec.lock b/backend-dart/pubspec.lock index 4351735..8f67ebb 100755 --- a/backend-dart/pubspec.lock +++ b/backend-dart/pubspec.lock @@ -799,7 +799,7 @@ packages: source: hosted version: "0.3.0" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff diff --git a/backend-dart/pubspec.yaml b/backend-dart/pubspec.yaml index efb1726..53cc8f8 100755 --- a/backend-dart/pubspec.yaml +++ b/backend-dart/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: riverpod_annotation: ^2.6.1 json_annotation: ^4.9.0 freezed_annotation: ^2.4.4 + uuid: ^4.5.1 dev_dependencies: lints: ^3.0.0