find all users by ids

This commit is contained in:
Sylvain Briat 2023-11-22 16:21:10 +01:00
parent ad8b98da40
commit 3f4236f34f
14 changed files with 815 additions and 432 deletions

View File

@ -56,6 +56,18 @@ The app exposes the following [gRPC](https://grpc.io/) services :
}
```
- **FindAllByIds** : find all users for the given ids
```json
{
"ids": [
"80126a61-d128-4f96-afdb-92e33c75a3e1",
"80126a61-d128-4f96-afdb-92e33c75a3e2",
"80126a61-d128-4f96-afdb-92e33c75a3e3"
]
}
```
- **Create** : create a user
```json

859
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -30,27 +30,26 @@
"migrate:deploy": "npx prisma migrate deploy"
},
"dependencies": {
"@grpc/grpc-js": "^1.9.6",
"@grpc/grpc-js": "^1.9.11",
"@grpc/proto-loader": "^0.7.10",
"@songkeys/nestjs-redis": "^10.0.0",
"@mobicoop/configuration-module": "^3.0.0",
"@mobicoop/ddd-library": "^2.0.0",
"@mobicoop/ddd-library": "^2.4.0",
"@mobicoop/health-module": "^2.3.1",
"@mobicoop/message-broker-module": "^2.1.1",
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/common": "^10.2.7",
"@nestjs/cache-manager": "^2.1.1",
"@nestjs/common": "^10.2.10",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.7",
"@nestjs/core": "^10.2.10",
"@nestjs/cqrs": "^10.2.6",
"@nestjs/event-emitter": "^2.0.2",
"@nestjs/microservices": "^10.2.7",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/microservices": "^10.2.10",
"@nestjs/platform-express": "^10.2.10",
"@nestjs/terminus": "^10.1.1",
"@prisma/client": "^5.4.2",
"@types/supertest": "^2.0.14",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"cache-manager": "^5.2.4",
"@prisma/client": "^5.6.0",
"@types/supertest": "^2.0.16",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"cache-manager": "^5.3.1",
"cache-manager-ioredis-yet": "^1.2.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
@ -58,25 +57,25 @@
"ioredis": "^5.3.2"
},
"devDependencies": {
"@nestjs/cli": "^10.1.18",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.7",
"@types/jest": "^29.5.6",
"@types/node": "^20.8.6",
"@types/uuid": "^9.0.5",
"eslint": "^8.51.0",
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.10",
"@types/jest": "^29.5.10",
"@types/node": "^20.9.4",
"@types/uuid": "^9.0.7",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "29.7.0",
"prettier": "^3.0.3",
"prisma": "^5.4.2",
"prettier": "^3.1.0",
"prisma": "^5.6.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "29.1.1",
"ts-loader": "^9.5.0",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "4.2.0",
"typescript": "^5.2.2"
"typescript": "^5.3.2"
},
"jest": {
"moduleFileExtensions": [

View File

@ -5,12 +5,6 @@ export const SERVICE_NAME = 'user';
export const GRPC_PACKAGE_NAME = 'user';
export const GRPC_SERVICE_NAME = 'UserService';
// configuration
export const SERVICE_CONFIGURATION_SET_QUEUE = 'user-configuration-set';
export const SERVICE_CONFIGURATION_DELETE_QUEUE = 'user-configuration-delete';
export const SERVICE_CONFIGURATION_PROPAGATE_QUEUE =
'user-configuration-propagate';
// health
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
export const HEALTH_USER_REPOSITORY = 'UserRepository';

View File

@ -1,10 +1,6 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ConfigModule } from '@nestjs/config';
import { UserModule } from './modules/user/user.module';
import {
ConfigurationModule,
ConfigurationModuleOptions,
} from '@mobicoop/configuration-module';
import {
HealthModule,
HealthModuleOptions,
@ -18,9 +14,6 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
import {
HEALTH_CRITICAL_LOGGING_KEY,
HEALTH_USER_REPOSITORY,
SERVICE_CONFIGURATION_DELETE_QUEUE,
SERVICE_CONFIGURATION_PROPAGATE_QUEUE,
SERVICE_CONFIGURATION_SET_QUEUE,
SERVICE_NAME,
} from './app.constants';
@ -28,36 +21,6 @@ import {
imports: [
ConfigModule.forRoot({ isGlobal: true }),
EventEmitterModule.forRoot(),
ConfigurationModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService,
): Promise<ConfigurationModuleOptions> => ({
domain: configService.get<string>(
'SERVICE_CONFIGURATION_DOMAIN',
) as string,
messageBroker: {
uri: configService.get<string>('MESSAGE_BROKER_URI') as string,
exchange: {
name: configService.get<string>(
'MESSAGE_BROKER_EXCHANGE',
) as string,
durable: configService.get<boolean>(
'MESSAGE_BROKER_EXCHANGE',
) as boolean,
},
},
redis: {
host: configService.get<string>('REDIS_HOST') as string,
password: configService.get<string>('REDIS_PASSWORD'),
port: configService.get<number>('REDIS_PORT') as number,
},
setConfigurationQueue: SERVICE_CONFIGURATION_SET_QUEUE,
deleteConfigurationQueue: SERVICE_CONFIGURATION_DELETE_QUEUE,
propagateConfigurationQueue: SERVICE_CONFIGURATION_PROPAGATE_QUEUE,
}),
}),
HealthModule.forRootAsync({
imports: [UserModule, MessagerModule],
inject: [USER_REPOSITORY, MESSAGE_PUBLISHER],

View File

@ -0,0 +1,17 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
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';
import { FindUsersByIdsQuery } from './find-users-by-ids.query';
@QueryHandler(FindUsersByIdsQuery)
export class FindUsersByIdsQueryHandler implements IQueryHandler {
constructor(
@Inject(USER_REPOSITORY)
private readonly userRepository: UserRepositoryPort,
) {}
async execute(query: FindUsersByIdsQuery): Promise<UserEntity[]> {
return await this.userRepository.findAllByIds(query.ids);
}
}

View File

@ -0,0 +1,10 @@
import { QueryBase } from '@mobicoop/ddd-library';
export class FindUsersByIdsQuery extends QueryBase {
readonly ids: string[];
constructor(ids: string[]) {
super();
this.ids = ids;
}
}

View File

@ -0,0 +1,5 @@
import { UserResponseDto } from './user.response.dto';
export class UsersResponseDto {
readonly users: readonly UserResponseDto[];
}

View File

@ -0,0 +1,7 @@
import { ArrayMinSize, IsArray } from 'class-validator';
export class FindUsersByIdsRequestDto {
@IsArray()
@ArrayMinSize(1)
ids: string[];
}

View File

@ -0,0 +1,44 @@
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { RpcValidationPipe } from '@mobicoop/ddd-library';
import { UserMapper } from '@modules/user/user.mapper';
import { UserEntity } from '@modules/user/core/domain/user.entity';
import { GRPC_SERVICE_NAME } from '@src/app.constants';
import { FindUsersByIdsRequestDto } from './dtos/find-users-by-ids.request.dto';
import { FindUsersByIdsQuery } from '@modules/user/core/application/queries/find-users-by-ids/find-users-by-ids.query';
import { UsersResponseDto } from '../dtos/users.response.dto';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class FindUsersByIdsGrpcController {
constructor(
protected readonly mapper: UserMapper,
private readonly queryBus: QueryBus,
) {}
@GrpcMethod(GRPC_SERVICE_NAME, 'FindAllByIds')
async findAllbyIds(
data: FindUsersByIdsRequestDto,
): Promise<UsersResponseDto> {
try {
const users: UserEntity[] = await this.queryBus.execute(
new FindUsersByIdsQuery(data.ids),
);
return {
users: users.map((user: UserEntity) => this.mapper.toResponse(user)),
};
} catch (e) {
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,
message: e.message,
});
}
}
}

View File

@ -4,7 +4,7 @@ package user;
service UserService {
rpc FindOneById(UserById) returns (User);
rpc FindAll(UserFilter) returns (Users);
rpc FindAllByIds(UsersById) returns (Users);
rpc Create(User) returns (UserById);
rpc Update(User) returns (UserById);
rpc Delete(UserById) returns (Empty);
@ -14,6 +14,10 @@ message UserById {
string id = 1;
}
message UsersById {
repeated string ids = 1;
}
message User {
string id = 1;
string firstName = 2;
@ -22,14 +26,8 @@ message User {
string phone = 5;
}
message UserFilter {
optional int32 page = 1;
optional int32 perPage = 2;
}
message Users {
repeated User data = 1;
int32 total = 2;
repeated User users = 1;
}
message Empty {}

View File

@ -0,0 +1,82 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserEntity } from '@modules/user/core/domain/user.entity';
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
import { FindUsersByIdsQueryHandler } from '@modules/user/core/application/queries/find-users-by-ids/find-users-by-ids.query-handler';
import { FindUsersByIdsQuery } from '@modules/user/core/application/queries/find-users-by-ids/find-users-by-ids.query';
const now = new Date('2023-06-21 06:00:00');
const user1: 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 user2: UserEntity = new UserEntity({
id: 'c160cf8c-f057-4962-841f-3ad68346df55',
props: {
firstName: 'Jane',
lastName: 'Doe',
email: 'jane.doe@email.com',
phone: '+33611223355',
},
createdAt: now,
updatedAt: now,
});
const user3: UserEntity = new UserEntity({
id: 'c160cf8c-f057-4962-841f-3ad68346df66',
props: {
firstName: 'Jimmy',
lastName: 'Doe',
email: 'jimmy.doe@email.com',
phone: '+33611223366',
},
createdAt: now,
updatedAt: now,
});
const mockUserRepository = {
findAllByIds: jest.fn().mockImplementation(() => [user1, user2, user3]),
};
describe('Find Users By Ids Query Handler', () => {
let findUsersByIdsQueryHandler: FindUsersByIdsQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: USER_REPOSITORY,
useValue: mockUserRepository,
},
FindUsersByIdsQueryHandler,
],
}).compile();
findUsersByIdsQueryHandler = module.get<FindUsersByIdsQueryHandler>(
FindUsersByIdsQueryHandler,
);
});
it('should be defined', () => {
expect(findUsersByIdsQueryHandler).toBeDefined();
});
describe('execution', () => {
it('should return users', async () => {
const findUsersByIdsQuery = new FindUsersByIdsQuery([
'c160cf8c-f057-4962-841f-3ad68346df44',
'c160cf8c-f057-4962-841f-3ad68346df55',
'c160cf8c-f057-4962-841f-3ad68346df66',
]);
const users: UserEntity[] =
await findUsersByIdsQueryHandler.execute(findUsersByIdsQuery);
expect(users).toHaveLength(3);
expect(users[1].getProps().firstName).toBe('Jane');
});
});
});

