refactor: query handlers now don't use repositories and retrieve models directly

This commit is contained in:
user
2022-10-11 19:55:11 +02:00
parent 3b3c3aa3d7
commit 92515cfa73
8 changed files with 62 additions and 50 deletions

View File

@@ -274,15 +274,15 @@ Read more:
### Queries
`Query` is similar to a `Command`. It signals user intent to find something and describes how to do it.
`Query` is similar to a `Command`. It belongs to a read model and signals user intent to find something and describes how to do it.
`Query` is just a data retrieval operation and should not make any state changes (like writes to the database, files, third party APIs, etc.).
`Query` is just a data retrieval operation and should not make any state changes (like writes to the database, files, third party APIs, etc.). For this reason, in read model we can bypass a domain and repository layers completely and query database directly from a query handler.
Similarly to Commands, Queries can use a `Query Bus` if needed. This way you can query anything from anywhere without importing repositories directly and avoid coupling.
Similarly to Commands, Queries can use a `Query Bus` if needed. This way you can query anything from anywhere without importing classes directly and avoid coupling.
Example files:
- [find-users.query-handler.ts](src/modules/user/queries/find-users/find-users.query-handler.ts) - a query handler
- [find-users.query-handler.ts](src/modules/user/queries/find-users/find-users.query-handler.ts) - a query handler. Notice how we query the database directly, without using domain objects or repositories (more info [here](https://codeopinion.com/should-you-use-the-repository-pattern-with-cqrs-yes-and-no/)).
---
@@ -1006,6 +1006,7 @@ This project contains abstract repository class that allows to make basic CRUD o
Read more:
- [Design the infrastructure persistence layer](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design)
- [Should you use the Repository Pattern? With CQRS, Yes and No!](https://codeopinion.com/should-you-use-the-repository-pattern-with-cqrs-yes-and-no/) - in a read model / query handlers it is not required to use a repository pattern.
## Persistence models

View File

@@ -18,5 +18,5 @@ export abstract class PaginatedResponseDto<T> extends Paginated<T> {
readonly page: number;
@ApiProperty({ isArray: true })
abstract data: T[];
abstract readonly data: readonly T[];
}

View File

@@ -11,7 +11,7 @@ export class Paginated<T> {
readonly count: number;
readonly limit: number;
readonly page: number;
readonly data: T[];
readonly data: readonly T[];
constructor(props: Paginated<T>) {
this.count = props.count;

View File

@@ -1,4 +1,4 @@
import { Paginated, PaginatedQueryParams, RepositoryPort } from '@libs/ddd';
import { PaginatedQueryParams, RepositoryPort } from '@libs/ddd';
import { UserEntity } from '../domain/user.entity';
export interface FindUsersParams extends PaginatedQueryParams {
@@ -9,5 +9,4 @@ export interface FindUsersParams extends PaginatedQueryParams {
export interface UserRepositoryPort extends RepositoryPort<UserEntity> {
findOneByEmail(email: string): Promise<UserEntity | null>;
findUsers(query: FindUsersParams): Promise<Paginated<UserEntity>>;
}

View File

@@ -1,6 +1,6 @@
import { InjectPool } from 'nestjs-slonik';
import { DatabasePool, sql } from 'slonik';
import { FindUsersParams, UserRepositoryPort } from './user.repository.port';
import { UserRepositoryPort } from './user.repository.port';
import { z } from 'zod';
import { UserMapper } from '../user.mapper';
import { UserRoles } from '../domain/user.types';
@@ -8,7 +8,6 @@ import { UserEntity } from '../domain/user.entity';
import { SqlRepositoryBase } from '@src/libs/db/sql-repository.base';
import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Paginated } from '@src/libs/ddd';
/**
* Runtime validation of user object for extra safety (in case database schema changes).
@@ -28,6 +27,9 @@ export const userSchema = z.object({
export type UserModel = z.TypeOf<typeof userSchema>;
/**
* Repository is used for retrieving/saving domain entities
* */
@Injectable()
export class UserRepository
extends SqlRepositoryBase<UserEntity, UserModel>
@@ -46,31 +48,6 @@ export class UserRepository
super(pool, mapper, eventEmitter, new Logger(UserRepository.name));
}
async findUsers(query: FindUsersParams): Promise<Paginated<UserEntity>> {
/**
* Constructing a query with Slonik.
* More info: https://contra.com/p/AqZWWoUB-writing-composable-sql-using-java-script
*/
const statement = sql.type(userSchema)`
SELECT *
FROM users
WHERE
${query.country ? sql`country = ${query.country}` : true} AND
${query.street ? sql`street = ${query.street}` : true} AND
${query.postalCode ? sql`"postalCode" = ${query.postalCode}` : true}
LIMIT ${query.limit}
OFFSET ${query.offset}`;
const records = await this.pool.query(statement);
return new Paginated({
data: records.rows.map(this.mapper.toDomain),
count: records.rowCount,
limit: query.limit,
page: query.page,
});
}
async updateAddress(user: UserEntity): Promise<void> {
const address = user.getPropsCopy().address;
const statement = sql.type(userSchema)`

View File

@@ -4,5 +4,5 @@ import { UserResponseDto } from './user.response.dto';
export class UserPaginatedResponseDto extends PaginatedResponseDto<UserResponseDto> {
@ApiProperty({ type: UserResponseDto, isArray: true })
data: UserResponseDto[];
readonly data: readonly UserResponseDto[];
}

View File

@@ -4,12 +4,13 @@ import { QueryBus } from '@nestjs/cqrs';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Result } from 'oxide.ts';
import { FindUsersRequestDto } from './find-users.request.dto';
import { UserEntity } from '@modules/user/domain/user.entity';
import { FindUsersQuery } from './find-users.query-handler';
import { UserMapper } from '@modules/user/user.mapper';
import { Paginated } from '@src/libs/ddd';
import { UserPaginatedResponseDto } from '../../dtos/user.paginated.response.dto';
import { PaginatedQueryRequestDto } from '@src/libs/api/paginated-query.request.dto';
import { UserModel } from '../../database/user.repository';
import { ResponseBase } from '@src/libs/api/response.base';
@Controller(routesV1.version)
export class FindUsersHttpController {
@@ -34,15 +35,22 @@ export class FindUsersHttpController {
page: queryParams?.page,
});
const result: Result<
Paginated<UserEntity>,
Paginated<UserModel>,
Error
> = await this.queryBus.execute(query);
const users = result.unwrap();
const paginated = result.unwrap();
// Whitelisting returned properties
return new UserPaginatedResponseDto({
...users,
data: users.data.map(this.userMapper.toResponse),
...paginated,
data: paginated.data.map((user) => ({
...new ResponseBase(user),
email: user.email,
country: user.country,
street: user.street,
postalCode: user.postalCode,
})),
});
}
}

View File

@@ -1,11 +1,10 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { Ok, Result } from 'oxide.ts';
import { Inject } from '@nestjs/common';
import { UserRepositoryPort } from '../../database/user.repository.port';
import { USER_REPOSITORY } from '../../user.di-tokens';
import { UserEntity } from '@modules/user/domain/user.entity';
import { PaginatedParams, PaginatedQueryBase } from '@libs/ddd/query.base';
import { Paginated } from '@src/libs/ddd';
import { InjectPool } from 'nestjs-slonik';
import { DatabasePool, sql } from 'slonik';
import { UserModel, userSchema } from '../../database/user.repository';
export class FindUsersQuery extends PaginatedQueryBase {
readonly country?: string;
@@ -25,14 +24,42 @@ export class FindUsersQuery extends PaginatedQueryBase {
@QueryHandler(FindUsersQuery)
export class FindUsersQueryHandler implements IQueryHandler {
constructor(
@Inject(USER_REPOSITORY)
private readonly userRepo: UserRepositoryPort,
@InjectPool()
private readonly pool: DatabasePool,
) {}
/**
* In read model we don't need to execute
* any business logic, so we can bypass
* domain and repository layers completely
* and execute query directly
*/
async execute(
query: FindUsersQuery,
): Promise<Result<Paginated<UserEntity>, Error>> {
const users = await this.userRepo.findUsers(query);
return Ok(users);
): Promise<Result<Paginated<UserModel>, Error>> {
/**
* Constructing a query with Slonik.
* More info: https://contra.com/p/AqZWWoUB-writing-composable-sql-using-java-script
*/
const statement = sql.type(userSchema)`
SELECT *
FROM users
WHERE
${query.country ? sql`country = ${query.country}` : true} AND
${query.street ? sql`street = ${query.street}` : true} AND
${query.postalCode ? sql`"postalCode" = ${query.postalCode}` : true}
LIMIT ${query.limit}
OFFSET ${query.offset}`;
const records = await this.pool.query(statement);
return Ok(
new Paginated({
data: records.rows,
count: records.rowCount,
limit: query.limit,
page: query.page,
}),
);
}
}