Merge branch 'hexagon' into 'main'
Hexagon; v1.0.0 See merge request v3/service/user!40
This commit is contained in:
commit
8e07b3b02a
|
@ -7,9 +7,9 @@ HEALTH_SERVICE_PORT=6001
|
|||
# PRISMA
|
||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=user"
|
||||
|
||||
# RABBIT MQ
|
||||
RMQ_URI=amqp://v3-broker:5672
|
||||
RMQ_EXCHANGE=mobicoop
|
||||
# MESSAGE BROKER
|
||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||
|
||||
# REDIS
|
||||
REDIS_HOST=v3-redis
|
||||
|
|
23
README.md
23
README.md
|
@ -48,24 +48,15 @@ npm run migrate
|
|||
|
||||
The app exposes the following [gRPC](https://grpc.io/) services :
|
||||
|
||||
- **FindByUuid** : find a user by its uuid
|
||||
- **FindById** : find a user by its id
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||
"id": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||
}
|
||||
```
|
||||
|
||||
- **FindAll** : find all users; you can use pagination with `page` (default:_1_) and `perPage` (default:_10_)
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"perPage": 10
|
||||
}
|
||||
```
|
||||
|
||||
- **Create** : create a user (note that uuid is optional, a uuid will be automatically attributed if it is not provided)
|
||||
- **Create** : create a user
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -82,15 +73,15 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
{
|
||||
"firstName": "Jezabel-Katarina",
|
||||
"email": "jezabel-katarina.doe@email.com",
|
||||
"uuid": "30f49838-3f24-42bb-a489-8ffb480173ae"
|
||||
"id": "30f49838-3f24-42bb-a489-8ffb480173ae"
|
||||
}
|
||||
```
|
||||
|
||||
- **Delete** : delete a user by its uuid
|
||||
- **Delete** : delete a user by its id
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||
"id": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -102,7 +93,7 @@ As mentionned earlier, RabbitMQ messages are sent after these events :
|
|||
|
||||
- **Update** (message : the updated user informations)
|
||||
|
||||
- **Delete** (message : the uuid of the deleted user)
|
||||
- **Delete** (message : the id of the deleted user)
|
||||
|
||||
Various messages are also sent for logging purpose.
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mobicoop/user",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"description": "Mobicoop V3 User Service",
|
||||
"author": "sbriat",
|
||||
"private": true,
|
||||
|
@ -17,33 +17,32 @@
|
|||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
||||
"pretty:check": "./node_modules/.bin/prettier --check .",
|
||||
"pretty": "./node_modules/.bin/prettier --write .",
|
||||
"test": "npm run migrate:test && dotenv -e .env.test jest",
|
||||
"test": "npm run test:unit && npm run test:integration",
|
||||
"test:unit": "jest --testPathPattern 'tests/unit/' --verbose",
|
||||
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose",
|
||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'",
|
||||
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
|
||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"generate": "docker exec v3-user-api sh -c 'npx prisma generate'",
|
||||
"migrate": "docker exec v3-user-api sh -c 'npx prisma migrate dev'",
|
||||
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
|
||||
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
||||
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
||||
"migrate:deploy": "npx prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automapper/classes": "^8.7.7",
|
||||
"@automapper/core": "^8.7.7",
|
||||
"@automapper/nestjs": "^8.7.7",
|
||||
"@grpc/grpc-js": "^1.8.0",
|
||||
"@grpc/proto-loader": "^0.7.4",
|
||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||
"@mobicoop/configuration-module": "^1.1.0",
|
||||
"@mobicoop/message-broker-module": "^1.0.5",
|
||||
"@mobicoop/configuration-module": "^1.2.0",
|
||||
"@mobicoop/ddd-library": "^1.0.0",
|
||||
"@mobicoop/health-module": "^2.0.0",
|
||||
"@mobicoop/message-broker-module": "^1.2.0",
|
||||
"@nestjs/cache-manager": "^1.0.0",
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/config": "^2.2.0",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
"@nestjs/cqrs": "^9.0.1",
|
||||
"@nestjs/event-emitter": "^2.0.0",
|
||||
"@nestjs/microservices": "^9.2.1",
|
||||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/terminus": "^9.2.2",
|
||||
|
@ -54,6 +53,7 @@
|
|||
"cache-manager": "^5.2.1",
|
||||
"cache-manager-ioredis-yet": "^1.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"ioredis": "^5.3.0"
|
||||
},
|
||||
|
@ -63,6 +63,7 @@
|
|||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/jest": "^29.2.5",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
@ -84,12 +85,13 @@
|
|||
"ts"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".dto.ts",
|
||||
".constants.ts",
|
||||
".di-tokens.ts",
|
||||
".response.ts",
|
||||
".port.ts",
|
||||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
|
@ -101,15 +103,20 @@
|
|||
"**/*.(t|j)s"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".dto.ts",
|
||||
".constants.ts",
|
||||
".di-tokens.ts",
|
||||
".response.ts",
|
||||
".port.ts",
|
||||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"moduleNameMapper": {
|
||||
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||
"^@src(.*)": "<rootDir>$1"
|
||||
},
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export const MESSAGE_BROKER_PUBLISHER = Symbol();
|
||||
export const MESSAGE_PUBLISHER = Symbol();
|
|
@ -1,70 +1,67 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
import { UserModule } from './modules/user/user.module';
|
||||
import {
|
||||
ConfigurationModule,
|
||||
ConfigurationModuleOptions,
|
||||
} from '@mobicoop/configuration-module';
|
||||
import {
|
||||
MessageBrokerModule,
|
||||
MessageBrokerModuleOptions,
|
||||
} from '@mobicoop/message-broker-module';
|
||||
HealthModule,
|
||||
HealthModuleOptions,
|
||||
HealthRepositoryPort,
|
||||
} from '@mobicoop/health-module';
|
||||
import { MessagerModule } from './modules/messager/messager.module';
|
||||
import { USER_REPOSITORY } from './modules/user/user.di-tokens';
|
||||
import { MESSAGE_PUBLISHER } from './modules/messager/messager.di-tokens';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
||||
UserModule,
|
||||
MessageBrokerModule.forRootAsync(
|
||||
{
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<MessageBrokerModuleOptions> => ({
|
||||
EventEmitterModule.forRoot(),
|
||||
ConfigurationModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<ConfigurationModuleOptions> => ({
|
||||
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
|
||||
messageBroker: {
|
||||
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||
handlers: {},
|
||||
}),
|
||||
},
|
||||
false,
|
||||
),
|
||||
ConfigurationModule.forRootAsync(
|
||||
{
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<ConfigurationModuleOptions> => ({
|
||||
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
|
||||
messageBroker: {
|
||||
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||
},
|
||||
redis: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
},
|
||||
setConfigurationBrokerQueue: 'user-configuration-create-update',
|
||||
deleteConfigurationQueue: 'user-configuration-delete',
|
||||
propagateConfigurationQueue: 'user-configuration-propagate',
|
||||
}),
|
||||
}),
|
||||
HealthModule.forRootAsync({
|
||||
imports: [UserModule, MessagerModule],
|
||||
inject: [USER_REPOSITORY, MESSAGE_PUBLISHER],
|
||||
useFactory: async (
|
||||
userRepository: HealthRepositoryPort,
|
||||
messagePublisher: MessagePublisherPort,
|
||||
): Promise<HealthModuleOptions> => ({
|
||||
serviceName: 'user',
|
||||
criticalLoggingKey: 'logging.user.health.crit',
|
||||
checkRepositories: [
|
||||
{
|
||||
name: 'UserRepository',
|
||||
repository: userRepository,
|
||||
},
|
||||
redis: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
},
|
||||
setConfigurationBrokerRoutingKeys: [
|
||||
'configuration.create',
|
||||
'configuration.update',
|
||||
],
|
||||
deleteConfigurationRoutingKey: 'configuration.delete',
|
||||
propagateConfigurationRoutingKey: 'configuration.propagate',
|
||||
setConfigurationBrokerQueue: 'user-configuration-create-update',
|
||||
deleteConfigurationQueue: 'user-configuration-delete',
|
||||
propagateConfigurationQueue: 'user-configuration-propagate',
|
||||
}),
|
||||
},
|
||||
true,
|
||||
),
|
||||
HealthModule,
|
||||
],
|
||||
messagePublisher,
|
||||
}),
|
||||
}),
|
||||
UserModule,
|
||||
MessagerModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
exports: [UserModule, MessagerModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
@ -2,7 +2,6 @@ syntax = "proto3";
|
|||
|
||||
package health;
|
||||
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
}
|
||||
|
@ -18,4 +17,5 @@ message HealthCheckResponse {
|
|||
NOT_SERVING = 2;
|
||||
}
|
||||
ServingStatus status = 1;
|
||||
string message = 2;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export interface IPublishMessage {
|
||||
publish(routingKey: string, message: string): void;
|
||||
}
|
|
@ -13,8 +13,8 @@ async function bootstrap() {
|
|||
options: {
|
||||
package: ['user', 'health'],
|
||||
protoPath: [
|
||||
join(__dirname, 'modules/user/adapters/primaries/user.proto'),
|
||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
||||
join(__dirname, 'modules/user/interface/grpc-controllers/user.proto'),
|
||||
join(__dirname, 'health.proto'),
|
||||
],
|
||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||
loader: { keepCase: true },
|
||||
|
|
|
@ -1,259 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { DatabaseException } from '../../exceptions/database.exception';
|
||||
import { ICollection } from '../../interfaces/collection.interface';
|
||||
import { IRepository } from '../../interfaces/repository.interface';
|
||||
import { PrismaService } from './prisma-service';
|
||||
|
||||
/**
|
||||
* Child classes MUST redefined _model property with appropriate model name
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class PrismaRepository<T> implements IRepository<T> {
|
||||
protected model: string;
|
||||
|
||||
constructor(protected readonly prisma: PrismaService) {}
|
||||
|
||||
findAll = async (
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
where?: any,
|
||||
include?: any,
|
||||
): Promise<ICollection<T>> => {
|
||||
const [data, total] = await this.prisma.$transaction([
|
||||
this.prisma[this.model].findMany({
|
||||
where,
|
||||
include,
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage,
|
||||
}),
|
||||
this.prisma[this.model].count({
|
||||
where,
|
||||
}),
|
||||
]);
|
||||
return Promise.resolve({
|
||||
data,
|
||||
total,
|
||||
});
|
||||
};
|
||||
|
||||
findOneByUuid = async (uuid: string): Promise<T> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].findUnique({
|
||||
where: { uuid },
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findOne = async (where: any, include?: any): Promise<T> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].findFirst({
|
||||
where: where,
|
||||
include: include,
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO : using any is not good, but needed for nested entities
|
||||
// TODO : Refactor for good clean architecture ?
|
||||
create = async (entity: Partial<T> | any, include?: any): Promise<T> => {
|
||||
try {
|
||||
const res = await this.prisma[this.model].create({
|
||||
data: entity,
|
||||
include: include,
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
update = async (uuid: string, entity: Partial<T>): Promise<T> => {
|
||||
try {
|
||||
const updatedEntity = await this.prisma[this.model].update({
|
||||
where: { uuid },
|
||||
data: entity,
|
||||
});
|
||||
return updatedEntity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWhere = async (
|
||||
where: any,
|
||||
entity: Partial<T> | any,
|
||||
include?: any,
|
||||
): Promise<T> => {
|
||||
try {
|
||||
const updatedEntity = await this.prisma[this.model].update({
|
||||
where: where,
|
||||
data: entity,
|
||||
include: include,
|
||||
});
|
||||
|
||||
return updatedEntity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
delete = async (uuid: string): Promise<T> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].delete({
|
||||
where: { uuid },
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
deleteMany = async (where: any): Promise<void> => {
|
||||
try {
|
||||
const entity = await this.prisma[this.model].deleteMany({
|
||||
where: where,
|
||||
});
|
||||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findAllByQuery = async (
|
||||
include: string[],
|
||||
where: string[],
|
||||
): Promise<ICollection<T>> => {
|
||||
const query = `SELECT ${include.join(',')} FROM ${
|
||||
this.model
|
||||
} WHERE ${where.join(' AND ')}`;
|
||||
const data: T[] = await this.prisma.$queryRawUnsafe(query);
|
||||
return Promise.resolve({
|
||||
data,
|
||||
total: data.length,
|
||||
});
|
||||
};
|
||||
|
||||
createWithFields = async (fields: object): Promise<number> => {
|
||||
try {
|
||||
const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join(
|
||||
'","',
|
||||
)}") VALUES (${Object.values(fields).join(',')})`;
|
||||
return await this.prisma.$executeRawUnsafe(command);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWithFields = async (uuid: string, entity: object): Promise<number> => {
|
||||
entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
|
||||
const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
|
||||
try {
|
||||
const command = `UPDATE ${this.model} SET ${values.join(
|
||||
', ',
|
||||
)} WHERE uuid = '${uuid}'`;
|
||||
return await this.prisma.$executeRawUnsafe(command);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
healthCheck = async (): Promise<boolean> => {
|
||||
try {
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from './adapters/secondaries/prisma-service';
|
||||
import { UserRepository } from './domain/user-repository';
|
||||
|
||||
@Module({
|
||||
providers: [PrismaService, UserRepository],
|
||||
exports: [PrismaService, UserRepository],
|
||||
})
|
||||
export class DatabaseModule {}
|
|
@ -1,3 +0,0 @@
|
|||
import { PrismaRepository } from '../adapters/secondaries/prisma-repository.abstract';
|
||||
|
||||
export class UserRepository<T> extends PrismaRepository<T> {}
|
|
@ -1,24 +0,0 @@
|
|||
export class DatabaseException implements Error {
|
||||
name: string;
|
||||
message: string;
|
||||
|
||||
constructor(
|
||||
private _type: string = 'unknown',
|
||||
private _code: string = '',
|
||||
message?: string,
|
||||
) {
|
||||
this.name = 'DatabaseException';
|
||||
this.message = message ?? 'An error occured with the database.';
|
||||
if (this.message.includes('Unique constraint failed')) {
|
||||
this.message = 'Already exists.';
|
||||
}
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
get code(): string {
|
||||
return this._code;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export interface ICollection<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { ICollection } from './collection.interface';
|
||||
|
||||
export interface IRepository<T> {
|
||||
findAll(
|
||||
page: number,
|
||||
perPage: number,
|
||||
params?: any,
|
||||
include?: any,
|
||||
): Promise<ICollection<T>>;
|
||||
findOne(where: any, include?: any): Promise<T>;
|
||||
findOneByUuid(uuid: string, include?: any): Promise<T>;
|
||||
create(entity: Partial<T> | any, include?: any): Promise<T>;
|
||||
update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
|
||||
updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
|
||||
delete(uuid: string): Promise<T>;
|
||||
deleteMany(where: any): Promise<void>;
|
||||
healthCheck(): Promise<boolean>;
|
||||
}
|
|
@ -1,571 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaService } from '../../adapters/secondaries/prisma-service';
|
||||
import { PrismaRepository } from '../../adapters/secondaries/prisma-repository.abstract';
|
||||
import { DatabaseException } from '../../exceptions/database.exception';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
class FakeEntity {
|
||||
uuid?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
let entityId = 2;
|
||||
const entityUuid = 'uuid-';
|
||||
const entityName = 'name-';
|
||||
|
||||
const createRandomEntity = (): FakeEntity => {
|
||||
const entity: FakeEntity = {
|
||||
uuid: `${entityUuid}${entityId}`,
|
||||
name: `${entityName}${entityId}`,
|
||||
};
|
||||
|
||||
entityId++;
|
||||
|
||||
return entity;
|
||||
};
|
||||
|
||||
const fakeEntityToCreate: FakeEntity = {
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
const fakeEntityCreated: FakeEntity = {
|
||||
...fakeEntityToCreate,
|
||||
uuid: 'some-uuid',
|
||||
};
|
||||
|
||||
const fakeEntities: FakeEntity[] = [];
|
||||
Array.from({ length: 10 }).forEach(() => {
|
||||
fakeEntities.push(createRandomEntity());
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
class FakePrismaRepository extends PrismaRepository<FakeEntity> {
|
||||
protected model = 'fake';
|
||||
}
|
||||
|
||||
class FakePrismaService extends PrismaService {
|
||||
fake: any;
|
||||
}
|
||||
|
||||
const mockPrismaService = {
|
||||
$transaction: jest.fn().mockImplementation(async (data: any) => {
|
||||
const entities = await data[0];
|
||||
if (entities.length == 1) {
|
||||
return Promise.resolve([[fakeEntityCreated], 1]);
|
||||
}
|
||||
|
||||
return Promise.resolve([fakeEntities, fakeEntities.length]);
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
$queryRawUnsafe: jest.fn().mockImplementation((query?: string) => {
|
||||
return Promise.resolve(fakeEntities);
|
||||
}),
|
||||
$executeRawUnsafe: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Error('an unknown error');
|
||||
})
|
||||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new Error('an unknown error');
|
||||
}),
|
||||
$queryRaw: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return true;
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
}),
|
||||
fake: {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Error('an unknown error');
|
||||
}),
|
||||
|
||||
findMany: jest.fn().mockImplementation((params?: any) => {
|
||||
if (params?.where?.limit == 1) {
|
||||
return Promise.resolve([fakeEntityCreated]);
|
||||
}
|
||||
|
||||
return Promise.resolve(fakeEntities);
|
||||
}),
|
||||
count: jest.fn().mockResolvedValue(fakeEntities.length),
|
||||
|
||||
findUnique: jest.fn().mockImplementation(async (params?: any) => {
|
||||
let entity;
|
||||
|
||||
if (params?.where?.uuid) {
|
||||
entity = fakeEntities.find(
|
||||
(entity) => entity.uuid === params?.where?.uuid,
|
||||
);
|
||||
}
|
||||
|
||||
if (!entity && params?.where?.uuid == 'unknown') {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
} else if (!entity) {
|
||||
throw new Error('no entity');
|
||||
}
|
||||
|
||||
return entity;
|
||||
}),
|
||||
|
||||
findFirst: jest
|
||||
.fn()
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
if (params?.where?.name) {
|
||||
return Promise.resolve(
|
||||
fakeEntities.find((entity) => entity.name === params?.where?.name),
|
||||
);
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Error('an unknown error');
|
||||
}),
|
||||
|
||||
update: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce((params: any) => {
|
||||
const entity = fakeEntities.find(
|
||||
(entity) => entity.name === params.where.name,
|
||||
);
|
||||
Object.entries(params.data).map(([key, value]) => {
|
||||
entity[key] = value;
|
||||
});
|
||||
|
||||
return Promise.resolve(entity);
|
||||
})
|
||||
.mockImplementation((params: any) => {
|
||||
const entity = fakeEntities.find(
|
||||
(entity) => entity.uuid === params.where.uuid,
|
||||
);
|
||||
Object.entries(params.data).map(([key, value]) => {
|
||||
entity[key] = value;
|
||||
});
|
||||
|
||||
return Promise.resolve(entity);
|
||||
}),
|
||||
|
||||
delete: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementation((params: any) => {
|
||||
let found = false;
|
||||
|
||||
fakeEntities.forEach((entity, index) => {
|
||||
if (entity.uuid === params?.where?.uuid) {
|
||||
found = true;
|
||||
fakeEntities.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new Error();
|
||||
}
|
||||
}),
|
||||
|
||||
deleteMany: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
.mockImplementation((params: any) => {
|
||||
let found = false;
|
||||
|
||||
fakeEntities.forEach((entity, index) => {
|
||||
if (entity.uuid === params?.where?.uuid) {
|
||||
found = true;
|
||||
fakeEntities.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new Error();
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe('PrismaRepository', () => {
|
||||
let fakeRepository: FakePrismaRepository;
|
||||
let prisma: FakePrismaService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FakePrismaRepository,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
fakeRepository = module.get<FakePrismaRepository>(FakePrismaRepository);
|
||||
prisma = module.get<PrismaService>(PrismaService) as FakePrismaService;
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(fakeRepository).toBeDefined();
|
||||
expect(prisma).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an array of entities', async () => {
|
||||
jest.spyOn(prisma.fake, 'findMany');
|
||||
jest.spyOn(prisma.fake, 'count');
|
||||
jest.spyOn(prisma, '$transaction');
|
||||
|
||||
const entities = await fakeRepository.findAll();
|
||||
expect(entities).toStrictEqual({
|
||||
data: fakeEntities,
|
||||
total: fakeEntities.length,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array containing only one entity', async () => {
|
||||
const entities = await fakeRepository.findAll(1, 10, { limit: 1 });
|
||||
|
||||
expect(prisma.fake.findMany).toHaveBeenCalledWith({
|
||||
skip: 0,
|
||||
take: 10,
|
||||
where: { limit: 1 },
|
||||
});
|
||||
expect(entities).toEqual({
|
||||
data: [fakeEntityCreated],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create an entity', async () => {
|
||||
jest.spyOn(prisma.fake, 'create');
|
||||
|
||||
const newEntity = await fakeRepository.create(fakeEntityToCreate);
|
||||
expect(newEntity).toBe(fakeEntityCreated);
|
||||
expect(prisma.fake.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.create(fakeEntityToCreate),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.create(fakeEntityToCreate),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneByUuid', () => {
|
||||
it('should find an entity by uuid', async () => {
|
||||
const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid);
|
||||
expect(entity).toBe(fakeEntities[0]);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOneByUuid('unknown'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOneByUuid('wrong-uuid'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find one entity', async () => {
|
||||
const entity = await fakeRepository.findOne({
|
||||
name: fakeEntities[0].name,
|
||||
});
|
||||
|
||||
expect(entity.name).toBe(fakeEntities[0].name);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOne({
|
||||
name: fakeEntities[0].name,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for unknown error', async () => {
|
||||
await expect(
|
||||
fakeRepository.findOne({
|
||||
name: fakeEntities[0].name,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
await expect(
|
||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should update an entity with name', async () => {
|
||||
const newName = 'new-random-name';
|
||||
|
||||
await fakeRepository.updateWhere(
|
||||
{ name: fakeEntities[0].name },
|
||||
{
|
||||
name: newName,
|
||||
},
|
||||
);
|
||||
expect(fakeEntities[0].name).toBe(newName);
|
||||
});
|
||||
|
||||
it('should update an entity with uuid', async () => {
|
||||
const newName = 'random-name';
|
||||
|
||||
await fakeRepository.update(fakeEntities[0].uuid, {
|
||||
name: newName,
|
||||
});
|
||||
expect(fakeEntities[0].name).toBe(newName);
|
||||
});
|
||||
|
||||
it("should throw an exception if an entity doesn't exist", async () => {
|
||||
await expect(
|
||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
await expect(
|
||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete an entity', async () => {
|
||||
const savedUuid = fakeEntities[0].uuid;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const res = await fakeRepository.delete(savedUuid);
|
||||
|
||||
const deletedEntity = fakeEntities.find(
|
||||
(entity) => entity.uuid === savedUuid,
|
||||
);
|
||||
expect(deletedEntity).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw an exception if an entity doesn't exist", async () => {
|
||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMany', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should delete entities based on their uuid', async () => {
|
||||
const savedUuid = fakeEntities[0].uuid;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const res = await fakeRepository.deleteMany({ uuid: savedUuid });
|
||||
|
||||
const deletedEntity = fakeEntities.find(
|
||||
(entity) => entity.uuid === savedUuid,
|
||||
);
|
||||
expect(deletedEntity).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should throw an exception if an entity doesn't exist", async () => {
|
||||
await expect(
|
||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAllByquery', () => {
|
||||
it('should return an array of entities', async () => {
|
||||
const entities = await fakeRepository.findAllByQuery(
|
||||
['uuid', 'name'],
|
||||
['name is not null'],
|
||||
);
|
||||
expect(entities).toStrictEqual({
|
||||
data: fakeEntities,
|
||||
total: fakeEntities.length,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWithFields', () => {
|
||||
it('should create an entity', async () => {
|
||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
||||
|
||||
const newEntity = await fakeRepository.createWithFields({
|
||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
name: 'my-name',
|
||||
});
|
||||
expect(newEntity).toBe(fakeEntityCreated);
|
||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.createWithFields({
|
||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
name: 'my-name',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.createWithFields({
|
||||
name: 'my-name',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateWithFields', () => {
|
||||
it('should update an entity', async () => {
|
||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
||||
|
||||
const updatedEntity = await fakeRepository.updateWithFields(
|
||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
{
|
||||
name: 'my-name',
|
||||
},
|
||||
);
|
||||
expect(updatedEntity).toBe(fakeEntityCreated);
|
||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(
|
||||
fakeRepository.updateWithFields(
|
||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
{
|
||||
name: 'my-name',
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
|
||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
||||
await expect(
|
||||
fakeRepository.updateWithFields(
|
||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
||||
{
|
||||
name: 'my-name',
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('healthCheck', () => {
|
||||
it('should throw a DatabaseException for client error', async () => {
|
||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a healthy result', async () => {
|
||||
const res = await fakeRepository.healthCheck();
|
||||
expect(res).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should throw an exception if database is not available', async () => {
|
||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
||||
DatabaseException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { GrpcMethod } from '@nestjs/microservices';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0,
|
||||
SERVING = 1,
|
||||
NOT_SERVING = 2,
|
||||
}
|
||||
|
||||
interface HealthCheckRequest {
|
||||
service: string;
|
||||
}
|
||||
|
||||
interface HealthCheckResponse {
|
||||
status: ServingStatus;
|
||||
}
|
||||
|
||||
@Controller()
|
||||
export class HealthServerController {
|
||||
constructor(
|
||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('Health', 'Check')
|
||||
async check(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
data: HealthCheckRequest,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
metadata: any,
|
||||
): Promise<HealthCheckResponse> {
|
||||
const healthCheck = await this.prismaHealthIndicatorUseCase.isHealthy(
|
||||
'prisma',
|
||||
);
|
||||
return {
|
||||
status:
|
||||
healthCheck['prisma'].status == 'up'
|
||||
? ServingStatus.SERVING
|
||||
: ServingStatus.NOT_SERVING,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import { Controller, Get, Inject } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheckService,
|
||||
HealthCheck,
|
||||
HealthCheckResult,
|
||||
} from '@nestjs/terminus';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
import { MESSAGE_PUBLISHER } from 'src/app.constants';
|
||||
import { IPublishMessage } from 'src/interfaces/message-publisher';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
||||
private readonly healthCheckService: HealthCheckService,
|
||||
@Inject(MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: IPublishMessage,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
async check() {
|
||||
try {
|
||||
return await this.healthCheckService.check([
|
||||
async () => this.prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
||||
]);
|
||||
} catch (error) {
|
||||
const healthCheckResult: HealthCheckResult = error.response;
|
||||
this.messagePublisher.publish(
|
||||
'logging.user.health.crit',
|
||||
JSON.stringify(healthCheckResult.error),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { IPublishMessage } from 'src/interfaces/message-publisher';
|
||||
|
||||
@Injectable()
|
||||
export class MessagePublisher implements IPublishMessage {
|
||||
constructor(
|
||||
@Inject(MESSAGE_BROKER_PUBLISHER)
|
||||
private readonly messageBrokerPublisher: MessageBrokerPublisher,
|
||||
) {}
|
||||
|
||||
publish = (routingKey: string, message: string): void => {
|
||||
this.messageBrokerPublisher.publish(routingKey, message);
|
||||
};
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheckError,
|
||||
HealthIndicator,
|
||||
HealthIndicatorResult,
|
||||
} from '@nestjs/terminus';
|
||||
import { UsersRepository } from '../../../user/adapters/secondaries/users.repository';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
|
||||
constructor(private readonly repository: UsersRepository) {
|
||||
super();
|
||||
}
|
||||
|
||||
isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
|
||||
try {
|
||||
await this.repository.healthCheck();
|
||||
return this.getStatus(key, true);
|
||||
} catch (e) {
|
||||
throw new HealthCheckError('Prisma', {
|
||||
prisma: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { HealthServerController } from './adapters/primaries/health-server.controller';
|
||||
import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase';
|
||||
import { UsersRepository } from '../user/adapters/secondaries/users.repository';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { HealthController } from './adapters/primaries/health.controller';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { MESSAGE_BROKER_PUBLISHER, MESSAGE_PUBLISHER } from 'src/app.constants';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { MessagePublisher } from './adapters/secondaries/message-publisher';
|
||||
|
||||
@Module({
|
||||
imports: [TerminusModule, DatabaseModule],
|
||||
controllers: [HealthServerController, HealthController],
|
||||
providers: [
|
||||
PrismaHealthIndicatorUseCase,
|
||||
UsersRepository,
|
||||
{
|
||||
provide: MESSAGE_BROKER_PUBLISHER,
|
||||
useClass: MessageBrokerPublisher,
|
||||
},
|
||||
{
|
||||
provide: MESSAGE_PUBLISHER,
|
||||
useClass: MessagePublisher,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class HealthModule {}
|
|
@ -1,36 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MessagePublisher } from '../../adapters/secondaries/message-publisher';
|
||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
|
||||
|
||||
const mockMessageBrokerPublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Messager', () => {
|
||||
let messagePublisher: MessagePublisher;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
MessagePublisher,
|
||||
{
|
||||
provide: MESSAGE_BROKER_PUBLISHER,
|
||||
useValue: mockMessageBrokerPublisher,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
messagePublisher = module.get<MessagePublisher>(MessagePublisher);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(messagePublisher).toBeDefined();
|
||||
});
|
||||
|
||||
it('should publish a message', async () => {
|
||||
jest.spyOn(mockMessageBrokerPublisher, 'publish');
|
||||
messagePublisher.publish('health.info', 'my-test');
|
||||
expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
import { UsersRepository } from '../../../user/adapters/secondaries/users.repository';
|
||||
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
const mockUsersRepository = {
|
||||
healthCheck: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(true);
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Prisma.PrismaClientKnownRequestError('Service unavailable', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
describe('PrismaHealthIndicatorUseCase', () => {
|
||||
let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: UsersRepository,
|
||||
useValue: mockUsersRepository,
|
||||
},
|
||||
PrismaHealthIndicatorUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>(
|
||||
PrismaHealthIndicatorUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(prismaHealthIndicatorUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should check health successfully', async () => {
|
||||
const healthIndicatorResult: HealthIndicatorResult =
|
||||
await prismaHealthIndicatorUseCase.isHealthy('prisma');
|
||||
|
||||
expect(healthIndicatorResult['prisma'].status).toBe('up');
|
||||
});
|
||||
|
||||
it('should throw an error if database is unavailable', async () => {
|
||||
await expect(
|
||||
prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
||||
).rejects.toBeInstanceOf(HealthCheckError);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export const MESSAGE_PUBLISHER = Symbol('MESSAGE_PUBLISHER');
|
|
@ -0,0 +1,36 @@
|
|||
import { Module, Provider } from '@nestjs/common';
|
||||
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||
import {
|
||||
MessageBrokerModule,
|
||||
MessageBrokerModuleOptions,
|
||||
MessageBrokerPublisher,
|
||||
} from '@mobicoop/message-broker-module';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
const imports = [
|
||||
MessageBrokerModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (
|
||||
configService: ConfigService,
|
||||
): Promise<MessageBrokerModuleOptions> => ({
|
||||
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||
name: 'user',
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
const providers: Provider[] = [
|
||||
{
|
||||
provide: MESSAGE_PUBLISHER,
|
||||
useClass: MessageBrokerPublisher,
|
||||
},
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports,
|
||||
providers,
|
||||
exports: [MESSAGE_PUBLISHER],
|
||||
})
|
||||
export class MessagerModule {}
|
|
@ -1,121 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { Controller, UseInterceptors, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
||||
import { CreateUserCommand } from '../../commands/create-user.command';
|
||||
import { DeleteUserCommand } from '../../commands/delete-user.command';
|
||||
import { UpdateUserCommand } from '../../commands/update-user.command';
|
||||
import { CreateUserRequest } from '../../domain/dtos/create-user.request';
|
||||
import { FindAllUsersRequest } from '../../domain/dtos/find-all-users.request';
|
||||
import { FindUserByUuidRequest } from '../../domain/dtos/find-user-by-uuid.request';
|
||||
import { UpdateUserRequest } from '../../domain/dtos/update-user.request';
|
||||
import { User } from '../../domain/entities/user';
|
||||
import { FindAllUsersQuery } from '../../queries/find-all-users.query';
|
||||
import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
|
||||
import { UserPresenter } from './user.presenter';
|
||||
import { ICollection } from '../../../database/interfaces/collection.interface';
|
||||
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
|
||||
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class UserController {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly queryBus: QueryBus,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('UsersService', 'FindAll')
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@CacheKey('UsersServiceFindAll')
|
||||
async findAll(data: FindAllUsersRequest): Promise<ICollection<User>> {
|
||||
const userCollection = await this.queryBus.execute(
|
||||
new FindAllUsersQuery(data),
|
||||
);
|
||||
return Promise.resolve({
|
||||
data: userCollection.data.map((user: User) =>
|
||||
this.mapper.map(user, User, UserPresenter),
|
||||
),
|
||||
total: userCollection.total,
|
||||
});
|
||||
}
|
||||
|
||||
@GrpcMethod('UsersService', 'FindOneByUuid')
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@CacheKey('UsersServiceFindOneByUuid')
|
||||
async findOneByUuid(data: FindUserByUuidRequest): Promise<UserPresenter> {
|
||||
try {
|
||||
const user = await this.queryBus.execute(new FindUserByUuidQuery(data));
|
||||
return this.mapper.map(user, User, UserPresenter);
|
||||
} catch (error) {
|
||||
throw new RpcException({
|
||||
code: 5,
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('UsersService', 'Create')
|
||||
async createUser(data: CreateUserRequest): Promise<UserPresenter> {
|
||||
try {
|
||||
const user = await this.commandBus.execute(new CreateUserCommand(data));
|
||||
return this.mapper.map(user, User, UserPresenter);
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('Already exists')) {
|
||||
throw new RpcException({
|
||||
code: 6,
|
||||
message: 'User already exists',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('UsersService', 'Update')
|
||||
async updateUser(data: UpdateUserRequest): Promise<UserPresenter> {
|
||||
try {
|
||||
const user = await this.commandBus.execute(new UpdateUserCommand(data));
|
||||
|
||||
return this.mapper.map(user, User, UserPresenter);
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('not found')) {
|
||||
throw new RpcException({
|
||||
code: 5,
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({});
|
||||
}
|
||||
}
|
||||
|
||||
@GrpcMethod('UsersService', 'Delete')
|
||||
async deleteUser(data: FindUserByUuidRequest): Promise<void> {
|
||||
try {
|
||||
await this.commandBus.execute(new DeleteUserCommand(data.uuid));
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
if (e instanceof DatabaseException) {
|
||||
if (e.message.includes('not found')) {
|
||||
throw new RpcException({
|
||||
code: 5,
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new RpcException({});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class UserPresenter {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
firstName: string;
|
||||
|
||||
@AutoMap()
|
||||
lastName: string;
|
||||
|
||||
@AutoMap()
|
||||
email: string;
|
||||
|
||||
@AutoMap()
|
||||
phone: string;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
|
||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
|
||||
@Injectable()
|
||||
export class MessagePublisher implements IPublishMessage {
|
||||
constructor(
|
||||
@Inject(MESSAGE_BROKER_PUBLISHER)
|
||||
private readonly messageBrokerPublisher: MessageBrokerPublisher,
|
||||
) {}
|
||||
|
||||
publish = (routingKey: string, message: string): void => {
|
||||
this.messageBrokerPublisher.publish(routingKey, message);
|
||||
};
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { UserRepository } from '../../../database/domain/user-repository';
|
||||
import { User } from '../../domain/entities/user';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepository extends UserRepository<User> {
|
||||
protected model = 'user';
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { CreateUserRequest } from '../domain/dtos/create-user.request';
|
||||
|
||||
export class CreateUserCommand {
|
||||
readonly createUserRequest: CreateUserRequest;
|
||||
|
||||
constructor(request: CreateUserRequest) {
|
||||
this.createUserRequest = request;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export class DeleteUserCommand {
|
||||
readonly uuid: string;
|
||||
|
||||
constructor(uuid: string) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { UpdateUserRequest } from '../domain/dtos/update-user.request';
|
||||
|
||||
export class UpdateUserCommand {
|
||||
readonly updateUserRequest: UpdateUserRequest;
|
||||
|
||||
constructor(request: UpdateUserRequest) {
|
||||
this.updateUserRequest = request;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class CreateUserCommand extends Command {
|
||||
readonly firstName?: string;
|
||||
readonly lastName?: string;
|
||||
readonly email?: string;
|
||||
readonly phone?: string;
|
||||
|
||||
constructor(props: CommandProps<CreateUserCommand>) {
|
||||
super(props);
|
||||
this.firstName = props.firstName;
|
||||
this.lastName = props.lastName;
|
||||
this.email = props.email;
|
||||
this.phone = props.phone;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import {
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
UniqueConstraintException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { CreateUserCommand } from './create-user.command';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { UserRepositoryPort } from '../../ports/user.repository.port';
|
||||
import { UserEntity } from '../../../domain/user.entity';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
UserAlreadyExistsException,
|
||||
} from '../../../domain/user.errors';
|
||||
|
||||
@CommandHandler(CreateUserCommand)
|
||||
export class CreateUserService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY)
|
||||
private readonly repository: UserRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateUserCommand): Promise<AggregateID> {
|
||||
const user = UserEntity.create({
|
||||
firstName: command.firstName,
|
||||
lastName: command.lastName,
|
||||
email: command.email,
|
||||
phone: command.phone,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.repository.insert(user);
|
||||
return user.id;
|
||||
} catch (error: any) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw new UserAlreadyExistsException(error);
|
||||
}
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('email')
|
||||
) {
|
||||
throw new EmailAlreadyExistsException(error);
|
||||
}
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('phone')
|
||||
) {
|
||||
throw new PhoneAlreadyExistsException(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class DeleteUserCommand extends Command {
|
||||
constructor(props: CommandProps<DeleteUserCommand>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { DeleteUserCommand } from './delete-user.command';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { UserRepositoryPort } from '../../ports/user.repository.port';
|
||||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
|
||||
@CommandHandler(DeleteUserCommand)
|
||||
export class DeleteUserService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY)
|
||||
private readonly userRepository: UserRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteUserCommand): Promise<boolean> {
|
||||
const user: UserEntity = await this.userRepository.findOneById(command.id);
|
||||
user.delete();
|
||||
const isDeleted: boolean = await this.userRepository.delete(user);
|
||||
return isDeleted;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UpdateUserCommand extends Command {
|
||||
readonly firstName?: string;
|
||||
readonly lastName?: string;
|
||||
readonly email?: string;
|
||||
readonly phone?: string;
|
||||
|
||||
constructor(props: CommandProps<UpdateUserCommand>) {
|
||||
super(props);
|
||||
this.firstName = props.firstName;
|
||||
this.lastName = props.lastName;
|
||||
this.email = props.email;
|
||||
this.phone = props.phone;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AggregateID, UniqueConstraintException } from '@mobicoop/ddd-library';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { UserRepositoryPort } from '../../ports/user.repository.port';
|
||||
import { UserEntity } from '../../../domain/user.entity';
|
||||
import { UpdateUserCommand } from './update-user.command';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
} from '@modules/user/core/domain/user.errors';
|
||||
|
||||
@CommandHandler(UpdateUserCommand)
|
||||
export class UpdateUserService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY)
|
||||
private readonly userRepository: UserRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateUserCommand): Promise<AggregateID> {
|
||||
try {
|
||||
const user: UserEntity = await this.userRepository.findOneById(
|
||||
command.id,
|
||||
);
|
||||
user.update({
|
||||
firstName: command.firstName,
|
||||
lastName: command.lastName,
|
||||
email: command.email,
|
||||
phone: command.phone,
|
||||
});
|
||||
await this.userRepository.update(user.id, user);
|
||||
return user.id;
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('email')
|
||||
) {
|
||||
throw new EmailAlreadyExistsException(error);
|
||||
}
|
||||
if (
|
||||
error instanceof UniqueConstraintException &&
|
||||
error.message.includes('phone')
|
||||
) {
|
||||
throw new PhoneAlreadyExistsException(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
|
||||
import { UserCreatedDomainEvent } from '../../domain/events/user-created.domain-event';
|
||||
import { UserCreatedIntegrationEvent } from '../events/user-created.integration-event';
|
||||
import { USER_CREATED_ROUTING_KEY } from '@modules/user/user.constants';
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenUserIsCreatedDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(USER_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
) {}
|
||||
|
||||
@OnEvent(UserCreatedDomainEvent.name, { async: true, promisify: true })
|
||||
async handle(event: UserCreatedDomainEvent): Promise<any> {
|
||||
const userCreatedIntegrationEvent = new UserCreatedIntegrationEvent({
|
||||
id: event.aggregateId,
|
||||
firstName: event.firstName,
|
||||
lastName: event.lastName,
|
||||
email: event.email,
|
||||
phone: event.phone,
|
||||
metadata: event.metadata,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
USER_CREATED_ROUTING_KEY,
|
||||
JSON.stringify(userCreatedIntegrationEvent),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
|
||||
import { UserDeletedDomainEvent } from '../../domain/events/user-deleted.domain-event';
|
||||
import { UserDeletedIntegrationEvent } from '../events/user-deleted.integration-event';
|
||||
import { USER_DELETED_ROUTING_KEY } from '@modules/user/user.constants';
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenUserIsDeletedDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(USER_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
) {}
|
||||
|
||||
@OnEvent(UserDeletedDomainEvent.name, { async: true, promisify: true })
|
||||
async handle(event: UserDeletedDomainEvent): Promise<any> {
|
||||
const userDeletedIntegrationEvent = new UserDeletedIntegrationEvent({
|
||||
id: event.aggregateId,
|
||||
metadata: event.metadata,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
USER_DELETED_ROUTING_KEY,
|
||||
JSON.stringify(userDeletedIntegrationEvent),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
|
||||
import { UserUpdatedDomainEvent } from '../../domain/events/user-updated.domain-event';
|
||||
import { UserUpdatedIntegrationEvent } from '../events/user-updated.integration-event';
|
||||
import { USER_UPDATED_ROUTING_KEY } from '@modules/user/user.constants';
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenUserIsUpdatedDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(USER_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
) {}
|
||||
|
||||
@OnEvent(UserUpdatedDomainEvent.name, { async: true, promisify: true })
|
||||
async handle(event: UserUpdatedDomainEvent): Promise<any> {
|
||||
const userUpdatedIntegrationEvent = new UserUpdatedIntegrationEvent({
|
||||
id: event.aggregateId,
|
||||
firstName: event.firstName,
|
||||
lastName: event.lastName,
|
||||
email: event.email,
|
||||
phone: event.phone,
|
||||
metadata: event.metadata,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
USER_UPDATED_ROUTING_KEY,
|
||||
JSON.stringify(userUpdatedIntegrationEvent),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserCreatedIntegrationEvent extends IntegrationEvent {
|
||||
readonly firstName: string;
|
||||
readonly lastName: string;
|
||||
readonly email: string;
|
||||
readonly phone: string;
|
||||
|
||||
constructor(props: IntegrationEventProps<UserCreatedIntegrationEvent>) {
|
||||
super(props);
|
||||
this.firstName = props.firstName;
|
||||
this.lastName = props.lastName;
|
||||
this.email = props.email;
|
||||
this.phone = props.phone;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserDeletedIntegrationEvent extends IntegrationEvent {
|
||||
constructor(props: IntegrationEventProps<UserDeletedIntegrationEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserUpdatedIntegrationEvent extends IntegrationEvent {
|
||||
readonly firstName: string;
|
||||
readonly lastName: string;
|
||||
readonly email: string;
|
||||
readonly phone: string;
|
||||
|
||||
constructor(props: IntegrationEventProps<UserUpdatedIntegrationEvent>) {
|
||||
super(props);
|
||||
this.firstName = props.firstName;
|
||||
this.lastName = props.lastName;
|
||||
this.email = props.email;
|
||||
this.phone = props.phone;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||
import { UserEntity } from '../../domain/user.entity';
|
||||
|
||||
export type UserRepositoryPort = RepositoryPort<UserEntity>;
|
|
@ -0,0 +1,17 @@
|
|||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { FindUserByIdQuery } from './find-user-by-id.query';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { UserRepositoryPort } from '../../ports/user.repository.port';
|
||||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
|
||||
@QueryHandler(FindUserByIdQuery)
|
||||
export class FindUserByIdQueryHandler implements IQueryHandler {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY)
|
||||
private readonly userRepository: UserRepositoryPort,
|
||||
) {}
|
||||
async execute(query: FindUserByIdQuery): Promise<UserEntity> {
|
||||
return await this.userRepository.findOneById(query.id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { QueryBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class FindUserByIdQuery extends QueryBase {
|
||||
readonly id: string;
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this.id = id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserCreatedDomainEvent extends DomainEvent {
|
||||
readonly firstName: string;
|
||||
readonly lastName: string;
|
||||
readonly email: string;
|
||||
readonly phone: string;
|
||||
|
||||
constructor(props: DomainEventProps<UserCreatedDomainEvent>) {
|
||||
super(props);
|
||||
this.firstName = props.firstName;
|
||||
this.lastName = props.lastName;
|
||||
this.email = props.email;
|
||||
this.phone = props.phone;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserDeletedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<UserDeletedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserUpdatedDomainEvent extends DomainEvent {
|
||||
readonly firstName: string;
|
||||
readonly lastName: string;
|
||||
readonly email: string;
|
||||
readonly phone: string;
|
||||
|
||||
constructor(props: DomainEventProps<UserUpdatedDomainEvent>) {
|
||||
super(props);
|
||||
this.firstName = props.firstName;
|
||||
this.lastName = props.lastName;
|
||||
this.email = props.email;
|
||||
this.phone = props.phone;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import { v4 } from 'uuid';
|
||||
import { CreateUserProps, UpdateUserProps, UserProps } from './user.types';
|
||||
import { UserCreatedDomainEvent } from './events/user-created.domain-event';
|
||||
import { UserUpdatedDomainEvent } from './events/user-updated.domain-event';
|
||||
import { UserDeletedDomainEvent } from './events/user-deleted.domain-event';
|
||||
|
||||
export class UserEntity extends AggregateRoot<UserProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
||||
static create = (create: CreateUserProps): UserEntity => {
|
||||
const id = v4();
|
||||
const props: UserProps = { ...create };
|
||||
const user = new UserEntity({ id, props });
|
||||
|
||||
user.addEvent(
|
||||
new UserCreatedDomainEvent({
|
||||
aggregateId: id,
|
||||
firstName: props.firstName,
|
||||
lastName: props.lastName,
|
||||
email: props.email,
|
||||
phone: props.phone,
|
||||
}),
|
||||
);
|
||||
return user;
|
||||
};
|
||||
|
||||
update(props: UpdateUserProps): void {
|
||||
this.props.firstName = props.firstName ?? this.props.firstName;
|
||||
this.props.lastName = props.lastName ?? this.props.lastName;
|
||||
this.props.email = props.email ?? this.props.email;
|
||||
this.props.phone = props.phone ?? this.props.phone;
|
||||
this.addEvent(
|
||||
new UserUpdatedDomainEvent({
|
||||
aggregateId: this._id,
|
||||
firstName: props.firstName,
|
||||
lastName: props.lastName,
|
||||
email: props.email,
|
||||
phone: props.phone,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.addEvent(
|
||||
new UserDeletedDomainEvent({
|
||||
aggregateId: this.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserAlreadyExistsException extends ExceptionBase {
|
||||
static readonly message = 'User already exists';
|
||||
|
||||
public readonly code = 'USER.ALREADY_EXISTS';
|
||||
|
||||
constructor(cause?: Error, metadata?: unknown) {
|
||||
super(UserAlreadyExistsException.message, cause, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
export class EmailAlreadyExistsException extends ExceptionBase {
|
||||
static readonly message = 'Email already exists';
|
||||
|
||||
public readonly code = 'EMAIL.ALREADY_EXISTS';
|
||||
|
||||
constructor(cause?: Error, metadata?: unknown) {
|
||||
super(EmailAlreadyExistsException.message, cause, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
export class PhoneAlreadyExistsException extends ExceptionBase {
|
||||
static readonly message = 'Phone already exists';
|
||||
|
||||
public readonly code = 'PHONE.ALREADY_EXISTS';
|
||||
|
||||
constructor(cause?: Error, metadata?: unknown) {
|
||||
super(PhoneAlreadyExistsException.message, cause, metadata);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// All properties that a User has
|
||||
export interface UserProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
// Properties that are needed for a User creation
|
||||
export interface CreateUserProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { IsInt, IsOptional } from 'class-validator';
|
||||
|
||||
export class FindAllUsersRequest {
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
page?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
perPage?: number;
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsPhoneNumber,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
export class UpdateUserRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
firstName?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
lastName?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
email?: string;
|
||||
|
||||
@IsPhoneNumber()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
phone?: string;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class User {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
|
||||
@AutoMap()
|
||||
firstName?: string;
|
||||
|
||||
@AutoMap()
|
||||
lastName?: string;
|
||||
|
||||
@AutoMap()
|
||||
email?: string;
|
||||
|
||||
@AutoMap()
|
||||
phone?: string;
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { CreateUserCommand } from '../../commands/create-user.command';
|
||||
import { CreateUserRequest } from '../dtos/create-user.request';
|
||||
import { User } from '../entities/user';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
|
||||
|
||||
@CommandHandler(CreateUserCommand)
|
||||
export class CreateUserUseCase {
|
||||
constructor(
|
||||
private readonly repository: UsersRepository,
|
||||
@Inject(MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: IPublishMessage,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
execute = async (command: CreateUserCommand): Promise<User> => {
|
||||
const entity = this.mapper.map(
|
||||
command.createUserRequest,
|
||||
CreateUserRequest,
|
||||
User,
|
||||
);
|
||||
|
||||
try {
|
||||
const user = await this.repository.create(entity);
|
||||
this.messagePublisher.publish('user.create', JSON.stringify(user));
|
||||
this.messagePublisher.publish(
|
||||
'logging.user.create.info',
|
||||
JSON.stringify(user),
|
||||
);
|
||||
return user;
|
||||
} catch (error) {
|
||||
let key = 'logging.user.create.crit';
|
||||
if (error.message.includes('Already exists')) {
|
||||
key = 'logging.user.create.warning';
|
||||
}
|
||||
this.messagePublisher.publish(
|
||||
key,
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { DeleteUserCommand } from '../../commands/delete-user.command';
|
||||
import { User } from '../entities/user';
|
||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
|
||||
@CommandHandler(DeleteUserCommand)
|
||||
export class DeleteUserUseCase {
|
||||
constructor(
|
||||
private readonly repository: UsersRepository,
|
||||
@Inject(MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: IPublishMessage,
|
||||
) {}
|
||||
|
||||
execute = async (command: DeleteUserCommand): Promise<User> => {
|
||||
try {
|
||||
const user = await this.repository.delete(command.uuid);
|
||||
this.messagePublisher.publish(
|
||||
'user.delete',
|
||||
JSON.stringify({ uuid: user.uuid }),
|
||||
);
|
||||
this.messagePublisher.publish(
|
||||
'logging.user.delete.info',
|
||||
JSON.stringify({ uuid: user.uuid }),
|
||||
);
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.messagePublisher.publish(
|
||||
'logging.user.delete.crit',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import { QueryHandler } from '@nestjs/cqrs';
|
||||
import { ICollection } from 'src/modules/database/interfaces/collection.interface';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { FindAllUsersQuery } from '../../queries/find-all-users.query';
|
||||
import { User } from '../entities/user';
|
||||
|
||||
@QueryHandler(FindAllUsersQuery)
|
||||
export class FindAllUsersUseCase {
|
||||
constructor(private readonly repository: UsersRepository) {}
|
||||
|
||||
execute = async (
|
||||
findAllUsersQuery: FindAllUsersQuery,
|
||||
): Promise<ICollection<User>> =>
|
||||
this.repository.findAll(findAllUsersQuery.page, findAllUsersQuery.perPage);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { Inject, NotFoundException } from '@nestjs/common';
|
||||
import { QueryHandler } from '@nestjs/cqrs';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
|
||||
import { User } from '../entities/user';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
|
||||
|
||||
@QueryHandler(FindUserByUuidQuery)
|
||||
export class FindUserByUuidUseCase {
|
||||
constructor(
|
||||
private readonly repository: UsersRepository,
|
||||
@Inject(MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: IPublishMessage,
|
||||
) {}
|
||||
|
||||
execute = async (findUserByUuid: FindUserByUuidQuery): Promise<User> => {
|
||||
try {
|
||||
const user = await this.repository.findOneByUuid(findUserByUuid.uuid);
|
||||
if (!user) throw new NotFoundException();
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.messagePublisher.publish(
|
||||
'logging.user.read.warning',
|
||||
JSON.stringify({
|
||||
query: findUserByUuid,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { CommandHandler } from '@nestjs/cqrs';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { UpdateUserCommand } from '../../commands/update-user.command';
|
||||
import { UpdateUserRequest } from '../dtos/update-user.request';
|
||||
import { User } from '../entities/user';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
|
||||
|
||||
@CommandHandler(UpdateUserCommand)
|
||||
export class UpdateUserUseCase {
|
||||
constructor(
|
||||
private readonly repository: UsersRepository,
|
||||
@Inject(MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: IPublishMessage,
|
||||
@InjectMapper() private readonly mapper: Mapper,
|
||||
) {}
|
||||
|
||||
execute = async (command: UpdateUserCommand): Promise<User> => {
|
||||
const entity = this.mapper.map(
|
||||
command.updateUserRequest,
|
||||
UpdateUserRequest,
|
||||
User,
|
||||
);
|
||||
|
||||
try {
|
||||
const user = await this.repository.update(
|
||||
command.updateUserRequest.uuid,
|
||||
entity,
|
||||
);
|
||||
this.messagePublisher.publish(
|
||||
'user.update',
|
||||
JSON.stringify(command.updateUserRequest),
|
||||
);
|
||||
this.messagePublisher.publish(
|
||||
'logging.user.update.info',
|
||||
JSON.stringify(command.updateUserRequest),
|
||||
);
|
||||
return user;
|
||||
} catch (error) {
|
||||
this.messagePublisher.publish(
|
||||
'logging.user.update.crit',
|
||||
JSON.stringify({
|
||||
command,
|
||||
error,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
LoggerBase,
|
||||
MessagePublisherPort,
|
||||
PrismaRepositoryBase,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { UserEntity } from '../core/domain/user.entity';
|
||||
import { UserRepositoryPort } from '../core/application/ports/user.repository.port';
|
||||
import { UserMapper } from '../user.mapper';
|
||||
import { USER_MESSAGE_PUBLISHER } from '../user.di-tokens';
|
||||
|
||||
export type UserBaseModel = {
|
||||
uuid: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
};
|
||||
|
||||
export type UserReadModel = UserBaseModel & {
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
};
|
||||
|
||||
export type UserWriteModel = UserBaseModel;
|
||||
|
||||
/**
|
||||
* Repository is used for retrieving/saving domain entities
|
||||
* */
|
||||
@Injectable()
|
||||
export class UserRepository
|
||||
extends PrismaRepositoryBase<UserEntity, UserReadModel, UserWriteModel>
|
||||
implements UserRepositoryPort
|
||||
{
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
mapper: UserMapper,
|
||||
eventEmitter: EventEmitter2,
|
||||
@Inject(USER_MESSAGE_PUBLISHER)
|
||||
protected readonly messagePublisher: MessagePublisherPort,
|
||||
) {
|
||||
super(
|
||||
prisma.user,
|
||||
prisma,
|
||||
mapper,
|
||||
eventEmitter,
|
||||
new LoggerBase({
|
||||
logger: new Logger(UserRepository.name),
|
||||
domain: 'user',
|
||||
messagePublisher,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
||||
import { UserResponseDto } from './user.response.dto';
|
||||
|
||||
export class UserPaginatedResponseDto extends PaginatedResponseDto<UserResponseDto> {
|
||||
readonly data: readonly UserResponseDto[];
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class UserResponseDto extends ResponseBase {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { CreateUserRequestDto } from './dtos/create-user.request.dto';
|
||||
import { CreateUserCommand } from '@modules/user/core/application/commands/create-user/create-user.command';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
UserAlreadyExistsException,
|
||||
} from '@modules/user/core/domain/user.errors';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class CreateUserGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('UserService', 'Create')
|
||||
async create(data: CreateUserRequestDto): Promise<IdResponse> {
|
||||
try {
|
||||
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||
new CreateUserCommand(data),
|
||||
);
|
||||
return new IdResponse(aggregateID);
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error instanceof UserAlreadyExistsException ||
|
||||
error instanceof EmailAlreadyExistsException ||
|
||||
error instanceof PhoneAlreadyExistsException
|
||||
)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { DeleteUserRequestDto } from './dtos/delete-user.request.dto';
|
||||
import { DeleteUserCommand } from '@modules/user/core/application/commands/delete-user/delete-user.command';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class DeleteUserGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('UserService', 'Delete')
|
||||
async delete(data: DeleteUserRequestDto): Promise<void> {
|
||||
try {
|
||||
await this.commandBus.execute(new DeleteUserCommand(data));
|
||||
} catch (error: any) {
|
||||
if (error instanceof NotFoundException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.NOT_FOUND,
|
||||
message: error.message,
|
||||
});
|
||||
if (error instanceof DatabaseErrorException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.INTERNAL,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
|
||||
export class CreateUserRequestDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
firstName: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lastName: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
|
||||
@IsPhoneNumber()
|
||||
@IsOptional()
|
||||
phone?: string;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class FindUserByUuidRequest {
|
||||
export class DeleteUserRequestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
uuid: string;
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class FindUserByIdRequestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id: string;
|
||||
}
|
|
@ -1,29 +1,22 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
|
||||
|
||||
export class CreateUserRequest {
|
||||
export class UpdateUserRequestDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
uuid?: string;
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
firstName?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
lastName?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
email?: string;
|
||||
|
||||
@IsPhoneNumber()
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
phone?: string;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { UserMapper } from '@modules/user/user.mapper';
|
||||
import { FindUserByIdRequestDto } from './dtos/find-user-by-id.request.dto';
|
||||
import { UserResponseDto } from '../dtos/user.response.dto';
|
||||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
import { FindUserByIdQuery } from '@modules/user/core/application/queries/find-user-by-id/find-user-by-id.query';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class FindUserByIdGrpcController {
|
||||
constructor(
|
||||
protected readonly mapper: UserMapper,
|
||||
private readonly queryBus: QueryBus,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('UserService', 'FindOneById')
|
||||
async findOnebyId(data: FindUserByIdRequestDto): Promise<UserResponseDto> {
|
||||
try {
|
||||
const user: UserEntity = await this.queryBus.execute(
|
||||
new FindUserByIdQuery(data.id),
|
||||
);
|
||||
return this.mapper.toResponse(user);
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundException) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.NOT_FOUND,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
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 { UpdateUserRequestDto } from './dtos/update-user.request.dto';
|
||||
import { UpdateUserCommand } from '@modules/user/core/application/commands/update-user/update-user.command';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
UserAlreadyExistsException,
|
||||
} from '@modules/user/core/domain/user.errors';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: true,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class UpdateUserGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod('UserService', 'Update')
|
||||
async updateUser(data: UpdateUserRequestDto): Promise<IdResponse> {
|
||||
try {
|
||||
const aggregateID: AggregateID = await this.commandBus.execute(
|
||||
new UpdateUserCommand({
|
||||
id: data.id,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
}),
|
||||
);
|
||||
return new IdResponse(aggregateID);
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error instanceof UserAlreadyExistsException ||
|
||||
error instanceof EmailAlreadyExistsException ||
|
||||
error instanceof PhoneAlreadyExistsException
|
||||
)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.ALREADY_EXISTS,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,20 +2,20 @@ syntax = "proto3";
|
|||
|
||||
package user;
|
||||
|
||||
service UsersService {
|
||||
rpc FindOneByUuid(UserByUuid) returns (User);
|
||||
service UserService {
|
||||
rpc FindOneById(UserById) returns (User);
|
||||
rpc FindAll(UserFilter) returns (Users);
|
||||
rpc Create(User) returns (User);
|
||||
rpc Update(User) returns (User);
|
||||
rpc Delete(UserByUuid) returns (Empty);
|
||||
rpc Create(User) returns (UserById);
|
||||
rpc Update(User) returns (UserById);
|
||||
rpc Delete(UserById) returns (Empty);
|
||||
}
|
||||
|
||||
message UserByUuid {
|
||||
string uuid = 1;
|
||||
message UserById {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message User {
|
||||
string uuid = 1;
|
||||
string id = 1;
|
||||
string firstName = 2;
|
||||
string lastName = 3;
|
||||
string email = 4;
|
|
@ -1,29 +0,0 @@
|
|||
import { createMap, forMember, ignore, Mapper } from '@automapper/core';
|
||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserPresenter } from '../adapters/primaries/user.presenter';
|
||||
import { CreateUserRequest } from '../domain/dtos/create-user.request';
|
||||
import { UpdateUserRequest } from '../domain/dtos/update-user.request';
|
||||
import { User } from '../domain/entities/user';
|
||||
|
||||
@Injectable()
|
||||
export class UserProfile extends AutomapperProfile {
|
||||
constructor(@InjectMapper() mapper: Mapper) {
|
||||
super(mapper);
|
||||
}
|
||||
|
||||
override get profile() {
|
||||
return (mapper) => {
|
||||
createMap(mapper, User, UserPresenter);
|
||||
|
||||
createMap(mapper, CreateUserRequest, User);
|
||||
|
||||
createMap(
|
||||
mapper,
|
||||
UpdateUserRequest,
|
||||
User,
|
||||
forMember((dest) => dest.uuid, ignore()),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { FindAllUsersRequest } from '../domain/dtos/find-all-users.request';
|
||||
|
||||
export class FindAllUsersQuery {
|
||||
page: number;
|
||||
perPage: number;
|
||||
|
||||
constructor(findAllUsersRequest?: FindAllUsersRequest) {
|
||||
this.page = findAllUsersRequest?.page ?? 1;
|
||||
this.perPage = findAllUsersRequest?.perPage ?? 10;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { FindUserByUuidRequest } from '../domain/dtos/find-user-by-uuid.request';
|
||||
|
||||
export class FindUserByUuidQuery {
|
||||
readonly uuid: string;
|
||||
|
||||
constructor(findUserByUuidRequest: FindUserByUuidRequest) {
|
||||
this.uuid = findUserByUuidRequest.uuid;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
import { CreateUserProps } from '@modules/user/core/domain/user.types';
|
||||
import { PrismaService } from '@modules/user/infrastructure/prisma.service';
|
||||
import { UserRepository } from '@modules/user/infrastructure/user.repository';
|
||||
import {
|
||||
USER_MESSAGE_PUBLISHER,
|
||||
USER_REPOSITORY,
|
||||
} from '@modules/user/user.di-tokens';
|
||||
import { UserMapper } from '@modules/user/user.mapper';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
describe('User Repository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let userRepository: UserRepository;
|
||||
|
||||
const executeInsertCommand = async (table: string, object: any) => {
|
||||
const command = `INSERT INTO "${table}" ("${Object.keys(object).join(
|
||||
'","',
|
||||
)}") VALUES ('${Object.values(object).join("','")}')`;
|
||||
await prismaService.$executeRawUnsafe(command);
|
||||
};
|
||||
const getSeed = (index: number, uuid: string): string => {
|
||||
return `${uuid.slice(0, -2)}${index.toString(16).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const baseUuid = {
|
||||
uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00',
|
||||
};
|
||||
|
||||
const createUsers = async (nbToCreate = 10) => {
|
||||
for (let i = 0; i < nbToCreate; i++) {
|
||||
const userToCreate = {
|
||||
uuid: getSeed(i, baseUuid.uuid),
|
||||
firstName: `John${i}`,
|
||||
lastName: `Doe${i}`,
|
||||
email: `john.doe${i}@email.com`,
|
||||
phone: `+33611223344${i}`,
|
||||
createdAt: '2023-07-24 13:07:05.000',
|
||||
updatedAt: '2023-07-24 13:07:05.000',
|
||||
};
|
||||
userToCreate.uuid = getSeed(i, baseUuid.uuid);
|
||||
await executeInsertCommand('user', userToCreate);
|
||||
}
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockLogger = {
|
||||
log: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
EventEmitterModule.forRoot(),
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
],
|
||||
providers: [
|
||||
PrismaService,
|
||||
UserMapper,
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useClass: UserRepository,
|
||||
},
|
||||
{
|
||||
provide: USER_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
],
|
||||
})
|
||||
// disable logging
|
||||
.setLogger(mockLogger)
|
||||
.compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
userRepository = module.get<UserRepository>(USER_REPOSITORY);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prismaService.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prismaService.user.deleteMany();
|
||||
});
|
||||
|
||||
describe('findOneById', () => {
|
||||
it('should return a user', async () => {
|
||||
await createUsers(1);
|
||||
const result = await userRepository.findOneById(baseUuid.uuid);
|
||||
expect(result.id).toBe(baseUuid.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a user', async () => {
|
||||
const beforeCount = await prismaService.user.count();
|
||||
|
||||
const createUserProps: CreateUserProps = {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
email: 'jane.doe@email.com',
|
||||
phone: '+33622334455',
|
||||
};
|
||||
|
||||
const userToCreate: UserEntity = UserEntity.create(createUserProps);
|
||||
await userRepository.insert(userToCreate);
|
||||
|
||||
const afterCount = await prismaService.user.count();
|
||||
|
||||
expect(afterCount - beforeCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,174 +0,0 @@
|
|||
import { TestingModule, Test } from '@nestjs/testing';
|
||||
import { DatabaseModule } from '../../../database/database.module';
|
||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma-service';
|
||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { User } from '../../domain/entities/user';
|
||||
|
||||
describe('UsersRepository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let usersRepository: UsersRepository;
|
||||
|
||||
const createUsers = async (nbToCreate = 10) => {
|
||||
for (let i = 0; i < nbToCreate; i++) {
|
||||
await prismaService.user.create({
|
||||
data: {
|
||||
firstName: `firstName-${i}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [DatabaseModule],
|
||||
providers: [UsersRepository, PrismaService],
|
||||
}).compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
usersRepository = module.get<UsersRepository>(UsersRepository);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prismaService.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prismaService.user.deleteMany();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return an empty data array', async () => {
|
||||
const res = await usersRepository.findAll();
|
||||
expect(res).toEqual({
|
||||
data: [],
|
||||
total: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a data array with 8 users', async () => {
|
||||
await createUsers(8);
|
||||
const users = await usersRepository.findAll();
|
||||
expect(users.data.length).toBe(8);
|
||||
expect(users.total).toBe(8);
|
||||
});
|
||||
|
||||
it('should return a data array limited to 10 users', async () => {
|
||||
await createUsers(20);
|
||||
const users = await usersRepository.findAll();
|
||||
expect(users.data.length).toBe(10);
|
||||
expect(users.total).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOneByUuid', () => {
|
||||
it('should return a user', async () => {
|
||||
const userToFind = await prismaService.user.create({
|
||||
data: {
|
||||
firstName: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await usersRepository.findOneByUuid(userToFind.uuid);
|
||||
expect(user.uuid).toBe(userToFind.uuid);
|
||||
});
|
||||
|
||||
it('should return null', async () => {
|
||||
const user = await usersRepository.findOneByUuid(
|
||||
'544572be-11fb-4244-8235-587221fc9104',
|
||||
);
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return a user according to its email', async () => {
|
||||
const userToFind = await prismaService.user.create({
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
},
|
||||
});
|
||||
|
||||
const user = await usersRepository.findOne({
|
||||
email: 'test@test.com',
|
||||
});
|
||||
|
||||
expect(user.uuid).toBe(userToFind.uuid);
|
||||
});
|
||||
|
||||
it('should return null with unknown email', async () => {
|
||||
const user = await usersRepository.findOne({
|
||||
email: 'wrong@email.com',
|
||||
});
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a user', async () => {
|
||||
const beforeCount = await prismaService.user.count();
|
||||
|
||||
const userToCreate: User = new User();
|
||||
userToCreate.firstName = 'test';
|
||||
const user = await usersRepository.create(userToCreate);
|
||||
|
||||
const afterCount = await prismaService.user.count();
|
||||
|
||||
expect(afterCount - beforeCount).toBe(1);
|
||||
expect(user.uuid).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update user firstName', async () => {
|
||||
const userToUpdate = await prismaService.user.create({
|
||||
data: {
|
||||
firstName: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
const toUpdate: User = new User();
|
||||
toUpdate.firstName = 'updated';
|
||||
const updateduser = await usersRepository.update(
|
||||
userToUpdate.uuid,
|
||||
toUpdate,
|
||||
);
|
||||
|
||||
expect(updateduser.uuid).toBe(userToUpdate.uuid);
|
||||
expect(updateduser.firstName).toBe('updated');
|
||||
});
|
||||
|
||||
it('should throw DatabaseException', async () => {
|
||||
const toUpdate: User = new User();
|
||||
toUpdate.firstName = 'updated';
|
||||
|
||||
await expect(
|
||||
usersRepository.update(
|
||||
'544572be-11fb-4244-8235-587221fc9104',
|
||||
toUpdate,
|
||||
),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a user', async () => {
|
||||
const userToRemove = await prismaService.user.create({
|
||||
data: {
|
||||
firstName: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
await usersRepository.delete(userToRemove.uuid);
|
||||
|
||||
const count = await prismaService.user.count();
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should throw DatabaseException', async () => {
|
||||
await expect(
|
||||
usersRepository.delete('544572be-11fb-4244-8235-587221fc9104'),
|
||||
).rejects.toBeInstanceOf(DatabaseException);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AggregateID, UniqueConstraintException } from '@mobicoop/ddd-library';
|
||||
import { ConflictException } from '@mobicoop/ddd-library';
|
||||
import { CreateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/create-user.request.dto';
|
||||
import { CreateUserService } from '@modules/user/core/application/commands/create-user/create-user.service';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
import { CreateUserCommand } from '@modules/user/core/application/commands/create-user/create-user.command';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
UserAlreadyExistsException,
|
||||
} from '@modules/user/core/domain/user.errors';
|
||||
|
||||
const createUserRequest: CreateUserRequestDto = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
};
|
||||
|
||||
const mockUserRepository = {
|
||||
insert: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new ConflictException('User already exists');
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UniqueConstraintException('email already exists');
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UniqueConstraintException('phone already exists');
|
||||
}),
|
||||
};
|
||||
|
||||
describe('create-user.service', () => {
|
||||
let createUserService: CreateUserService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
CreateUserService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
createUserService = module.get<CreateUserService>(CreateUserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(createUserService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
const createUserCommand = new CreateUserCommand(createUserRequest);
|
||||
it('should create a new user', async () => {
|
||||
UserEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
const result: AggregateID = await createUserService.execute(
|
||||
createUserCommand,
|
||||
);
|
||||
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
||||
});
|
||||
it('should throw an error if something bad happens', async () => {
|
||||
UserEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
await expect(
|
||||
createUserService.execute(createUserCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
it('should throw an exception if User already exists', async () => {
|
||||
UserEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
await expect(
|
||||
createUserService.execute(createUserCommand),
|
||||
).rejects.toBeInstanceOf(UserAlreadyExistsException);
|
||||
});
|
||||
it('should throw an exception if Email already exists', async () => {
|
||||
UserEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
await expect(
|
||||
createUserService.execute(createUserCommand),
|
||||
).rejects.toBeInstanceOf(EmailAlreadyExistsException);
|
||||
});
|
||||
it('should throw an exception if Phone already exists', async () => {
|
||||
UserEntity.create = jest.fn().mockReturnValue({
|
||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||
});
|
||||
await expect(
|
||||
createUserService.execute(createUserCommand),
|
||||
).rejects.toBeInstanceOf(PhoneAlreadyExistsException);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { DeleteUserCommand } from '@modules/user/core/application/commands/delete-user/delete-user.command';
|
||||
import { DeleteUserService } from '@modules/user/core/application/commands/delete-user/delete-user.service';
|
||||
import { DeleteUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/delete-user.request.dto';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const deleteUserRequest: DeleteUserRequestDto = {
|
||||
id: '165192d4-398a-4469-a16b-98c02cc6f531',
|
||||
};
|
||||
|
||||
const mockUserEntity = {
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUserRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => mockUserEntity),
|
||||
delete: jest.fn().mockImplementationOnce(() => true),
|
||||
};
|
||||
|
||||
describe('Delete User Service', () => {
|
||||
let deleteUserService: DeleteUserService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
DeleteUserService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteUserService = module.get<DeleteUserService>(DeleteUserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteUserService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
const deleteUserCommand = new DeleteUserCommand(deleteUserRequest);
|
||||
it('should delete a user', async () => {
|
||||
const result: boolean = await deleteUserService.execute(
|
||||
deleteUserCommand,
|
||||
);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
import { FindUserByIdQueryHandler } from '@modules/user/core/application/queries/find-user-by-id/find-user-by-id.query-handler';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { FindUserByIdQuery } from '@modules/user/core/application/queries/find-user-by-id/find-user-by-id.query';
|
||||
|
||||
const now = new Date('2023-06-21 06:00:00');
|
||||
const user: UserEntity = new UserEntity({
|
||||
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||
props: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const mockUserRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => user),
|
||||
};
|
||||
|
||||
describe('find-user-by-id.query-handler', () => {
|
||||
let findUserByIdQueryHandler: FindUserByIdQueryHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
FindUserByIdQueryHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findUserByIdQueryHandler = module.get<FindUserByIdQueryHandler>(
|
||||
FindUserByIdQueryHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findUserByIdQueryHandler).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should return a user', async () => {
|
||||
const findUserbyIdQuery = new FindUserByIdQuery(
|
||||
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
|
||||
);
|
||||
const user: UserEntity = await findUserByIdQueryHandler.execute(
|
||||
findUserbyIdQuery,
|
||||
);
|
||||
expect(user.getProps().lastName).toBe('Doe');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PublishMessageWhenUserIsCreatedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-created.domain-event-handler';
|
||||
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
|
||||
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-event';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Publish message when user is created domain event handler', () => {
|
||||
let publishMessageWhenUserIsCreatedDomainEventHandler: PublishMessageWhenUserIsCreatedDomainEventHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USER_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
PublishMessageWhenUserIsCreatedDomainEventHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
publishMessageWhenUserIsCreatedDomainEventHandler =
|
||||
module.get<PublishMessageWhenUserIsCreatedDomainEventHandler>(
|
||||
PublishMessageWhenUserIsCreatedDomainEventHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish a message', () => {
|
||||
jest.spyOn(mockMessagePublisher, 'publish');
|
||||
const userCreatedDomainEvent: UserCreatedDomainEvent = {
|
||||
id: 'some-domain-event-id',
|
||||
aggregateId: 'some-aggregate-id',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
metadata: {
|
||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||
correlationId: 'some-correlation-id',
|
||||
},
|
||||
};
|
||||
publishMessageWhenUserIsCreatedDomainEventHandler.handle(
|
||||
userCreatedDomainEvent,
|
||||
);
|
||||
expect(publishMessageWhenUserIsCreatedDomainEventHandler).toBeDefined();
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
|
||||
'user.created',
|
||||
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"firstName":"John","lastName":"Doe","email":"john.doe@email.com","phone":"+33611223344"}',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
|
||||
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-event';
|
||||
import { PublishMessageWhenUserIsDeletedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Publish message when user is deleted domain event handler', () => {
|
||||
let publishMessageWhenUserIsDeletedDomainEventHandler: PublishMessageWhenUserIsDeletedDomainEventHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USER_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
PublishMessageWhenUserIsDeletedDomainEventHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
publishMessageWhenUserIsDeletedDomainEventHandler =
|
||||
module.get<PublishMessageWhenUserIsDeletedDomainEventHandler>(
|
||||
PublishMessageWhenUserIsDeletedDomainEventHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish a message', () => {
|
||||
jest.spyOn(mockMessagePublisher, 'publish');
|
||||
const userDeletedDomainEvent: UserCreatedDomainEvent = {
|
||||
id: 'some-domain-event-id',
|
||||
aggregateId: 'some-aggregate-id',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
metadata: {
|
||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||
correlationId: 'some-correlation-id',
|
||||
},
|
||||
};
|
||||
publishMessageWhenUserIsDeletedDomainEventHandler.handle(
|
||||
userDeletedDomainEvent,
|
||||
);
|
||||
expect(publishMessageWhenUserIsDeletedDomainEventHandler).toBeDefined();
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
|
||||
'user.deleted',
|
||||
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000}}',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens';
|
||||
import { PublishMessageWhenUserIsUpdatedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler';
|
||||
import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-event';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Publish message when user is updated domain event handler', () => {
|
||||
let publishMessageWhenUserIsUpdatedDomainEventHandler: PublishMessageWhenUserIsUpdatedDomainEventHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USER_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
PublishMessageWhenUserIsUpdatedDomainEventHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
publishMessageWhenUserIsUpdatedDomainEventHandler =
|
||||
module.get<PublishMessageWhenUserIsUpdatedDomainEventHandler>(
|
||||
PublishMessageWhenUserIsUpdatedDomainEventHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish a message', () => {
|
||||
jest.spyOn(mockMessagePublisher, 'publish');
|
||||
const userUpdatedDomainEvent: UserUpdatedDomainEvent = {
|
||||
id: 'some-domain-event-id',
|
||||
aggregateId: 'some-aggregate-id',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
email: 'jane.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
metadata: {
|
||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
||||
correlationId: 'some-correlation-id',
|
||||
},
|
||||
};
|
||||
publishMessageWhenUserIsUpdatedDomainEventHandler.handle(
|
||||
userUpdatedDomainEvent,
|
||||
);
|
||||
expect(publishMessageWhenUserIsUpdatedDomainEventHandler).toBeDefined();
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
|
||||
'user.updated',
|
||||
'{"id":"some-aggregate-id","metadata":{"correlationId":"some-correlation-id","timestamp":1687928400000},"firstName":"Jane","lastName":"Doe","email":"jane.doe@email.com","phone":"+33611223344"}',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,126 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import {
|
||||
AggregateID,
|
||||
NotFoundException,
|
||||
UniqueConstraintException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
|
||||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
} from '@modules/user/core/domain/user.errors';
|
||||
import { UpdateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/update-user.request.dto';
|
||||
import { UpdateUserService } from '@modules/user/core/application/commands/update-user/update-user.service';
|
||||
import { UpdateUserCommand } from '@modules/user/core/application/commands/update-user/update-user.command';
|
||||
|
||||
const updateFirstNameUserRequest: UpdateUserRequestDto = {
|
||||
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||
firstName: 'Johnny',
|
||||
};
|
||||
|
||||
const updateEmailUserRequest: UpdateUserRequestDto = {
|
||||
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||
email: 'john.doe@already.exists.email.com',
|
||||
};
|
||||
|
||||
const updatePhoneUserRequest: UpdateUserRequestDto = {
|
||||
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||
phone: '+33611223344',
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const userToUpdate: UserEntity = new UserEntity({
|
||||
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
props: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
},
|
||||
});
|
||||
|
||||
const mockUserRepository = {
|
||||
findOneById: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException('Record not found');
|
||||
})
|
||||
.mockImplementation(() => userToUpdate),
|
||||
update: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => 'c97b1783-76cf-4840-b298-b90b13c58894')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UniqueConstraintException('email already exists');
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UniqueConstraintException('phone already exists');
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('update-user.service', () => {
|
||||
let updateUserService: UpdateUserService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: USER_REPOSITORY,
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
UpdateUserService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateUserService = module.get<UpdateUserService>(UpdateUserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateUserService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should throw an exception if user is not found', async () => {
|
||||
const updateUserCommand = new UpdateUserCommand(
|
||||
updateFirstNameUserRequest,
|
||||
);
|
||||
await expect(
|
||||
updateUserService.execute(updateUserCommand),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
it('should update a user firstName', async () => {
|
||||
jest.spyOn(userToUpdate, 'update');
|
||||
const updateUserCommand = new UpdateUserCommand(
|
||||
updateFirstNameUserRequest,
|
||||
);
|
||||
const result: AggregateID = await updateUserService.execute(
|
||||
updateUserCommand,
|
||||
);
|
||||
expect(result).toBe('c97b1783-76cf-4840-b298-b90b13c58894');
|
||||
expect(userToUpdate.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should throw an exception if Email already exists', async () => {
|
||||
const updateUserCommand = new UpdateUserCommand(updateEmailUserRequest);
|
||||
await expect(
|
||||
updateUserService.execute(updateUserCommand),
|
||||
).rejects.toBeInstanceOf(EmailAlreadyExistsException);
|
||||
});
|
||||
it('should throw an exception if Phone already exists', async () => {
|
||||
const updateUserCommand = new UpdateUserCommand(updatePhoneUserRequest);
|
||||
await expect(
|
||||
updateUserService.execute(updateUserCommand),
|
||||
).rejects.toBeInstanceOf(PhoneAlreadyExistsException);
|
||||
});
|
||||
it('should throw an error if something bad happens', async () => {
|
||||
const updateUserCommand = new UpdateUserCommand(updatePhoneUserRequest);
|
||||
await expect(
|
||||
updateUserService.execute(updateUserCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-event';
|
||||
import { UserDeletedDomainEvent } from '@modules/user/core/domain/events/user-deleted.domain-event';
|
||||
import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-event';
|
||||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
import {
|
||||
CreateUserProps,
|
||||
UpdateUserProps,
|
||||
} from '@modules/user/core/domain/user.types';
|
||||
|
||||
const createUserProps: CreateUserProps = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
};
|
||||
|
||||
const updateUserProps: UpdateUserProps = {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Dane',
|
||||
email: 'jane.dane@email.com',
|
||||
};
|
||||
|
||||
describe('User entity create', () => {
|
||||
it('should create a new user entity', async () => {
|
||||
const userEntity: UserEntity = UserEntity.create(createUserProps);
|
||||
expect(userEntity.id.length).toBe(36);
|
||||
expect(userEntity.getProps().email).toBe('john.doe@email.com');
|
||||
expect(userEntity.domainEvents.length).toBe(1);
|
||||
expect(userEntity.domainEvents[0]).toBeInstanceOf(UserCreatedDomainEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User entity update', () => {
|
||||
it('should update a user entity', async () => {
|
||||
const userEntity: UserEntity = UserEntity.create(createUserProps);
|
||||
userEntity.update(updateUserProps);
|
||||
expect(userEntity.getProps().firstName).toBe('Jane');
|
||||
expect(userEntity.getProps().lastName).toBe('Dane');
|
||||
expect(userEntity.getProps().email).toBe('jane.dane@email.com');
|
||||
// 2 events because UserEntity.create sends a UserCreatedDomainEvent
|
||||
expect(userEntity.domainEvents.length).toBe(2);
|
||||
expect(userEntity.domainEvents[1]).toBeInstanceOf(UserUpdatedDomainEvent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User entity delete', () => {
|
||||
it('should delete a user entity', async () => {
|
||||
const userEntity: UserEntity = UserEntity.create(createUserProps);
|
||||
userEntity.delete();
|
||||
// 2 events because UserEntity.create sends a UserCreatedDomainEvent
|
||||
expect(userEntity.domainEvents.length).toBe(2);
|
||||
expect(userEntity.domainEvents[1]).toBeInstanceOf(UserDeletedDomainEvent);
|
||||
});
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { CreateUserCommand } from '../../commands/create-user.command';
|
||||
import { CreateUserRequest } from '../../domain/dtos/create-user.request';
|
||||
import { User } from '../../domain/entities/user';
|
||||
import { CreateUserUseCase } from '../../domain/usecases/create-user.usecase';
|
||||
import { UserProfile } from '../../mappers/user.profile';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
|
||||
const newUserRequest: CreateUserRequest = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '0601020304',
|
||||
};
|
||||
const newUserCommand: CreateUserCommand = new CreateUserCommand(newUserRequest);
|
||||
|
||||
const mockUsersRepository = {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
...newUserRequest,
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
});
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Already exists');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('CreateUserUseCase', () => {
|
||||
let createUserUseCase: CreateUserUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: UsersRepository,
|
||||
useValue: mockUsersRepository,
|
||||
},
|
||||
CreateUserUseCase,
|
||||
UserProfile,
|
||||
{
|
||||
provide: MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
createUserUseCase = module.get<CreateUserUseCase>(CreateUserUseCase);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(createUserUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should create and return a new user', async () => {
|
||||
const newUser: User = await createUserUseCase.execute(newUserCommand);
|
||||
|
||||
expect(newUser.lastName).toBe(newUserRequest.lastName);
|
||||
expect(newUser.uuid).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw an error if user already exists', async () => {
|
||||
await expect(
|
||||
createUserUseCase.execute(newUserCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,93 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { DeleteUserCommand } from '../../commands/delete-user.command';
|
||||
import { DeleteUserUseCase } from '../../domain/usecases/delete-user.usecase';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
|
||||
const mockUsers = [
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '0601020304',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
email: 'jane.doe@email.com',
|
||||
phone: '0602030405',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a93',
|
||||
firstName: 'Jimmy',
|
||||
lastName: 'Doe',
|
||||
email: 'jimmy.doe@email.com',
|
||||
phone: '0603040506',
|
||||
},
|
||||
];
|
||||
|
||||
const mockUsersRepository = {
|
||||
delete: jest
|
||||
.fn()
|
||||
.mockImplementationOnce((uuid: string) => {
|
||||
let savedUser = {};
|
||||
mockUsers.forEach((user, index) => {
|
||||
if (user.uuid === uuid) {
|
||||
savedUser = { ...user };
|
||||
mockUsers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
return savedUser;
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Error');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('DeleteUserUseCase', () => {
|
||||
let deleteUserUseCase: DeleteUserUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: UsersRepository,
|
||||
useValue: mockUsersRepository,
|
||||
},
|
||||
DeleteUserUseCase,
|
||||
{
|
||||
provide: MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteUserUseCase = module.get<DeleteUserUseCase>(DeleteUserUseCase);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteUserUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should delete a user', async () => {
|
||||
const savedUuid = mockUsers[0].uuid;
|
||||
const deleteUserCommand = new DeleteUserCommand(savedUuid);
|
||||
await deleteUserUseCase.execute(deleteUserCommand);
|
||||
|
||||
const deletedUser = mockUsers.find((user) => user.uuid === savedUuid);
|
||||
expect(deletedUser).toBeUndefined();
|
||||
});
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
await expect(
|
||||
deleteUserUseCase.execute(new DeleteUserCommand('wrong uuid')),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { FindAllUsersRequest } from '../../domain/dtos/find-all-users.request';
|
||||
import { FindAllUsersUseCase } from '../../domain/usecases/find-all-users.usecase';
|
||||
import { FindAllUsersQuery } from '../../queries/find-all-users.query';
|
||||
|
||||
const findAllUsersRequest: FindAllUsersRequest = new FindAllUsersRequest();
|
||||
findAllUsersRequest.page = 1;
|
||||
findAllUsersRequest.perPage = 10;
|
||||
|
||||
const findAllUsersQuery: FindAllUsersQuery = new FindAllUsersQuery(
|
||||
findAllUsersRequest,
|
||||
);
|
||||
|
||||
const mockUsers = [
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '0601020304',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a92',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
email: 'jane.doe@email.com',
|
||||
phone: '0602030405',
|
||||
},
|
||||
{
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a93',
|
||||
firstName: 'Jimmy',
|
||||
lastName: 'Doe',
|
||||
email: 'jimmy.doe@email.com',
|
||||
phone: '0603040506',
|
||||
},
|
||||
];
|
||||
|
||||
const mockUsersRepository = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
findAll: jest.fn().mockImplementation((query?: FindAllUsersQuery) => {
|
||||
return Promise.resolve(mockUsers);
|
||||
}),
|
||||
};
|
||||
|
||||
describe('FindAllUsersUseCase', () => {
|
||||
let findAllUsersUseCase: FindAllUsersUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: UsersRepository,
|
||||
useValue: mockUsersRepository,
|
||||
},
|
||||
FindAllUsersUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAllUsersUseCase = module.get<FindAllUsersUseCase>(FindAllUsersUseCase);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAllUsersUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should return an array filled with users', async () => {
|
||||
const users = await findAllUsersUseCase.execute(findAllUsersQuery);
|
||||
|
||||
expect(users).toBe(mockUsers);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,82 +0,0 @@
|
|||
import { NotFoundException } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { FindUserByUuidRequest } from '../../domain/dtos/find-user-by-uuid.request';
|
||||
import { FindUserByUuidUseCase } from '../../domain/usecases/find-user-by-uuid.usecase';
|
||||
import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
|
||||
const mockUser = {
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '0601020304',
|
||||
};
|
||||
|
||||
const mockUserRepository = {
|
||||
findOneByUuid: jest
|
||||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((query?: FindUserByUuidQuery) => {
|
||||
return Promise.resolve(mockUser);
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
return Promise.resolve(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('FindUserByUuidUseCase', () => {
|
||||
let findUserByUuidUseCase: FindUserByUuidUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: UsersRepository,
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
{
|
||||
provide: MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
FindUserByUuidUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findUserByUuidUseCase = module.get<FindUserByUuidUseCase>(
|
||||
FindUserByUuidUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findUserByUuidUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should return a user', async () => {
|
||||
const findUserByUuidRequest: FindUserByUuidRequest =
|
||||
new FindUserByUuidRequest();
|
||||
findUserByUuidRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
const user = await findUserByUuidUseCase.execute(
|
||||
new FindUserByUuidQuery(findUserByUuidRequest),
|
||||
);
|
||||
expect(user).toBe(mockUser);
|
||||
});
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
const findUserByUuidRequest: FindUserByUuidRequest =
|
||||
new FindUserByUuidRequest();
|
||||
findUserByUuidRequest.uuid = 'bb281075-1b98-4456-89d6-c643d3044a90';
|
||||
await expect(
|
||||
findUserByUuidUseCase.execute(
|
||||
new FindUserByUuidQuery(findUserByUuidRequest),
|
||||
),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaService } from '@modules/user/infrastructure/prisma.service';
|
||||
import { UserRepository } from '@modules/user/infrastructure/user.repository';
|
||||
import { UserMapper } from '@modules/user/user.mapper';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('User repository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let userMapper: UserMapper;
|
||||
let eventEmitter: EventEmitter2;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [EventEmitterModule.forRoot()],
|
||||
providers: [PrismaService, UserMapper],
|
||||
}).compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
userMapper = module.get<UserMapper>(UserMapper);
|
||||
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
|
||||
});
|
||||
it('should be defined', () => {
|
||||
expect(
|
||||
new UserRepository(
|
||||
prismaService,
|
||||
userMapper,
|
||||
eventEmitter,
|
||||
mockMessagePublisher,
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
UserAlreadyExistsException,
|
||||
} from '@modules/user/core/domain/user.errors';
|
||||
import { CreateUserGrpcController } from '@modules/user/interface/grpc-controllers/create-user.grpc.controller';
|
||||
import { CreateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/create-user.request.dto';
|
||||
|
||||
const createUserRequest: CreateUserRequestDto = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new UserAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new EmailAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new PhoneAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Create User Grpc Controller', () => {
|
||||
let createUserGrpcController: CreateUserGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
CreateUserGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
createUserGrpcController = module.get<CreateUserGrpcController>(
|
||||
CreateUserGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(createUserGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a new user', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
const result: IdResponse = await createUserGrpcController.create(
|
||||
createUserRequest,
|
||||
);
|
||||
expect(result).toBeInstanceOf(IdResponse);
|
||||
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if user already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createUserGrpcController.create(createUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if email already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createUserGrpcController.create(createUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if phone already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createUserGrpcController.create(createUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createUserGrpcController.create(createUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
import {
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { DeleteUserGrpcController } from '@modules/user/interface/grpc-controllers/delete-user.grpc.controller';
|
||||
import { DeleteUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/delete-user.request.dto';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const deleteUserRequest: DeleteUserRequestDto = {
|
||||
id: '78153e03-4861-4f58-a705-88526efee53b',
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => ({}))
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new DatabaseErrorException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Delete User Grpc Controller', () => {
|
||||
let deleteUserGrpcController: DeleteUserGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
DeleteUserGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteUserGrpcController = module.get<DeleteUserGrpcController>(
|
||||
DeleteUserGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteUserGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should delete a user', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
await deleteUserGrpcController.delete(deleteUserRequest);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if user does not exist', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteUserGrpcController.delete(deleteUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if a database error occurs', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteUserGrpcController.delete(deleteUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.INTERNAL);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await deleteUserGrpcController.delete(deleteUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { FindUserByIdGrpcController } from '@modules/user/interface/grpc-controllers/find-user-by-id.grpc.controller';
|
||||
import { UserMapper } from '@modules/user/user.mapper';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockQueryBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => '200d61a8-d878-4378-a609-c19ea71633d2')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new NotFoundException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
const mockUserMapper = {
|
||||
toResponse: jest.fn().mockImplementationOnce(() => ({
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
})),
|
||||
};
|
||||
|
||||
describe('Find User By Id Grpc Controller', () => {
|
||||
let findUserbyIdGrpcController: FindUserByIdGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: QueryBus,
|
||||
useValue: mockQueryBus,
|
||||
},
|
||||
{
|
||||
provide: UserMapper,
|
||||
useValue: mockUserMapper,
|
||||
},
|
||||
FindUserByIdGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findUserbyIdGrpcController = module.get<FindUserByIdGrpcController>(
|
||||
FindUserByIdGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findUserbyIdGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return a user', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockUserMapper, 'toResponse');
|
||||
const response = await findUserbyIdGrpcController.findOnebyId({
|
||||
id: '6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
|
||||
});
|
||||
expect(response.firstName).toBe('John');
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if user is not found', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockUserMapper, 'toResponse');
|
||||
expect.assertions(4);
|
||||
try {
|
||||
await findUserbyIdGrpcController.findOnebyId({
|
||||
id: 'ac85f5f4-41cd-4c5d-9aee-0a1acb176fb8',
|
||||
});
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockUserMapper, 'toResponse');
|
||||
expect.assertions(4);
|
||||
try {
|
||||
await findUserbyIdGrpcController.findOnebyId({
|
||||
id: '53c8e7ec-ef68-42bc-ba4c-5ef3effa60a6',
|
||||
});
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
EmailAlreadyExistsException,
|
||||
PhoneAlreadyExistsException,
|
||||
} from '@modules/user/core/domain/user.errors';
|
||||
import { UpdateUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/update-user.request.dto';
|
||||
import { UpdateUserGrpcController } from '@modules/user/interface/grpc-controllers/update-user.grpc.controller';
|
||||
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const updateFirstNameUserRequest: UpdateUserRequestDto = {
|
||||
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||
firstName: 'Johnny',
|
||||
};
|
||||
|
||||
const updateEmailUserRequest: UpdateUserRequestDto = {
|
||||
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||
email: 'john.doe@already.exists.email.com',
|
||||
};
|
||||
|
||||
const updatePhoneUserRequest: UpdateUserRequestDto = {
|
||||
id: 'c97b1783-76cf-4840-b298-b90b13c58894',
|
||||
phone: '+33611223344',
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => 'c97b1783-76cf-4840-b298-b90b13c58894')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new EmailAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new PhoneAlreadyExistsException();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Update User Grpc Controller', () => {
|
||||
let updateUserGrpcController: UpdateUserGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
UpdateUserGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateUserGrpcController = module.get<UpdateUserGrpcController>(
|
||||
UpdateUserGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateUserGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update a user', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
const result: IdResponse = await updateUserGrpcController.updateUser(
|
||||
updateFirstNameUserRequest,
|
||||
);
|
||||
expect(result).toBeInstanceOf(IdResponse);
|
||||
expect(result.id).toBe('c97b1783-76cf-4840-b298-b90b13c58894');
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if email already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await updateUserGrpcController.updateUser(updateEmailUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a dedicated RpcException if phone already exists', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await updateUserGrpcController.updateUser(updatePhoneUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await updateUserGrpcController.updateUser(updateFirstNameUserRequest);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MessagePublisher } from '../../adapters/secondaries/message-publisher';
|
||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
|
||||
|
||||
const mockMessageBrokerPublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Messager', () => {
|
||||
let messagePublisher: MessagePublisher;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
MessagePublisher,
|
||||
{
|
||||
provide: MESSAGE_BROKER_PUBLISHER,
|
||||
useValue: mockMessageBrokerPublisher,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
messagePublisher = module.get<MessagePublisher>(MessagePublisher);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(messagePublisher).toBeDefined();
|
||||
});
|
||||
|
||||
it('should publish a message', async () => {
|
||||
jest.spyOn(mockMessageBrokerPublisher, 'publish');
|
||||
messagePublisher.publish('user.create.info', 'my-test');
|
||||
expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,84 +0,0 @@
|
|||
import { classes } from '@automapper/classes';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersRepository } from '../../adapters/secondaries/users.repository';
|
||||
import { UpdateUserCommand } from '../../commands/update-user.command';
|
||||
import { UpdateUserRequest } from '../../domain/dtos/update-user.request';
|
||||
import { User } from '../../domain/entities/user';
|
||||
import { UpdateUserUseCase } from '../../domain/usecases/update-user.usecase';
|
||||
import { UserProfile } from '../../mappers/user.profile';
|
||||
import { MESSAGE_PUBLISHER } from '../../../../app.constants';
|
||||
|
||||
const originalUser: User = new User();
|
||||
originalUser.uuid = 'bb281075-1b98-4456-89d6-c643d3044a91';
|
||||
originalUser.lastName = 'Doe';
|
||||
|
||||
const updateUserRequest: UpdateUserRequest = {
|
||||
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
|
||||
lastName: 'Dane',
|
||||
};
|
||||
|
||||
const updateUserCommand: UpdateUserCommand = new UpdateUserCommand(
|
||||
updateUserRequest,
|
||||
);
|
||||
|
||||
const mockUsersRepository = {
|
||||
update: jest
|
||||
.fn()
|
||||
.mockImplementationOnce((uuid: string, params: any) => {
|
||||
originalUser.lastName = params.lastName;
|
||||
|
||||
return Promise.resolve(originalUser);
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new Error('Error');
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('UpdateUserUseCase', () => {
|
||||
let updateUserUseCase: UpdateUserUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
|
||||
providers: [
|
||||
{
|
||||
provide: UsersRepository,
|
||||
useValue: mockUsersRepository,
|
||||
},
|
||||
UpdateUserUseCase,
|
||||
UserProfile,
|
||||
{
|
||||
provide: MESSAGE_PUBLISHER,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateUserUseCase = module.get<UpdateUserUseCase>(UpdateUserUseCase);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateUserUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should update a user', async () => {
|
||||
const updatedUser: User = await updateUserUseCase.execute(
|
||||
updateUserCommand,
|
||||
);
|
||||
|
||||
expect(updatedUser.lastName).toBe(updateUserRequest.lastName);
|
||||
});
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
await expect(
|
||||
updateUserUseCase.execute(updateUserCommand),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
import { UserEntity } from '@modules/user/core/domain/user.entity';
|
||||
import {
|
||||
UserReadModel,
|
||||
UserWriteModel,
|
||||
} from '@modules/user/infrastructure/user.repository';
|
||||
import { UserResponseDto } from '@modules/user/interface/dtos/user.response.dto';
|
||||
import { UserMapper } from '@modules/user/user.mapper';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
const now = new Date('2023-06-21 06:00:00');
|
||||
const userEntity: UserEntity = new UserEntity({
|
||||
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||
props: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const userReadModel: UserReadModel = {
|
||||
uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@email.com',
|
||||
phone: '+33611223344',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
describe('User Mapper', () => {
|
||||
let userMapper: UserMapper;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [UserMapper],
|
||||
}).compile();
|
||||
userMapper = module.get<UserMapper>(UserMapper);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(userMapper).toBeDefined();
|
||||
});
|
||||
|
||||
it('should map domain entity to persistence data', async () => {
|
||||
const mapped: UserWriteModel = userMapper.toPersistence(userEntity);
|
||||
expect(mapped.lastName).toBe('Doe');
|
||||
});
|
||||
|
||||
it('should map persisted data to domain entity', async () => {
|
||||
const mapped: UserEntity = userMapper.toDomain(userReadModel);
|
||||
expect(mapped.getProps().firstName).toBe('John');
|
||||
});
|
||||
|
||||
it('should map domain entity to response', async () => {
|
||||
const mapped: UserResponseDto = userMapper.toResponse(userEntity);
|
||||
expect(mapped.id).toBe('c160cf8c-f057-4962-841f-3ad68346df44');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
export const USER_CREATED_ROUTING_KEY = 'user.created';
|
||||
export const USER_UPDATED_ROUTING_KEY = 'user.updated';
|
||||
export const USER_DELETED_ROUTING_KEY = 'user.deleted';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue