WIP handle unique constraint exception
This commit is contained in:
		
							parent
							
								
									0162066557
								
							
						
					
					
						commit
						f33f679e12
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -36,11 +36,13 @@
 | 
				
			||||||
    "@golevelup/nestjs-rabbitmq": "^3.4.0",
 | 
					    "@golevelup/nestjs-rabbitmq": "^3.4.0",
 | 
				
			||||||
    "@grpc/grpc-js": "^1.8.0",
 | 
					    "@grpc/grpc-js": "^1.8.0",
 | 
				
			||||||
    "@grpc/proto-loader": "^0.7.4",
 | 
					    "@grpc/proto-loader": "^0.7.4",
 | 
				
			||||||
 | 
					    "@mobicoop/ddd-library": "file:../../packages/dddlibrary",
 | 
				
			||||||
    "@nestjs/axios": "^1.0.1",
 | 
					    "@nestjs/axios": "^1.0.1",
 | 
				
			||||||
    "@nestjs/common": "^9.0.0",
 | 
					    "@nestjs/common": "^9.0.0",
 | 
				
			||||||
    "@nestjs/config": "^2.2.0",
 | 
					    "@nestjs/config": "^2.2.0",
 | 
				
			||||||
    "@nestjs/core": "^9.0.0",
 | 
					    "@nestjs/core": "^9.0.0",
 | 
				
			||||||
    "@nestjs/cqrs": "^9.0.1",
 | 
					    "@nestjs/cqrs": "^9.0.1",
 | 
				
			||||||
 | 
					    "@nestjs/event-emitter": "^2.0.0",
 | 
				
			||||||
    "@nestjs/microservices": "^9.2.1",
 | 
					    "@nestjs/microservices": "^9.2.1",
 | 
				
			||||||
    "@nestjs/platform-express": "^9.0.0",
 | 
					    "@nestjs/platform-express": "^9.0.0",
 | 
				
			||||||
    "@nestjs/terminus": "^9.2.2",
 | 
					    "@nestjs/terminus": "^9.2.2",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ CREATE TABLE "auth" (
 | 
				
			||||||
-- CreateTable
 | 
					-- CreateTable
 | 
				
			||||||
CREATE TABLE "username" (
 | 
					CREATE TABLE "username" (
 | 
				
			||||||
    "username" TEXT NOT NULL,
 | 
					    "username" TEXT NOT NULL,
 | 
				
			||||||
    "uuid" UUID NOT NULL,
 | 
					    "authUuid" UUID NOT NULL,
 | 
				
			||||||
    "type" "Type" NOT NULL DEFAULT 'EMAIL',
 | 
					    "type" "Type" NOT NULL DEFAULT 'EMAIL',
 | 
				
			||||||
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
					    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
    "updatedAt" TIMESTAMP(3) NOT NULL,
 | 
					    "updatedAt" TIMESTAMP(3) NOT NULL,
 | 
				
			||||||
| 
						 | 
					@ -23,4 +23,7 @@ CREATE TABLE "username" (
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- CreateIndex
 | 
					-- CreateIndex
 | 
				
			||||||
CREATE UNIQUE INDEX "username_uuid_type_key" ON "username"("uuid", "type");
 | 
					CREATE UNIQUE INDEX "username_authUuid_type_key" ON "username"("authUuid", "type");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AddForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "username" ADD CONSTRAINT "username_authUuid_fkey" FOREIGN KEY ("authUuid") REFERENCES "auth"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
				
			||||||
| 
						 | 
					@ -11,22 +11,24 @@ datasource db {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Auth {
 | 
					model Auth {
 | 
				
			||||||
  uuid      String   @id @db.Uuid
 | 
					  uuid      String     @id @default(uuid()) @db.Uuid
 | 
				
			||||||
  password  String
 | 
					  password  String
 | 
				
			||||||
  createdAt DateTime   @default(now())
 | 
					  createdAt DateTime   @default(now())
 | 
				
			||||||
  updatedAt DateTime   @updatedAt
 | 
					  updatedAt DateTime   @updatedAt
 | 
				
			||||||
 | 
					  usernames Username[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @@map("auth")
 | 
					  @@map("auth")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Username {
 | 
					model Username {
 | 
				
			||||||
  username  String   @id
 | 
					  username  String   @id
 | 
				
			||||||
  uuid      String   @db.Uuid
 | 
					  authUuid  String   @db.Uuid
 | 
				
			||||||
  type      Type     @default(EMAIL) // type is needed in case of username update
 | 
					  type      Type     @default(EMAIL) // type is needed in case of username update
 | 
				
			||||||
  createdAt DateTime @default(now())
 | 
					  createdAt DateTime @default(now())
 | 
				
			||||||
  updatedAt DateTime @updatedAt
 | 
					  updatedAt DateTime @updatedAt
 | 
				
			||||||
 | 
					  Auth      Auth     @relation(fields: [authUuid], references: [uuid], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @@unique([uuid, type])
 | 
					  @@unique([authUuid, type])
 | 
				
			||||||
  @@map("username")
 | 
					  @@map("username")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,15 @@ import { classes } from '@automapper/classes';
 | 
				
			||||||
import { AutomapperModule } from '@automapper/nestjs';
 | 
					import { AutomapperModule } from '@automapper/nestjs';
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
import { ConfigModule } from '@nestjs/config';
 | 
					import { ConfigModule } from '@nestjs/config';
 | 
				
			||||||
import { AuthenticationModule } from './modules/authentication/authentication.module';
 | 
					 | 
				
			||||||
import { AuthorizationModule } from './modules/authorization/authorization.module';
 | 
					import { AuthorizationModule } from './modules/authorization/authorization.module';
 | 
				
			||||||
import { HealthModule } from './modules/health/health.module';
 | 
					import { HealthModule } from './modules/health/health.module';
 | 
				
			||||||
 | 
					import { AuthenticationModule } from '@modules/newauthentication/authentication.module';
 | 
				
			||||||
 | 
					import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    ConfigModule.forRoot({ isGlobal: true }),
 | 
					    ConfigModule.forRoot({ isGlobal: true }),
 | 
				
			||||||
 | 
					    EventEmitterModule.forRoot(),
 | 
				
			||||||
    AutomapperModule.forRoot({ strategyInitializer: classes() }),
 | 
					    AutomapperModule.forRoot({ strategyInitializer: classes() }),
 | 
				
			||||||
    AuthenticationModule,
 | 
					    AuthenticationModule,
 | 
				
			||||||
    AuthorizationModule,
 | 
					    AuthorizationModule,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ async function bootstrap() {
 | 
				
			||||||
      protoPath: [
 | 
					      protoPath: [
 | 
				
			||||||
        join(
 | 
					        join(
 | 
				
			||||||
          __dirname,
 | 
					          __dirname,
 | 
				
			||||||
          'modules/authentication/adapters/primaries/authentication.proto',
 | 
					          'modules/newauthentication/interface/grpc-controllers/authentication.proto',
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        join(
 | 
					        join(
 | 
				
			||||||
          __dirname,
 | 
					          __dirname,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,7 +79,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // TODO : using any is not good, but needed for nested entities
 | 
					  // TODO : using any is not good, but needed for nested entities
 | 
				
			||||||
  // TODO : Refactor for good clean architecture ?
 | 
					  // TODO : Refactor for good clean architecture ?
 | 
				
			||||||
  create = async (entity: Partial<T> | any, include?: any): Promise<T> => {
 | 
					  async create(entity: Partial<T> | any, include?: any): Promise<T> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await this.prisma[this.model].create({
 | 
					      const res = await this.prisma[this.model].create({
 | 
				
			||||||
        data: entity,
 | 
					        data: entity,
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
 | 
				
			||||||
        throw new DatabaseException();
 | 
					        throw new DatabaseException();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update = async (uuid: string, entity: Partial<T>): Promise<T> => {
 | 
					  update = async (uuid: string, entity: Partial<T>): Promise<T> => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export const AUTHENTICATION_REPOSITORY = Symbol('AUTHENTICATION_REPOSITORY');
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,66 @@
 | 
				
			||||||
 | 
					import { Mapper } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { AuthenticationEntity } from './core/domain/authentication.entity';
 | 
				
			||||||
 | 
					import { AuthenticationResponseDto } from './interface/dtos/authentication.response.dto';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AuthenticationReadModel,
 | 
				
			||||||
 | 
					  AuthenticationWriteModel,
 | 
				
			||||||
 | 
					  UsernameModel,
 | 
				
			||||||
 | 
					} from './infrastructure/authentication.repository';
 | 
				
			||||||
 | 
					import { Type, UsernameProps } from './core/domain/username.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Mapper constructs objects that are used in different layers:
 | 
				
			||||||
 | 
					 * Record is an object that is stored in a database,
 | 
				
			||||||
 | 
					 * Entity is an object that is used in application domain layer,
 | 
				
			||||||
 | 
					 * and a ResponseDTO is an object returned to a user (usually as json).
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AuthenticationMapper
 | 
				
			||||||
 | 
					  implements
 | 
				
			||||||
 | 
					    Mapper<
 | 
				
			||||||
 | 
					      AuthenticationEntity,
 | 
				
			||||||
 | 
					      AuthenticationReadModel,
 | 
				
			||||||
 | 
					      AuthenticationWriteModel,
 | 
				
			||||||
 | 
					      AuthenticationResponseDto
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  toPersistence = (entity: AuthenticationEntity): AuthenticationWriteModel => {
 | 
				
			||||||
 | 
					    const copy = entity.getProps();
 | 
				
			||||||
 | 
					    const record: AuthenticationWriteModel = {
 | 
				
			||||||
 | 
					      uuid: copy.id,
 | 
				
			||||||
 | 
					      password: copy.password,
 | 
				
			||||||
 | 
					      usernames: {
 | 
				
			||||||
 | 
					        create: copy.usernames.map((username: UsernameProps) => ({
 | 
				
			||||||
 | 
					          username: username.name,
 | 
				
			||||||
 | 
					          type: username.type,
 | 
				
			||||||
 | 
					        })),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      createdAt: copy.createdAt,
 | 
				
			||||||
 | 
					      updatedAt: copy.updatedAt,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    return record;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toDomain = (record: AuthenticationReadModel): AuthenticationEntity => {
 | 
				
			||||||
 | 
					    const entity = new AuthenticationEntity({
 | 
				
			||||||
 | 
					      id: record.uuid,
 | 
				
			||||||
 | 
					      createdAt: new Date(record.createdAt),
 | 
				
			||||||
 | 
					      updatedAt: new Date(record.updatedAt),
 | 
				
			||||||
 | 
					      props: {
 | 
				
			||||||
 | 
					        password: record.password,
 | 
				
			||||||
 | 
					        usernames: record.usernames.map((username: UsernameModel) => ({
 | 
				
			||||||
 | 
					          name: username.username,
 | 
				
			||||||
 | 
					          type: Type[username.type],
 | 
				
			||||||
 | 
					        })),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return entity;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toResponse = (entity: AuthenticationEntity): AuthenticationResponseDto => {
 | 
				
			||||||
 | 
					    const response = new AuthenticationResponseDto(entity);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					import { Module, Provider } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { CreateAuthenticationGrpcController } from './interface/grpc-controllers/create-authentication.grpc.controller';
 | 
				
			||||||
 | 
					import { CreateAuthenticationService } from './core/application/commands/create-authentication/create-authentication.service';
 | 
				
			||||||
 | 
					import { AuthenticationMapper } from './authentication.mapper';
 | 
				
			||||||
 | 
					import { AUTHENTICATION_REPOSITORY } from './authentication.di-tokens';
 | 
				
			||||||
 | 
					import { AuthenticationRepository } from './infrastructure/authentication.repository';
 | 
				
			||||||
 | 
					import { PrismaService } from './infrastructure/prisma.service';
 | 
				
			||||||
 | 
					import { CqrsModule } from '@nestjs/cqrs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const grpcControllers = [CreateAuthenticationGrpcController];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const commandHandlers: Provider[] = [CreateAuthenticationService];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mappers: Provider[] = [AuthenticationMapper];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const repositories: Provider[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    provide: AUTHENTICATION_REPOSITORY,
 | 
				
			||||||
 | 
					    useClass: AuthenticationRepository,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const orms: Provider[] = [PrismaService];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [CqrsModule],
 | 
				
			||||||
 | 
					  controllers: [...grpcControllers],
 | 
				
			||||||
 | 
					  providers: [...commandHandlers, ...mappers, ...repositories, ...orms],
 | 
				
			||||||
 | 
					  exports: [PrismaService, AuthenticationMapper, AUTHENTICATION_REPOSITORY],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AuthenticationModule {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import { Command, CommandProps } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { Username } from '../types/username';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CreateAuthenticationCommand extends Command {
 | 
				
			||||||
 | 
					  readonly password: string;
 | 
				
			||||||
 | 
					  readonly usernames: Username[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(props: CommandProps<CreateAuthenticationCommand>) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					    this.password = props.password;
 | 
				
			||||||
 | 
					    this.usernames = props.usernames;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
				
			||||||
 | 
					import { Inject } from '@nestjs/common';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AggregateID,
 | 
				
			||||||
 | 
					  ConflictException,
 | 
				
			||||||
 | 
					  UniqueConflictException,
 | 
				
			||||||
 | 
					} from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { CreateAuthenticationCommand } from './create-authentication.command';
 | 
				
			||||||
 | 
					import { AUTHENTICATION_REPOSITORY } from '@modules/newauthentication/authentication.di-tokens';
 | 
				
			||||||
 | 
					import { AuthenticationRepositoryPort } from '../ports/authentication.repository.port';
 | 
				
			||||||
 | 
					import { AuthenticationEntity } from '@modules/newauthentication/core/domain/authentication.entity';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AuthenticationAlreadyExistsException,
 | 
				
			||||||
 | 
					  UsernameAlreadyExistsException,
 | 
				
			||||||
 | 
					} from '@modules/newauthentication/core/domain/authentication.errors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@CommandHandler(CreateAuthenticationCommand)
 | 
				
			||||||
 | 
					export class CreateAuthenticationService implements ICommandHandler {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @Inject(AUTHENTICATION_REPOSITORY)
 | 
				
			||||||
 | 
					    private readonly repository: AuthenticationRepositoryPort,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async execute(command: CreateAuthenticationCommand): Promise<AggregateID> {
 | 
				
			||||||
 | 
					    const authentication = await AuthenticationEntity.create({
 | 
				
			||||||
 | 
					      password: command.password,
 | 
				
			||||||
 | 
					      usernames: command.usernames,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.repository.insert(authentication);
 | 
				
			||||||
 | 
					      return authentication.id;
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      console.log('error', error.cause);
 | 
				
			||||||
 | 
					      if (error instanceof ConflictException) {
 | 
				
			||||||
 | 
					        throw new AuthenticationAlreadyExistsException(error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        error instanceof UniqueConflictException &&
 | 
				
			||||||
 | 
					        error.message.includes('username')
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        throw new UsernameAlreadyExistsException(error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      throw error;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					import { RepositoryPort } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { AuthenticationEntity } from '@modules/newauthentication/core/domain/authentication.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AuthenticationRepositoryPort = RepositoryPort<AuthenticationEntity>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					import { Type } from '@modules/newauthentication/core/domain/username.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Username = {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  type: Type;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { v4 } from 'uuid';
 | 
				
			||||||
 | 
					import * as bcrypt from 'bcrypt';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AuthenticationProps,
 | 
				
			||||||
 | 
					  CreateAuthenticationProps,
 | 
				
			||||||
 | 
					} from './authentication.types';
 | 
				
			||||||
 | 
					import { AuthenticationCreatedDomainEvent } from './events/authentication-created.domain-events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AuthenticationEntity extends AggregateRoot<AuthenticationProps> {
 | 
				
			||||||
 | 
					  protected readonly _id: AggregateID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static create = async (
 | 
				
			||||||
 | 
					    create: CreateAuthenticationProps,
 | 
				
			||||||
 | 
					  ): Promise<AuthenticationEntity> => {
 | 
				
			||||||
 | 
					    const id = v4();
 | 
				
			||||||
 | 
					    const props: AuthenticationProps = { ...create };
 | 
				
			||||||
 | 
					    const hash = await bcrypt.hash(props.password, 10);
 | 
				
			||||||
 | 
					    const authentication = new AuthenticationEntity({
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					      props: {
 | 
				
			||||||
 | 
					        password: hash,
 | 
				
			||||||
 | 
					        usernames: props.usernames,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    authentication.addEvent(
 | 
				
			||||||
 | 
					      new AuthenticationCreatedDomainEvent({ aggregateId: id }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return authentication;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  validate(): void {
 | 
				
			||||||
 | 
					    // entity business rules validation to protect it's invariant before saving entity to a database
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { ExceptionBase } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AuthenticationAlreadyExistsException extends ExceptionBase {
 | 
				
			||||||
 | 
					  static readonly message = 'Authentication already exists';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public readonly code = 'AUTHENTICATION.ALREADY_EXISTS';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(cause?: Error, metadata?: unknown) {
 | 
				
			||||||
 | 
					    super(AuthenticationAlreadyExistsException.message, cause, metadata);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UsernameAlreadyExistsException extends ExceptionBase {
 | 
				
			||||||
 | 
					  static readonly message = 'Username already exists';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public readonly code = 'USERNAME.ALREADY_EXISTS';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(cause?: Error, metadata?: unknown) {
 | 
				
			||||||
 | 
					    super(UsernameAlreadyExistsException.message, cause, metadata);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import { UsernameProps } from './username.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// All properties that an Authentication has
 | 
				
			||||||
 | 
					export interface AuthenticationProps {
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					  usernames: UsernameProps[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Properties that are needed for an Authentication creation
 | 
				
			||||||
 | 
					export interface CreateAuthenticationProps {
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					  usernames: UsernameProps[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AuthenticationCreatedDomainEvent extends DomainEvent {
 | 
				
			||||||
 | 
					  constructor(props: DomainEventProps<AuthenticationCreatedDomainEvent>) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import { Entity } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { UsernameProps } from './username.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UsernameEntity extends Entity<UsernameProps> {
 | 
				
			||||||
 | 
					  protected _id: string;
 | 
				
			||||||
 | 
					  validate(): void {
 | 
				
			||||||
 | 
					    throw new Error('Method not implemented.');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					// All properties that a Username has
 | 
				
			||||||
 | 
					export interface UsernameProps {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  type: Type;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Properties that are needed for a Username creation
 | 
				
			||||||
 | 
					export interface CreateUsernameProps {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  type: Type;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum Type {
 | 
				
			||||||
 | 
					  EMAIL = 'EMAIL',
 | 
				
			||||||
 | 
					  PHONE = 'PHONE',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { PrismaRepositoryBase } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { AuthenticationEntity } from '../core/domain/authentication.entity';
 | 
				
			||||||
 | 
					import { AuthenticationRepositoryPort } from '../core/application/commands/ports/authentication.repository.port';
 | 
				
			||||||
 | 
					import { PrismaService } from './prisma.service';
 | 
				
			||||||
 | 
					import { AuthenticationMapper } from '../authentication.mapper';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AuthenticationBaseModel = {
 | 
				
			||||||
 | 
					  uuid: string;
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					  createdAt: Date;
 | 
				
			||||||
 | 
					  updatedAt: Date;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AuthenticationReadModel = AuthenticationBaseModel & {
 | 
				
			||||||
 | 
					  usernames: UsernameModel[];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AuthenticationWriteModel = AuthenticationBaseModel & {
 | 
				
			||||||
 | 
					  usernames: {
 | 
				
			||||||
 | 
					    create: UsernameModel[];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type UsernameModel = {
 | 
				
			||||||
 | 
					  username: string;
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 *  Repository is used for retrieving/saving domain entities
 | 
				
			||||||
 | 
					 * */
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AuthenticationRepository
 | 
				
			||||||
 | 
					  extends PrismaRepositoryBase<
 | 
				
			||||||
 | 
					    AuthenticationEntity,
 | 
				
			||||||
 | 
					    AuthenticationReadModel,
 | 
				
			||||||
 | 
					    AuthenticationWriteModel
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					  implements AuthenticationRepositoryPort
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    prisma: PrismaService,
 | 
				
			||||||
 | 
					    mapper: AuthenticationMapper,
 | 
				
			||||||
 | 
					    eventEmitter: EventEmitter2,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    super(
 | 
				
			||||||
 | 
					      prisma.auth,
 | 
				
			||||||
 | 
					      prisma,
 | 
				
			||||||
 | 
					      mapper,
 | 
				
			||||||
 | 
					      eventEmitter,
 | 
				
			||||||
 | 
					      new Logger(AuthenticationRepository.name),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { PrismaClient } from '@prisma/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class PrismaService extends PrismaClient implements OnModuleInit {
 | 
				
			||||||
 | 
					  async onModuleInit() {
 | 
				
			||||||
 | 
					    await this.$connect();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async enableShutdownHooks(app: INestApplication) {
 | 
				
			||||||
 | 
					    this.$on('beforeExit', async () => {
 | 
				
			||||||
 | 
					      await app.close();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					import { PaginatedResponseDto } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { AuthenticationResponseDto } from './authentication.response.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AauthenticationPaginatedResponseDto extends PaginatedResponseDto<AuthenticationResponseDto> {
 | 
				
			||||||
 | 
					  readonly data: readonly AuthenticationResponseDto[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					import { ResponseBase } from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AuthenticationResponseDto extends ResponseBase {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					syntax = "proto3";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package authentication;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					service AuthenticationService {
 | 
				
			||||||
 | 
					  rpc Validate(AuthenticationByUsernamePassword) returns (Id);
 | 
				
			||||||
 | 
					  rpc Create(Authentication) returns (Id);
 | 
				
			||||||
 | 
					  rpc AddUsername(Username) returns (Id);
 | 
				
			||||||
 | 
					  rpc UpdatePassword(Password) returns (Id);
 | 
				
			||||||
 | 
					  rpc UpdateUsername(Username) returns (Id);
 | 
				
			||||||
 | 
					  rpc DeleteUsername(Username) returns (Id);
 | 
				
			||||||
 | 
					  rpc Delete(Id) returns (Empty);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					message AuthenticationByUsernamePassword {
 | 
				
			||||||
 | 
					  string username = 1;
 | 
				
			||||||
 | 
					  string password = 2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					message Authentication {
 | 
				
			||||||
 | 
					  repeated Username usernames = 1;
 | 
				
			||||||
 | 
					  string password = 2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					message Password {
 | 
				
			||||||
 | 
					  string id = 1;
 | 
				
			||||||
 | 
					  string password = 2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					message Username {
 | 
				
			||||||
 | 
					  string id = 1;
 | 
				
			||||||
 | 
					  string name = 2;
 | 
				
			||||||
 | 
					  string type = 3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					message Id {
 | 
				
			||||||
 | 
					  string id = 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					message Empty {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AggregateID,
 | 
				
			||||||
 | 
					  IdResponse,
 | 
				
			||||||
 | 
					  RpcExceptionCode,
 | 
				
			||||||
 | 
					  RpcValidationPipe,
 | 
				
			||||||
 | 
					} from '@mobicoop/ddd-library';
 | 
				
			||||||
 | 
					import { Controller, UsePipes } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { CommandBus } from '@nestjs/cqrs';
 | 
				
			||||||
 | 
					import { GrpcMethod, RpcException } from '@nestjs/microservices';
 | 
				
			||||||
 | 
					import { CreateAuthenticationRequestDto } from './dtos/create-authentication.request.dto';
 | 
				
			||||||
 | 
					import { CreateAuthenticationCommand } from '@modules/newauthentication/core/application/commands/create-authentication/create-authentication.command';
 | 
				
			||||||
 | 
					import { AuthenticationAlreadyExistsException } from '@modules/newauthentication/core/domain/authentication.errors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@UsePipes(
 | 
				
			||||||
 | 
					  new RpcValidationPipe({
 | 
				
			||||||
 | 
					    whitelist: true,
 | 
				
			||||||
 | 
					    forbidUnknownValues: false,
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@Controller()
 | 
				
			||||||
 | 
					export class CreateAuthenticationGrpcController {
 | 
				
			||||||
 | 
					  constructor(private readonly commandBus: CommandBus) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @GrpcMethod('AuthenticationService', 'Create')
 | 
				
			||||||
 | 
					  async validate(data: CreateAuthenticationRequestDto): Promise<IdResponse> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const aggregateID: AggregateID = await this.commandBus.execute(
 | 
				
			||||||
 | 
					        new CreateAuthenticationCommand(data),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return new IdResponse(aggregateID);
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      if (error instanceof AuthenticationAlreadyExistsException)
 | 
				
			||||||
 | 
					        throw new RpcException({
 | 
				
			||||||
 | 
					          code: RpcExceptionCode.ALREADY_EXISTS,
 | 
				
			||||||
 | 
					          message: error.message,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      throw new RpcException({
 | 
				
			||||||
 | 
					        code: RpcExceptionCode.UNKNOWN,
 | 
				
			||||||
 | 
					        message: error.message,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { Type } from 'class-transformer';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ArrayMinSize,
 | 
				
			||||||
 | 
					  IsArray,
 | 
				
			||||||
 | 
					  IsNotEmpty,
 | 
				
			||||||
 | 
					  IsString,
 | 
				
			||||||
 | 
					  ValidateNested,
 | 
				
			||||||
 | 
					} from 'class-validator';
 | 
				
			||||||
 | 
					import { UsernameDto } from './username.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CreateAuthenticationRequestDto {
 | 
				
			||||||
 | 
					  @Type(() => UsernameDto)
 | 
				
			||||||
 | 
					  @IsArray()
 | 
				
			||||||
 | 
					  @ArrayMinSize(1)
 | 
				
			||||||
 | 
					  @ValidateNested({ each: true })
 | 
				
			||||||
 | 
					  usernames: UsernameDto[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					import { Type } from '@modules/newauthentication/core/domain/username.types';
 | 
				
			||||||
 | 
					import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UsernameDto {
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsEnum(Type)
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  type: Type;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,10 @@
 | 
				
			||||||
    "noImplicitAny": false,
 | 
					    "noImplicitAny": false,
 | 
				
			||||||
    "strictBindCallApply": false,
 | 
					    "strictBindCallApply": false,
 | 
				
			||||||
    "forceConsistentCasingInFileNames": false,
 | 
					    "forceConsistentCasingInFileNames": false,
 | 
				
			||||||
    "noFallthroughCasesInSwitch": false
 | 
					    "noFallthroughCasesInSwitch": false,
 | 
				
			||||||
 | 
					    "paths": {
 | 
				
			||||||
 | 
					      "@modules/*": ["src/modules/*"],
 | 
				
			||||||
 | 
					      "@src/*": ["src/*"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue