diff --git a/backend-dart/lib/application/repository/user_repository_impl.dart b/backend-dart/lib/application/repository/user_repository_impl.dart index 7635f98..4e8a76f 100755 --- a/backend-dart/lib/application/repository/user_repository_impl.dart +++ b/backend-dart/lib/application/repository/user_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:backend_dart/domain/entities/user.dart'; -import 'package:backend_dart/domain/interface/database.dart'; +import 'package:backend_dart/domain/data/database.dart'; import 'package:backend_dart/domain/interface/error.dart'; import 'package:backend_dart/domain/repository/user_repository.dart'; import 'package:fpdart/fpdart.dart'; @@ -9,7 +9,7 @@ class UserRepositoryImpl implements UserRepository { UserRepositoryImpl(this.database); @override - TaskEither create(User user) { + TaskEither create(UserCreate user) { return database.users .generateId() .map( @@ -29,7 +29,7 @@ class UserRepositoryImpl implements UserRepository { } @override - TaskEither update(User user) { + TaskEither update(UserUpdate user) { return database.users.update(user); } diff --git a/backend-dart/lib/application/service/dto/user_dto.dart b/backend-dart/lib/application/service/dto/user_dto.dart index 27ca162..e1dfb7a 100644 --- a/backend-dart/lib/application/service/dto/user_dto.dart +++ b/backend-dart/lib/application/service/dto/user_dto.dart @@ -6,15 +6,39 @@ part 'user_dto.g.dart'; @freezed class UserDto with _$UserDto { const factory UserDto({ - String? id, - String? name, - String? email, - String? password, + required String id, + required String name, + required String email, DateTime? createdAt, DateTime? updatedAt, }) = _UserDto; /// JSON-Serialisierung - factory UserDto.fromJson(Map json) => - _$UserDtoFromJson(json); + factory UserDto.fromJson(Map json) => _$UserDtoFromJson(json); +} + +@freezed +class UserCreateDto with _$UserCreateDto { + const factory UserCreateDto({ + required String name, + required String email, + required String password, + }) = _UserCreateDto; + + /// JSON-Serialisierung + factory UserCreateDto.fromJson(Map json) => + _$UserCreateDtoFromJson(json); +} + +@freezed +class UserUpdateDto with _$UserUpdateDto { + const factory UserUpdateDto({ + String? name, + String? email, + String? password, + }) = _UserUpdateDto; + + /// JSON-Serialisierung + factory UserUpdateDto.fromJson(Map json) => + _$UserUpdateDtoFromJson(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 index 867047b..523111b 100644 --- a/backend-dart/lib/application/service/dto/user_dto.freezed.dart +++ b/backend-dart/lib/application/service/dto/user_dto.freezed.dart @@ -20,10 +20,9 @@ UserDto _$UserDtoFromJson(Map json) { /// @nodoc mixin _$UserDto { - String? get id => throw _privateConstructorUsedError; - String? get name => throw _privateConstructorUsedError; - String? get email => throw _privateConstructorUsedError; - String? get password => throw _privateConstructorUsedError; + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; DateTime? get createdAt => throw _privateConstructorUsedError; DateTime? get updatedAt => throw _privateConstructorUsedError; @@ -42,10 +41,9 @@ abstract class $UserDtoCopyWith<$Res> { _$UserDtoCopyWithImpl<$Res, UserDto>; @useResult $Res call( - {String? id, - String? name, - String? email, - String? password, + {String id, + String name, + String email, DateTime? createdAt, DateTime? updatedAt}); } @@ -65,30 +63,25 @@ class _$UserDtoCopyWithImpl<$Res, $Val extends UserDto> @pragma('vm:prefer-inline') @override $Res call({ - Object? id = freezed, - Object? name = freezed, - Object? email = freezed, - Object? password = freezed, + Object? id = null, + Object? name = null, + Object? email = null, Object? createdAt = freezed, Object? updatedAt = freezed, }) { return _then(_value.copyWith( - id: freezed == id + id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as String?, - name: freezed == name + as String, + name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable - as String?, - email: freezed == email + as String, + email: null == 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?, + as String, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable @@ -109,10 +102,9 @@ abstract class _$$UserDtoImplCopyWith<$Res> implements $UserDtoCopyWith<$Res> { @override @useResult $Res call( - {String? id, - String? name, - String? email, - String? password, + {String id, + String name, + String email, DateTime? createdAt, DateTime? updatedAt}); } @@ -130,30 +122,25 @@ class __$$UserDtoImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? id = freezed, - Object? name = freezed, - Object? email = freezed, - Object? password = freezed, + Object? id = null, + Object? name = null, + Object? email = null, Object? createdAt = freezed, Object? updatedAt = freezed, }) { return _then(_$UserDtoImpl( - id: freezed == id + id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as String?, - name: freezed == name + as String, + name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable - as String?, - email: freezed == email + as String, + email: null == 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?, + as String, createdAt: freezed == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable @@ -170,10 +157,9 @@ class __$$UserDtoImplCopyWithImpl<$Res> @JsonSerializable() class _$UserDtoImpl implements _UserDto { const _$UserDtoImpl( - {this.id, - this.name, - this.email, - this.password, + {required this.id, + required this.name, + required this.email, this.createdAt, this.updatedAt}); @@ -181,13 +167,11 @@ class _$UserDtoImpl implements _UserDto { _$$UserDtoImplFromJson(json); @override - final String? id; + final String id; @override - final String? name; + final String name; @override - final String? email; - @override - final String? password; + final String email; @override final DateTime? createdAt; @override @@ -195,7 +179,7 @@ class _$UserDtoImpl implements _UserDto { @override String toString() { - return 'UserDto(id: $id, name: $name, email: $email, password: $password, createdAt: $createdAt, updatedAt: $updatedAt)'; + return 'UserDto(id: $id, name: $name, email: $email, createdAt: $createdAt, updatedAt: $updatedAt)'; } @override @@ -206,8 +190,6 @@ class _$UserDtoImpl implements _UserDto { (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) || @@ -217,7 +199,7 @@ class _$UserDtoImpl implements _UserDto { @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => - Object.hash(runtimeType, id, name, email, password, createdAt, updatedAt); + Object.hash(runtimeType, id, name, email, createdAt, updatedAt); /// Create a copy of UserDto /// with the given fields replaced by the non-null parameter values. @@ -237,23 +219,20 @@ class _$UserDtoImpl implements _UserDto { abstract class _UserDto implements UserDto { const factory _UserDto( - {final String? id, - final String? name, - final String? email, - final String? password, + {required final String id, + required final String name, + required final String email, final DateTime? createdAt, final DateTime? updatedAt}) = _$UserDtoImpl; factory _UserDto.fromJson(Map json) = _$UserDtoImpl.fromJson; @override - String? get id; + String get id; @override - String? get name; + String get name; @override - String? get email; - @override - String? get password; + String get email; @override DateTime? get createdAt; @override @@ -266,3 +245,374 @@ abstract class _UserDto implements UserDto { _$$UserDtoImplCopyWith<_$UserDtoImpl> get copyWith => throw _privateConstructorUsedError; } + +UserCreateDto _$UserCreateDtoFromJson(Map json) { + return _UserCreateDto.fromJson(json); +} + +/// @nodoc +mixin _$UserCreateDto { + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + + /// Serializes this UserCreateDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserCreateDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCreateDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCreateDtoCopyWith<$Res> { + factory $UserCreateDtoCopyWith( + UserCreateDto value, $Res Function(UserCreateDto) then) = + _$UserCreateDtoCopyWithImpl<$Res, UserCreateDto>; + @useResult + $Res call({String name, String email, String password}); +} + +/// @nodoc +class _$UserCreateDtoCopyWithImpl<$Res, $Val extends UserCreateDto> + implements $UserCreateDtoCopyWith<$Res> { + _$UserCreateDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserCreateDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? email = null, + Object? password = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserCreateDtoImplCopyWith<$Res> + implements $UserCreateDtoCopyWith<$Res> { + factory _$$UserCreateDtoImplCopyWith( + _$UserCreateDtoImpl value, $Res Function(_$UserCreateDtoImpl) then) = + __$$UserCreateDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String email, String password}); +} + +/// @nodoc +class __$$UserCreateDtoImplCopyWithImpl<$Res> + extends _$UserCreateDtoCopyWithImpl<$Res, _$UserCreateDtoImpl> + implements _$$UserCreateDtoImplCopyWith<$Res> { + __$$UserCreateDtoImplCopyWithImpl( + _$UserCreateDtoImpl _value, $Res Function(_$UserCreateDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of UserCreateDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? email = null, + Object? password = null, + }) { + return _then(_$UserCreateDtoImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserCreateDtoImpl implements _UserCreateDto { + const _$UserCreateDtoImpl( + {required this.name, required this.email, required this.password}); + + factory _$UserCreateDtoImpl.fromJson(Map json) => + _$$UserCreateDtoImplFromJson(json); + + @override + final String name; + @override + final String email; + @override + final String password; + + @override + String toString() { + return 'UserCreateDto(name: $name, email: $email, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserCreateDtoImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, email, password); + + /// Create a copy of UserCreateDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserCreateDtoImplCopyWith<_$UserCreateDtoImpl> get copyWith => + __$$UserCreateDtoImplCopyWithImpl<_$UserCreateDtoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserCreateDtoImplToJson( + this, + ); + } +} + +abstract class _UserCreateDto implements UserCreateDto { + const factory _UserCreateDto( + {required final String name, + required final String email, + required final String password}) = _$UserCreateDtoImpl; + + factory _UserCreateDto.fromJson(Map json) = + _$UserCreateDtoImpl.fromJson; + + @override + String get name; + @override + String get email; + @override + String get password; + + /// Create a copy of UserCreateDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserCreateDtoImplCopyWith<_$UserCreateDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} + +UserUpdateDto _$UserUpdateDtoFromJson(Map json) { + return _UserUpdateDto.fromJson(json); +} + +/// @nodoc +mixin _$UserUpdateDto { + String? get name => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; + String? get password => throw _privateConstructorUsedError; + + /// Serializes this UserUpdateDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserUpdateDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserUpdateDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserUpdateDtoCopyWith<$Res> { + factory $UserUpdateDtoCopyWith( + UserUpdateDto value, $Res Function(UserUpdateDto) then) = + _$UserUpdateDtoCopyWithImpl<$Res, UserUpdateDto>; + @useResult + $Res call({String? name, String? email, String? password}); +} + +/// @nodoc +class _$UserUpdateDtoCopyWithImpl<$Res, $Val extends UserUpdateDto> + implements $UserUpdateDtoCopyWith<$Res> { + _$UserUpdateDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserUpdateDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + }) { + return _then(_value.copyWith( + 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?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserUpdateDtoImplCopyWith<$Res> + implements $UserUpdateDtoCopyWith<$Res> { + factory _$$UserUpdateDtoImplCopyWith( + _$UserUpdateDtoImpl value, $Res Function(_$UserUpdateDtoImpl) then) = + __$$UserUpdateDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String? name, String? email, String? password}); +} + +/// @nodoc +class __$$UserUpdateDtoImplCopyWithImpl<$Res> + extends _$UserUpdateDtoCopyWithImpl<$Res, _$UserUpdateDtoImpl> + implements _$$UserUpdateDtoImplCopyWith<$Res> { + __$$UserUpdateDtoImplCopyWithImpl( + _$UserUpdateDtoImpl _value, $Res Function(_$UserUpdateDtoImpl) _then) + : super(_value, _then); + + /// Create a copy of UserUpdateDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + }) { + return _then(_$UserUpdateDtoImpl( + 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?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserUpdateDtoImpl implements _UserUpdateDto { + const _$UserUpdateDtoImpl({this.name, this.email, this.password}); + + factory _$UserUpdateDtoImpl.fromJson(Map json) => + _$$UserUpdateDtoImplFromJson(json); + + @override + final String? name; + @override + final String? email; + @override + final String? password; + + @override + String toString() { + return 'UserUpdateDto(name: $name, email: $email, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserUpdateDtoImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, email, password); + + /// Create a copy of UserUpdateDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserUpdateDtoImplCopyWith<_$UserUpdateDtoImpl> get copyWith => + __$$UserUpdateDtoImplCopyWithImpl<_$UserUpdateDtoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserUpdateDtoImplToJson( + this, + ); + } +} + +abstract class _UserUpdateDto implements UserUpdateDto { + const factory _UserUpdateDto( + {final String? name, + final String? email, + final String? password}) = _$UserUpdateDtoImpl; + + factory _UserUpdateDto.fromJson(Map json) = + _$UserUpdateDtoImpl.fromJson; + + @override + String? get name; + @override + String? get email; + @override + String? get password; + + /// Create a copy of UserUpdateDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserUpdateDtoImplCopyWith<_$UserUpdateDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/backend-dart/lib/application/service/dto/user_dto.g.dart b/backend-dart/lib/application/service/dto/user_dto.g.dart index a71206c..01b59bf 100644 --- a/backend-dart/lib/application/service/dto/user_dto.g.dart +++ b/backend-dart/lib/application/service/dto/user_dto.g.dart @@ -8,10 +8,9 @@ part of 'user_dto.dart'; _$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?, + id: json['id'] as String, + name: json['name'] as String, + email: json['email'] as String, createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String), @@ -25,7 +24,34 @@ Map _$$UserDtoImplToJson(_$UserDtoImpl instance) => 'id': instance.id, 'name': instance.name, 'email': instance.email, - 'password': instance.password, 'createdAt': instance.createdAt?.toIso8601String(), 'updatedAt': instance.updatedAt?.toIso8601String(), }; + +_$UserCreateDtoImpl _$$UserCreateDtoImplFromJson(Map json) => + _$UserCreateDtoImpl( + name: json['name'] as String, + email: json['email'] as String, + password: json['password'] as String, + ); + +Map _$$UserCreateDtoImplToJson(_$UserCreateDtoImpl instance) => + { + 'name': instance.name, + 'email': instance.email, + 'password': instance.password, + }; + +_$UserUpdateDtoImpl _$$UserUpdateDtoImplFromJson(Map json) => + _$UserUpdateDtoImpl( + name: json['name'] as String?, + email: json['email'] as String?, + password: json['password'] as String?, + ); + +Map _$$UserUpdateDtoImplToJson(_$UserUpdateDtoImpl instance) => + { + 'name': instance.name, + 'email': instance.email, + 'password': instance.password, + }; diff --git a/backend-dart/lib/application/service/mapper/user_dto_mapper.dart b/backend-dart/lib/application/service/mapper/user_dto_mapper.dart index 30a8d00..72e5315 100644 --- a/backend-dart/lib/application/service/mapper/user_dto_mapper.dart +++ b/backend-dart/lib/application/service/mapper/user_dto_mapper.dart @@ -18,7 +18,6 @@ class UserDtoMapper implements IMapper { id: dto.id, name: dto.name, email: dto.email, - password: dto.password, createdAt: dto.createdAt, updatedAt: dto.updatedAt, )); @@ -32,4 +31,20 @@ class UserDtoMapper implements IMapper { TaskEither> listTo(Iterable origins) { return TaskEither.traverseList(origins.toList(), to); } + + TaskEither fromCreateTo(UserCreateDto origin) => + TaskEither.of(UserCreate( + name: origin.name, + email: origin.email, + password: origin.password, + )); + + TaskEither fromUpdateTo( + UserUpdateDto origin, String id) => + TaskEither.of(UserUpdate( + id: id, + name: origin.name, + email: origin.email, + password: origin.password, + )); } diff --git a/backend-dart/lib/application/service/user_service.dart b/backend-dart/lib/application/service/user_service.dart index b825d97..c0cd0a8 100644 --- a/backend-dart/lib/application/service/user_service.dart +++ b/backend-dart/lib/application/service/user_service.dart @@ -2,11 +2,10 @@ 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/request_helper.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/common/validation.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'; @@ -40,15 +39,11 @@ class UserService { @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) + return requestToJson(request) + .flatMap( + (json) => validateJsonKeys(json, ['name', 'email', 'password'])) + .flatMap((json) => decodeJson(json, UserCreateDto.fromJson)) + .flatMap(mapper.fromCreateTo) .flatMap(users.create) .flatMap(mapper.to) .match((left) => ResponseHelpers.fromError(left), @@ -58,15 +53,10 @@ class UserService { @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) + return requestToJson(request) + .flatMap((json) => validateJsonKeys(json, [])) + .flatMap((json) => decodeJson(json, UserUpdateDto.fromJson)) + .flatMap((dto) => mapper.fromUpdateTo(dto, userId)) .flatMap(users.update) .flatMap(mapper.to) .match((left) => ResponseHelpers.fromError(left), diff --git a/backend-dart/lib/common/extensions.dart b/backend-dart/lib/common/extensions.dart new file mode 100644 index 0000000..51fdd3a --- /dev/null +++ b/backend-dart/lib/common/extensions.dart @@ -0,0 +1,8 @@ +extension Let on T? { + R? let(R Function(T it) action) { + if (this != null) { + return action(this!); + } + return null; + } +} diff --git a/backend-dart/lib/common/request_helper.dart b/backend-dart/lib/common/request_helper.dart new file mode 100644 index 0000000..5689a5f --- /dev/null +++ b/backend-dart/lib/common/request_helper.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; +import 'dart:core'; + +import 'package:backend_dart/domain/errors/app_error.dart'; +import 'package:backend_dart/domain/interface/error.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:shelf/shelf.dart'; + +TaskEither> requestToJson(Request request) { + return TaskEither.tryCatch( + () async { + final body = await request.readAsString(); + return jsonDecode(body); + }, + (error, stack) => AppError.inputError( + message: 'Failed to decode JSON: ${error.toString()}', + ), + ); +} + +TaskEither decodeJson( + Map json, T Function(Map) fromJson) { + return TaskEither.tryCatch( + () async { + return fromJson(json); + }, + (error, stack) => AppError.inputError( + message: 'Failed to decode JSON: ${error.toString()}', + ), + ); +} \ No newline at end of file diff --git a/backend-dart/lib/common/secure_hash.dart b/backend-dart/lib/common/secure_hash.dart new file mode 100644 index 0000000..016d0e0 --- /dev/null +++ b/backend-dart/lib/common/secure_hash.dart @@ -0,0 +1,10 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +String generateSecureHash(String input) { + final bytes = utf8.encode(input); + final hash = sha256.convert(bytes); + print(hash); + return hash.toString(); +} diff --git a/backend-dart/lib/common/validation.dart b/backend-dart/lib/common/validation.dart new file mode 100644 index 0000000..1124e70 --- /dev/null +++ b/backend-dart/lib/common/validation.dart @@ -0,0 +1,22 @@ +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(); + + if (missingKeys.isNotEmpty) { + throw Exception('Missing required keys: ${missingKeys.join(', ')}'); + } + + return json; + }, + (error, _) => AppError.validationError( + message: 'Failed to validate JSON keys: ${error.toString()}', + ), + ); +} diff --git a/backend-dart/lib/domain/interface/database.dart b/backend-dart/lib/domain/data/database.dart similarity index 56% rename from backend-dart/lib/domain/interface/database.dart rename to backend-dart/lib/domain/data/database.dart index 9e4781c..1d81e04 100644 --- a/backend-dart/lib/domain/interface/database.dart +++ b/backend-dart/lib/domain/data/database.dart @@ -1,7 +1,6 @@ import 'package:backend_dart/domain/data/user_data_source.dart'; -import 'package:backend_dart/domain/entities/user.dart'; abstract class IDatabase { - UserDataSource get users; + UserDataSource get users; Future close(); } diff --git a/backend-dart/lib/domain/data/project_data_source.dart b/backend-dart/lib/domain/data/project_data_source.dart new file mode 100644 index 0000000..c20da8e --- /dev/null +++ b/backend-dart/lib/domain/data/project_data_source.dart @@ -0,0 +1,34 @@ +import 'package:backend_dart/domain/entities/project.dart'; + +/// Interface for managing project data interactions. +abstract class ProjectDataSource { + /// Creates a new project in the data source. + /// + /// Throws an error if the creation fails. + Future createProject(NewProject project); + + /// Retrieves a project by its unique ID. + /// + /// Returns `null` if no project is found. + Future findProjectById(String id); + + /// Retrieves all projects associated with a specific user. + /// + /// Returns an empty list if no projects are found. + Future> findProjectsByUserId(String userId); + + /// Updates an existing project in the data source. + /// + /// Throws an error if the update fails. + Future updateProject(ProjectUpdate project); + + /// Deletes a project by its unique ID. + /// + /// Throws an error if the deletion fails. + Future deleteProject(String id); + + /// Retrieves all projects in the data source. + /// + /// Returns an empty list if no projects exist. + Future> findAllProjects(); +} diff --git a/backend-dart/lib/domain/data/user_data_source.dart b/backend-dart/lib/domain/data/user_data_source.dart index f14aaa7..cb05b07 100644 --- a/backend-dart/lib/domain/data/user_data_source.dart +++ b/backend-dart/lib/domain/data/user_data_source.dart @@ -1,18 +1,19 @@ +import 'package:backend_dart/domain/entities/user.dart'; import 'package:backend_dart/domain/interface/error.dart'; import 'package:fpdart/fpdart.dart'; -abstract class UserDataSource { - TaskEither create(T user); +abstract class UserDataSource { + TaskEither create(UserCreate user); - TaskEither findByEmail(String email); + TaskEither findByEmail(String email); - TaskEither findById(String id); + TaskEither findById(String id); - TaskEither update(T user); + TaskEither update(UserUpdate user); TaskEither delete(String id); - TaskEither> findAll(); + TaskEither> findAll(); - TaskEither generateId(); + TaskEither generateId(); } diff --git a/backend-dart/lib/domain/entities/project.dart b/backend-dart/lib/domain/entities/project.dart new file mode 100644 index 0000000..167f33a --- /dev/null +++ b/backend-dart/lib/domain/entities/project.dart @@ -0,0 +1,36 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'project.freezed.dart'; + +@freezed +class NewProject with _$NewProject { + const factory NewProject({ + required String name, + String? description, + String? clientId, + required String userId, + }) = _NewProject; +} + +@freezed +class Project with _$Project { + const factory Project({ + required String id, + required String name, + String? description, + String? clientId, + required String userId, + required DateTime createdAt, + required DateTime updatedAt, + }) = _Project; +} + +@freezed +class ProjectUpdate with _$ProjectUpdate { + const factory ProjectUpdate({ + String? name, + String? description, + String? clientId, + String? userId, + }) = _ProjectUpdate; +} diff --git a/backend-dart/lib/domain/entities/project.freezed.dart b/backend-dart/lib/domain/entities/project.freezed.dart new file mode 100644 index 0000000..9a412de --- /dev/null +++ b/backend-dart/lib/domain/entities/project.freezed.dart @@ -0,0 +1,644 @@ +// 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 'project.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 _$NewProject { + String get name => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + String? get clientId => throw _privateConstructorUsedError; + String get userId => throw _privateConstructorUsedError; + + /// Create a copy of NewProject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $NewProjectCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NewProjectCopyWith<$Res> { + factory $NewProjectCopyWith( + NewProject value, $Res Function(NewProject) then) = + _$NewProjectCopyWithImpl<$Res, NewProject>; + @useResult + $Res call( + {String name, String? description, String? clientId, String userId}); +} + +/// @nodoc +class _$NewProjectCopyWithImpl<$Res, $Val extends NewProject> + implements $NewProjectCopyWith<$Res> { + _$NewProjectCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of NewProject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? description = freezed, + Object? clientId = freezed, + Object? userId = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NewProjectImplCopyWith<$Res> + implements $NewProjectCopyWith<$Res> { + factory _$$NewProjectImplCopyWith( + _$NewProjectImpl value, $Res Function(_$NewProjectImpl) then) = + __$$NewProjectImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String name, String? description, String? clientId, String userId}); +} + +/// @nodoc +class __$$NewProjectImplCopyWithImpl<$Res> + extends _$NewProjectCopyWithImpl<$Res, _$NewProjectImpl> + implements _$$NewProjectImplCopyWith<$Res> { + __$$NewProjectImplCopyWithImpl( + _$NewProjectImpl _value, $Res Function(_$NewProjectImpl) _then) + : super(_value, _then); + + /// Create a copy of NewProject + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? description = freezed, + Object? clientId = freezed, + Object? userId = null, + }) { + return _then(_$NewProjectImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$NewProjectImpl implements _NewProject { + const _$NewProjectImpl( + {required this.name, + this.description, + this.clientId, + required this.userId}); + + @override + final String name; + @override + final String? description; + @override + final String? clientId; + @override + final String userId; + + @override + String toString() { + return 'NewProject(name: $name, description: $description, clientId: $clientId, userId: $userId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NewProjectImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.userId, userId) || other.userId == userId)); + } + + @override + int get hashCode => + Object.hash(runtimeType, name, description, clientId, userId); + + /// Create a copy of NewProject + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NewProjectImplCopyWith<_$NewProjectImpl> get copyWith => + __$$NewProjectImplCopyWithImpl<_$NewProjectImpl>(this, _$identity); +} + +abstract class _NewProject implements NewProject { + const factory _NewProject( + {required final String name, + final String? description, + final String? clientId, + required final String userId}) = _$NewProjectImpl; + + @override + String get name; + @override + String? get description; + @override + String? get clientId; + @override + String get userId; + + /// Create a copy of NewProject + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NewProjectImplCopyWith<_$NewProjectImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$Project { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + String? get clientId => throw _privateConstructorUsedError; + String get userId => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; + + /// Create a copy of Project + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProjectCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProjectCopyWith<$Res> { + factory $ProjectCopyWith(Project value, $Res Function(Project) then) = + _$ProjectCopyWithImpl<$Res, Project>; + @useResult + $Res call( + {String id, + String name, + String? description, + String? clientId, + String userId, + DateTime createdAt, + DateTime updatedAt}); +} + +/// @nodoc +class _$ProjectCopyWithImpl<$Res, $Val extends Project> + implements $ProjectCopyWith<$Res> { + _$ProjectCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Project + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? description = freezed, + Object? clientId = freezed, + Object? userId = null, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProjectImplCopyWith<$Res> implements $ProjectCopyWith<$Res> { + factory _$$ProjectImplCopyWith( + _$ProjectImpl value, $Res Function(_$ProjectImpl) then) = + __$$ProjectImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String name, + String? description, + String? clientId, + String userId, + DateTime createdAt, + DateTime updatedAt}); +} + +/// @nodoc +class __$$ProjectImplCopyWithImpl<$Res> + extends _$ProjectCopyWithImpl<$Res, _$ProjectImpl> + implements _$$ProjectImplCopyWith<$Res> { + __$$ProjectImplCopyWithImpl( + _$ProjectImpl _value, $Res Function(_$ProjectImpl) _then) + : super(_value, _then); + + /// Create a copy of Project + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? description = freezed, + Object? clientId = freezed, + Object? userId = null, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then(_$ProjectImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc + +class _$ProjectImpl implements _Project { + const _$ProjectImpl( + {required this.id, + required this.name, + this.description, + this.clientId, + required this.userId, + required this.createdAt, + required this.updatedAt}); + + @override + final String id; + @override + final String name; + @override + final String? description; + @override + final String? clientId; + @override + final String userId; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + + @override + String toString() { + return 'Project(id: $id, name: $name, description: $description, clientId: $clientId, userId: $userId, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProjectImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @override + int get hashCode => Object.hash(runtimeType, id, name, description, clientId, + userId, createdAt, updatedAt); + + /// Create a copy of Project + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProjectImplCopyWith<_$ProjectImpl> get copyWith => + __$$ProjectImplCopyWithImpl<_$ProjectImpl>(this, _$identity); +} + +abstract class _Project implements Project { + const factory _Project( + {required final String id, + required final String name, + final String? description, + final String? clientId, + required final String userId, + required final DateTime createdAt, + required final DateTime updatedAt}) = _$ProjectImpl; + + @override + String get id; + @override + String get name; + @override + String? get description; + @override + String? get clientId; + @override + String get userId; + @override + DateTime get createdAt; + @override + DateTime get updatedAt; + + /// Create a copy of Project + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProjectImplCopyWith<_$ProjectImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$ProjectUpdate { + String? get name => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + String? get clientId => throw _privateConstructorUsedError; + String? get userId => throw _privateConstructorUsedError; + + /// Create a copy of ProjectUpdate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProjectUpdateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProjectUpdateCopyWith<$Res> { + factory $ProjectUpdateCopyWith( + ProjectUpdate value, $Res Function(ProjectUpdate) then) = + _$ProjectUpdateCopyWithImpl<$Res, ProjectUpdate>; + @useResult + $Res call( + {String? name, String? description, String? clientId, String? userId}); +} + +/// @nodoc +class _$ProjectUpdateCopyWithImpl<$Res, $Val extends ProjectUpdate> + implements $ProjectUpdateCopyWith<$Res> { + _$ProjectUpdateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProjectUpdate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? description = freezed, + Object? clientId = freezed, + Object? userId = freezed, + }) { + return _then(_value.copyWith( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + userId: freezed == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProjectUpdateImplCopyWith<$Res> + implements $ProjectUpdateCopyWith<$Res> { + factory _$$ProjectUpdateImplCopyWith( + _$ProjectUpdateImpl value, $Res Function(_$ProjectUpdateImpl) then) = + __$$ProjectUpdateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? name, String? description, String? clientId, String? userId}); +} + +/// @nodoc +class __$$ProjectUpdateImplCopyWithImpl<$Res> + extends _$ProjectUpdateCopyWithImpl<$Res, _$ProjectUpdateImpl> + implements _$$ProjectUpdateImplCopyWith<$Res> { + __$$ProjectUpdateImplCopyWithImpl( + _$ProjectUpdateImpl _value, $Res Function(_$ProjectUpdateImpl) _then) + : super(_value, _then); + + /// Create a copy of ProjectUpdate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? description = freezed, + Object? clientId = freezed, + Object? userId = freezed, + }) { + return _then(_$ProjectUpdateImpl( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + clientId: freezed == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String?, + userId: freezed == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$ProjectUpdateImpl implements _ProjectUpdate { + const _$ProjectUpdateImpl( + {this.name, this.description, this.clientId, this.userId}); + + @override + final String? name; + @override + final String? description; + @override + final String? clientId; + @override + final String? userId; + + @override + String toString() { + return 'ProjectUpdate(name: $name, description: $description, clientId: $clientId, userId: $userId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProjectUpdateImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.userId, userId) || other.userId == userId)); + } + + @override + int get hashCode => + Object.hash(runtimeType, name, description, clientId, userId); + + /// Create a copy of ProjectUpdate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProjectUpdateImplCopyWith<_$ProjectUpdateImpl> get copyWith => + __$$ProjectUpdateImplCopyWithImpl<_$ProjectUpdateImpl>(this, _$identity); +} + +abstract class _ProjectUpdate implements ProjectUpdate { + const factory _ProjectUpdate( + {final String? name, + final String? description, + final String? clientId, + final String? userId}) = _$ProjectUpdateImpl; + + @override + String? get name; + @override + String? get description; + @override + String? get clientId; + @override + String? get userId; + + /// Create a copy of ProjectUpdate + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProjectUpdateImplCopyWith<_$ProjectUpdateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/backend-dart/lib/domain/entities/user.dart b/backend-dart/lib/domain/entities/user.dart index 8e268c1..2dd0537 100755 --- a/backend-dart/lib/domain/entities/user.dart +++ b/backend-dart/lib/domain/entities/user.dart @@ -5,11 +5,31 @@ part 'user.freezed.dart'; @freezed class User with _$User { const factory User({ - String? id, - String? name, - String? email, - String? password, + required String id, + required String name, + required String email, + String? passwordHash, DateTime? createdAt, DateTime? updatedAt, }) = _User; } + +@freezed +class UserCreate with _$UserCreate { + const factory UserCreate({ + String? id, + required String name, + required String email, + required String password, + }) = _UserCreate; +} + +@freezed +class UserUpdate with _$UserUpdate { + const factory UserUpdate({ + required String id, + String? name, + String? email, + String? password, + }) = _UserUpdate; +} diff --git a/backend-dart/lib/domain/entities/user.freezed.dart b/backend-dart/lib/domain/entities/user.freezed.dart index e262253..80e141f 100644 --- a/backend-dart/lib/domain/entities/user.freezed.dart +++ b/backend-dart/lib/domain/entities/user.freezed.dart @@ -16,10 +16,10 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$User { - String? get id => throw _privateConstructorUsedError; - String? get name => throw _privateConstructorUsedError; - String? get email => throw _privateConstructorUsedError; - String? get password => throw _privateConstructorUsedError; + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String? get passwordHash => throw _privateConstructorUsedError; DateTime? get createdAt => throw _privateConstructorUsedError; DateTime? get updatedAt => throw _privateConstructorUsedError; @@ -35,10 +35,10 @@ abstract class $UserCopyWith<$Res> { _$UserCopyWithImpl<$Res, User>; @useResult $Res call( - {String? id, - String? name, - String? email, - String? password, + {String id, + String name, + String email, + String? passwordHash, DateTime? createdAt, DateTime? updatedAt}); } @@ -58,29 +58,29 @@ class _$UserCopyWithImpl<$Res, $Val extends User> @pragma('vm:prefer-inline') @override $Res call({ - Object? id = freezed, - Object? name = freezed, - Object? email = freezed, - Object? password = freezed, + Object? id = null, + Object? name = null, + Object? email = null, + Object? passwordHash = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, }) { return _then(_value.copyWith( - id: freezed == id + id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as String?, - name: freezed == name + as String, + name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable - as String?, - email: freezed == email + as String, + email: null == 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, + passwordHash: freezed == passwordHash + ? _value.passwordHash + : passwordHash // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt @@ -102,10 +102,10 @@ abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { @override @useResult $Res call( - {String? id, - String? name, - String? email, - String? password, + {String id, + String name, + String email, + String? passwordHash, DateTime? createdAt, DateTime? updatedAt}); } @@ -122,29 +122,29 @@ class __$$UserImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? id = freezed, - Object? name = freezed, - Object? email = freezed, - Object? password = freezed, + Object? id = null, + Object? name = null, + Object? email = null, + Object? passwordHash = freezed, Object? createdAt = freezed, Object? updatedAt = freezed, }) { return _then(_$UserImpl( - id: freezed == id + id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as String?, - name: freezed == name + as String, + name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable - as String?, - email: freezed == email + as String, + email: null == 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, + passwordHash: freezed == passwordHash + ? _value.passwordHash + : passwordHash // ignore: cast_nullable_to_non_nullable as String?, createdAt: freezed == createdAt ? _value.createdAt @@ -162,21 +162,21 @@ class __$$UserImplCopyWithImpl<$Res> class _$UserImpl implements _User { const _$UserImpl( - {this.id, - this.name, - this.email, - this.password, + {required this.id, + required this.name, + required this.email, + this.passwordHash, this.createdAt, this.updatedAt}); @override - final String? id; + final String id; @override - final String? name; + final String name; @override - final String? email; + final String email; @override - final String? password; + final String? passwordHash; @override final DateTime? createdAt; @override @@ -184,7 +184,7 @@ class _$UserImpl implements _User { @override String toString() { - return 'User(id: $id, name: $name, email: $email, password: $password, createdAt: $createdAt, updatedAt: $updatedAt)'; + return 'User(id: $id, name: $name, email: $email, passwordHash: $passwordHash, createdAt: $createdAt, updatedAt: $updatedAt)'; } @override @@ -195,8 +195,8 @@ class _$UserImpl implements _User { (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.passwordHash, passwordHash) || + other.passwordHash == passwordHash) && (identical(other.createdAt, createdAt) || other.createdAt == createdAt) && (identical(other.updatedAt, updatedAt) || @@ -204,8 +204,8 @@ class _$UserImpl implements _User { } @override - int get hashCode => - Object.hash(runtimeType, id, name, email, password, createdAt, updatedAt); + int get hashCode => Object.hash( + runtimeType, id, name, email, passwordHash, createdAt, updatedAt); /// Create a copy of User /// with the given fields replaced by the non-null parameter values. @@ -218,21 +218,21 @@ class _$UserImpl implements _User { abstract class _User implements User { const factory _User( - {final String? id, - final String? name, - final String? email, - final String? password, + {required final String id, + required final String name, + required final String email, + final String? passwordHash, final DateTime? createdAt, final DateTime? updatedAt}) = _$UserImpl; @override - String? get id; + String get id; @override - String? get name; + String get name; @override - String? get email; + String get email; @override - String? get password; + String? get passwordHash; @override DateTime? get createdAt; @override @@ -245,3 +245,370 @@ abstract class _User implements User { _$$UserImplCopyWith<_$UserImpl> get copyWith => throw _privateConstructorUsedError; } + +/// @nodoc +mixin _$UserCreate { + String? get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + + /// Create a copy of UserCreate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCreateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCreateCopyWith<$Res> { + factory $UserCreateCopyWith( + UserCreate value, $Res Function(UserCreate) then) = + _$UserCreateCopyWithImpl<$Res, UserCreate>; + @useResult + $Res call({String? id, String name, String email, String password}); +} + +/// @nodoc +class _$UserCreateCopyWithImpl<$Res, $Val extends UserCreate> + implements $UserCreateCopyWith<$Res> { + _$UserCreateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserCreate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = null, + Object? email = null, + Object? password = null, + }) { + return _then(_value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserCreateImplCopyWith<$Res> + implements $UserCreateCopyWith<$Res> { + factory _$$UserCreateImplCopyWith( + _$UserCreateImpl value, $Res Function(_$UserCreateImpl) then) = + __$$UserCreateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String? id, String name, String email, String password}); +} + +/// @nodoc +class __$$UserCreateImplCopyWithImpl<$Res> + extends _$UserCreateCopyWithImpl<$Res, _$UserCreateImpl> + implements _$$UserCreateImplCopyWith<$Res> { + __$$UserCreateImplCopyWithImpl( + _$UserCreateImpl _value, $Res Function(_$UserCreateImpl) _then) + : super(_value, _then); + + /// Create a copy of UserCreate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = null, + Object? email = null, + Object? password = null, + }) { + return _then(_$UserCreateImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$UserCreateImpl implements _UserCreate { + const _$UserCreateImpl( + {this.id, + required this.name, + required this.email, + required this.password}); + + @override + final String? id; + @override + final String name; + @override + final String email; + @override + final String password; + + @override + String toString() { + return 'UserCreate(id: $id, name: $name, email: $email, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserCreateImpl && + (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)); + } + + @override + int get hashCode => Object.hash(runtimeType, id, name, email, password); + + /// Create a copy of UserCreate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserCreateImplCopyWith<_$UserCreateImpl> get copyWith => + __$$UserCreateImplCopyWithImpl<_$UserCreateImpl>(this, _$identity); +} + +abstract class _UserCreate implements UserCreate { + const factory _UserCreate( + {final String? id, + required final String name, + required final String email, + required final String password}) = _$UserCreateImpl; + + @override + String? get id; + @override + String get name; + @override + String get email; + @override + String get password; + + /// Create a copy of UserCreate + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserCreateImplCopyWith<_$UserCreateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$UserUpdate { + String get id => throw _privateConstructorUsedError; + String? get name => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; + String? get password => throw _privateConstructorUsedError; + + /// Create a copy of UserUpdate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserUpdateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserUpdateCopyWith<$Res> { + factory $UserUpdateCopyWith( + UserUpdate value, $Res Function(UserUpdate) then) = + _$UserUpdateCopyWithImpl<$Res, UserUpdate>; + @useResult + $Res call({String id, String? name, String? email, String? password}); +} + +/// @nodoc +class _$UserUpdateCopyWithImpl<$Res, $Val extends UserUpdate> + implements $UserUpdateCopyWith<$Res> { + _$UserUpdateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserUpdate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + }) { + return _then(_value.copyWith( + id: null == 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?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UserUpdateImplCopyWith<$Res> + implements $UserUpdateCopyWith<$Res> { + factory _$$UserUpdateImplCopyWith( + _$UserUpdateImpl value, $Res Function(_$UserUpdateImpl) then) = + __$$UserUpdateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String? name, String? email, String? password}); +} + +/// @nodoc +class __$$UserUpdateImplCopyWithImpl<$Res> + extends _$UserUpdateCopyWithImpl<$Res, _$UserUpdateImpl> + implements _$$UserUpdateImplCopyWith<$Res> { + __$$UserUpdateImplCopyWithImpl( + _$UserUpdateImpl _value, $Res Function(_$UserUpdateImpl) _then) + : super(_value, _then); + + /// Create a copy of UserUpdate + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = freezed, + Object? email = freezed, + Object? password = freezed, + }) { + return _then(_$UserUpdateImpl( + id: null == 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?, + )); + } +} + +/// @nodoc + +class _$UserUpdateImpl implements _UserUpdate { + const _$UserUpdateImpl( + {required this.id, this.name, this.email, this.password}); + + @override + final String id; + @override + final String? name; + @override + final String? email; + @override + final String? password; + + @override + String toString() { + return 'UserUpdate(id: $id, name: $name, email: $email, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserUpdateImpl && + (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)); + } + + @override + int get hashCode => Object.hash(runtimeType, id, name, email, password); + + /// Create a copy of UserUpdate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserUpdateImplCopyWith<_$UserUpdateImpl> get copyWith => + __$$UserUpdateImplCopyWithImpl<_$UserUpdateImpl>(this, _$identity); +} + +abstract class _UserUpdate implements UserUpdate { + const factory _UserUpdate( + {required final String id, + final String? name, + final String? email, + final String? password}) = _$UserUpdateImpl; + + @override + String get id; + @override + String? get name; + @override + String? get email; + @override + String? get password; + + /// Create a copy of UserUpdate + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserUpdateImplCopyWith<_$UserUpdateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/backend-dart/lib/domain/repository/user_repository.dart b/backend-dart/lib/domain/repository/user_repository.dart index 1c599b1..af94616 100755 --- a/backend-dart/lib/domain/repository/user_repository.dart +++ b/backend-dart/lib/domain/repository/user_repository.dart @@ -3,13 +3,13 @@ import 'package:backend_dart/domain/interface/error.dart'; import 'package:fpdart/fpdart.dart'; abstract class UserRepository { - TaskEither create(User user); + TaskEither create(UserCreate user); TaskEither findByEmail(String email); TaskEither findById(String id); - TaskEither update(User user); + TaskEither update(UserUpdate user); TaskEither delete(String id); diff --git a/backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart b/backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart index c37f70f..bbc87a2 100644 --- a/backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart +++ b/backend-dart/lib/infrastructure/persistence/mapper/user_dbo_mapper.dart @@ -1,37 +1,40 @@ +import 'package:backend_dart/common/extensions.dart'; +import 'package:backend_dart/common/secure_hash.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:backend_dart/infrastructure/persistence/db/model.dart'; +import 'package:backend_dart/infrastructure/persistence/db/prisma.dart'; import 'package:fpdart/fpdart.dart'; +import 'package:orm/orm.dart'; -class UserDboMapper implements IMapper { - @override +class UserDboMapper { TaskEither from(UserDbo target) => TaskEither.of(User( id: target.id!, name: target.name!, email: target.email!, - password: target.password, + passwordHash: 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); - } + TaskEither fromUpdateTo( + UserUpdate origin) => + TaskEither.of(UserDboUncheckedUpdateInput( + id: PrismaUnion.$1(origin.id), + name: origin.name.let(PrismaUnion.$1), + email: origin.email.let(PrismaUnion.$1), + password: origin.password.let(generateSecureHash).let(PrismaUnion.$1), + )); - @override - TaskEither to(User origin) => TaskEither.of(UserDbo( + TaskEither fromCreateTo(UserCreate origin) => + TaskEither.of(UserDboCreateInput( id: origin.id, name: origin.name, email: origin.email, - password: origin.password, - createdAt: origin.createdAt, - updatedAt: origin.updatedAt, + password: generateSecureHash(origin.password), )); } diff --git a/backend-dart/lib/infrastructure/persistence/prisma_database.dart b/backend-dart/lib/infrastructure/persistence/prisma_database.dart index 8276b9d..5626f86 100644 --- a/backend-dart/lib/infrastructure/persistence/prisma_database.dart +++ b/backend-dart/lib/infrastructure/persistence/prisma_database.dart @@ -1,4 +1,4 @@ -import 'package:backend_dart/domain/interface/database.dart'; +import 'package:backend_dart/domain/data/database.dart'; import 'package:backend_dart/infrastructure/persistence/db/client.dart'; import 'package:backend_dart/infrastructure/persistence/prisma_user_data_source.dart'; 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 82fe335..7b3604c 100755 --- a/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart +++ b/backend-dart/lib/infrastructure/persistence/prisma_user_data_source.dart @@ -12,36 +12,25 @@ import 'package:uuid/uuid.dart'; import 'db/client.dart'; -class PrismaUserDataSource implements UserDataSource { +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); - } + TaskEither create(UserCreate user) => mapper + .fromCreateTo(user) + .flatMap((userDbo) => TaskEither.tryCatch( + () async { + final createdUser = await prisma.userDbo.create( + data: PrismaUnion.$1(userDbo), + ); + return createdUser; + }, + (error, _) => AppError.databaseError( + message: 'Failed to create user: ${error.toString()}', + ), + ).flatMap(mapper.from)); @override TaskEither findByEmail(String email) { @@ -81,41 +70,36 @@ class PrismaUserDataSource implements UserDataSource { } @override - TaskEither update(User user) { + TaskEither update(UserUpdate user) { return mapper - .to(user) - .flatMap((userDbo) => TaskEither.tryCatch(() async { + .fromUpdateTo(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), + data: PrismaUnion.$2(userDbo), + where: UserDboWhereUniqueInput(id: user.id), ); return updatedUser; }, - (error, _) => AppError.databaseError( - message: 'Failed to update user: ${error.toString()}', - ))) - .flatMap(errorOnNull(AppError.notFound( - 'User not found', - ))) + (error, _) => AppError.databaseError( + message: 'Failed to update user: ${error.toString()}', + ), + ), + ) + .flatMap( + errorOnNull( + AppError.notFound( + 'User not found', + ), + ), + ) .flatMap(mapper.from); } diff --git a/backend-dart/pubspec.lock b/backend-dart/pubspec.lock index 8f67ebb..c6915f8 100755 --- a/backend-dart/pubspec.lock +++ b/backend-dart/pubspec.lock @@ -199,7 +199,7 @@ packages: source: hosted version: "1.11.0" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" diff --git a/backend-dart/pubspec.yaml b/backend-dart/pubspec.yaml index 53cc8f8..e99c0e1 100755 --- a/backend-dart/pubspec.yaml +++ b/backend-dart/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: json_annotation: ^4.9.0 freezed_annotation: ^2.4.4 uuid: ^4.5.1 + crypto: ^3.0.6 dev_dependencies: lints: ^3.0.0