View File

@ -0,0 +1,95 @@
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { FindUsersByIdsGrpcController } from '@modules/user/interface/grpc-controllers/find-users-by-ids.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(() => [
'6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
'6dcf093c-c7db-4dae-8e9c-c715cebf83c8',
'6dcf093c-c7db-4dae-8e9c-c715cebf83c9',
])
.mockImplementationOnce(() => {
throw new Error();
}),
};
const mockUserMapper = {
toResponse: jest.fn().mockImplementationOnce(() => ({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@email.com',
phone: '+33611223344',
})),
};
describe('Find Users By Ids Grpc Controller', () => {
let findUsersbyIdsGrpcController: FindUsersByIdsGrpcController;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: QueryBus,
useValue: mockQueryBus,
},
{
provide: UserMapper,
useValue: mockUserMapper,
},
FindUsersByIdsGrpcController,
],
}).compile();
findUsersbyIdsGrpcController = module.get<FindUsersByIdsGrpcController>(
FindUsersByIdsGrpcController,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(findUsersbyIdsGrpcController).toBeDefined();
});
it('should return users', async () => {
jest.spyOn(mockQueryBus, 'execute');
jest.spyOn(mockUserMapper, 'toResponse');
const response = await findUsersbyIdsGrpcController.findAllbyIds({
ids: [
'6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
'6dcf093c-c7db-4dae-8e9c-c715cebf83c8',
'6dcf093c-c7db-4dae-8e9c-c715cebf83c9',
],
});
expect(response.users).toHaveLength(3);
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(3);
});
it('should throw a generic RpcException', async () => {
jest.spyOn(mockQueryBus, 'execute');
jest.spyOn(mockUserMapper, 'toResponse');
expect.assertions(4);
try {
await findUsersbyIdsGrpcController.findAllbyIds({
ids: [
'6dcf093c-c7db-4dae-8e9c-c715cebf83c7',
'6dcf093c-c7db-4dae-8e9c-c715cebf83c8',
'6dcf093c-c7db-4dae-8e9c-c715cebf83c9',
],
});
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
}
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
expect(mockUserMapper.toResponse).toHaveBeenCalledTimes(0);
});
});

View File

@ -20,6 +20,8 @@ import { PublishMessageWhenUserIsCreatedDomainEventHandler } from './core/applic
import { PublishMessageWhenUserIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler';
import { PublishMessageWhenUserIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler';
import { RedisClientOptions } from '@songkeys/nestjs-redis';
import { FindUsersByIdsGrpcController } from './interface/grpc-controllers/find-users-by-ids.grpc.controller';
import { FindUsersByIdsQueryHandler } from './core/application/queries/find-users-by-ids/find-users-by-ids.query-handler';
const imports = [
CqrsModule,
@ -42,6 +44,7 @@ const grpcControllers = [
UpdateUserGrpcController,
DeleteUserGrpcController,
FindUserByIdGrpcController,
FindUsersByIdsGrpcController,
];
const eventHandlers: Provider[] = [
@ -56,7 +59,10 @@ const commandHandlers: Provider[] = [
DeleteUserService,
];
const queryHandlers: Provider[] = [FindUserByIdQueryHandler];
const queryHandlers: Provider[] = [
FindUserByIdQueryHandler,
FindUsersByIdsQueryHandler,
];
const mappers: Provider[] = [UserMapper];