move libs to dedicated package
This commit is contained in:
parent
62244b6eb1
commit
27114c79b0
|
@ -13,6 +13,7 @@
|
||||||
"@grpc/proto-loader": "^0.7.6",
|
"@grpc/proto-loader": "^0.7.6",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/configuration-module": "^1.2.0",
|
"@mobicoop/configuration-module": "^1.2.0",
|
||||||
|
"@mobicoop/ddd-library": "^0.0.1",
|
||||||
"@mobicoop/message-broker-module": "^1.2.0",
|
"@mobicoop/message-broker-module": "^1.2.0",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.3.1",
|
"@nestjs/config": "^2.3.1",
|
||||||
|
@ -1415,6 +1416,21 @@
|
||||||
"@nestjs/common": "^9.4.2"
|
"@nestjs/common": "^9.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mobicoop/ddd-library": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-T6g3pgodrMOZ1yrtNWylgu6EjRz3HPgcD9UoK0cJCvfiq9WjTH9TOZ6wKh9vIijiO+a5KJkZiKHbbjzuJvdwCg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/event-emitter": "^1.4.2",
|
||||||
|
"@nestjs/microservices": "^9.4.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^9.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mobicoop/message-broker-module": {
|
"node_modules/@mobicoop/message-broker-module": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"license": "AGPL",
|
"license": "AGPL",
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
"@grpc/proto-loader": "^0.7.6",
|
"@grpc/proto-loader": "^0.7.6",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/configuration-module": "^1.2.0",
|
"@mobicoop/configuration-module": "^1.2.0",
|
||||||
|
"@mobicoop/ddd-library": "^0.0.1",
|
||||||
"@mobicoop/message-broker-module": "^1.2.0",
|
"@mobicoop/message-broker-module": "^1.2.0",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.3.1",
|
"@nestjs/config": "^2.3.1",
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
export class ApiErrorResponse {
|
|
||||||
readonly statusCode: number;
|
|
||||||
|
|
||||||
readonly message: string;
|
|
||||||
|
|
||||||
readonly error: string;
|
|
||||||
|
|
||||||
readonly correlationId: string;
|
|
||||||
|
|
||||||
readonly subErrors?: string[];
|
|
||||||
|
|
||||||
constructor(body: ApiErrorResponse) {
|
|
||||||
this.statusCode = body.statusCode;
|
|
||||||
this.message = body.message;
|
|
||||||
this.error = body.error;
|
|
||||||
this.correlationId = body.correlationId;
|
|
||||||
this.subErrors = body.subErrors;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export class IdResponse {
|
|
||||||
constructor(id: string) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly id: string;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Paginated } from '../ddd';
|
|
||||||
|
|
||||||
export abstract class PaginatedResponseDto<T> extends Paginated<T> {
|
|
||||||
readonly total: number;
|
|
||||||
readonly perPage: number;
|
|
||||||
readonly page: number;
|
|
||||||
abstract readonly data: readonly T[];
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { IdResponse } from './id.response.dto';
|
|
||||||
|
|
||||||
export interface BaseResponseProps {
|
|
||||||
id: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Most of our response objects will have properties like
|
|
||||||
* id, createdAt and updatedAt so we can move them to a
|
|
||||||
* separate class and extend it to avoid duplication.
|
|
||||||
*/
|
|
||||||
export class ResponseBase extends IdResponse {
|
|
||||||
constructor(props: BaseResponseProps) {
|
|
||||||
super(props.id);
|
|
||||||
this.createdAt = new Date(props.createdAt).toISOString();
|
|
||||||
this.updatedAt = new Date(props.updatedAt).toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly createdAt: string;
|
|
||||||
readonly updatedAt: string;
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { AggregateRoot, Mapper, RepositoryPort } from '../ddd';
|
|
||||||
import { ObjectLiteral } from '../types';
|
|
||||||
import { LoggerPort } from '../ports/logger.port';
|
|
||||||
import {
|
|
||||||
PrismaRawRepositoryPort,
|
|
||||||
PrismaRepositoryPort,
|
|
||||||
} from '../ports/prisma-repository.port';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import {
|
|
||||||
ConflictException,
|
|
||||||
DatabaseErrorException,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@libs/exceptions';
|
|
||||||
|
|
||||||
export abstract class PrismaRepositoryBase<
|
|
||||||
Aggregate extends AggregateRoot<any>,
|
|
||||||
DbReadModel extends ObjectLiteral,
|
|
||||||
DbWriteModel extends ObjectLiteral,
|
|
||||||
> implements RepositoryPort<Aggregate>
|
|
||||||
{
|
|
||||||
protected constructor(
|
|
||||||
protected readonly prisma: PrismaRepositoryPort<Aggregate> | any,
|
|
||||||
protected readonly prismaRaw: PrismaRawRepositoryPort,
|
|
||||||
protected readonly mapper: Mapper<Aggregate, DbReadModel, DbWriteModel>,
|
|
||||||
protected readonly eventEmitter: EventEmitter2,
|
|
||||||
protected readonly logger: LoggerPort,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async findOneById(id: string, include?: any): Promise<Aggregate> {
|
|
||||||
const entity = await this.prisma.findUnique({
|
|
||||||
where: { uuid: id },
|
|
||||||
include,
|
|
||||||
});
|
|
||||||
if (entity) return this.mapper.toDomain(entity);
|
|
||||||
throw new NotFoundException('Record not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
async insert(entity: Aggregate): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.prisma.create({
|
|
||||||
data: this.mapper.toPersistence(entity),
|
|
||||||
});
|
|
||||||
entity.publishEvents(this.logger, this.eventEmitter);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
if (e.message.includes('Already exists')) {
|
|
||||||
throw new ConflictException('Record already exists', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async healthCheck(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await this.prismaRaw.$queryRaw`SELECT 1`;
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseErrorException(e.message);
|
|
||||||
}
|
|
||||||
throw new DatabaseErrorException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Entity } from './entity.base';
|
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { LoggerPort } from '@libs/ports/logger.port';
|
|
||||||
import { DomainEvent } from './domain-event.base';
|
|
||||||
|
|
||||||
export abstract class AggregateRoot<EntityProps> extends Entity<EntityProps> {
|
|
||||||
private _domainEvents: DomainEvent[] = [];
|
|
||||||
|
|
||||||
get domainEvents(): DomainEvent[] {
|
|
||||||
return this._domainEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addEvent(domainEvent: DomainEvent): void {
|
|
||||||
this._domainEvents.push(domainEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearEvents(): void {
|
|
||||||
this._domainEvents = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async publishEvents(
|
|
||||||
logger: LoggerPort,
|
|
||||||
eventEmitter: EventEmitter2,
|
|
||||||
): Promise<void> {
|
|
||||||
await Promise.all(
|
|
||||||
this.domainEvents.map(async (event) => {
|
|
||||||
logger.debug(
|
|
||||||
`"${event.constructor.name}" event published for aggregate ${this.constructor.name} : ${this.id}`,
|
|
||||||
);
|
|
||||||
return eventEmitter.emitAsync(event.constructor.name, event);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.clearEvents();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { ArgumentNotProvidedException } from '../exceptions';
|
|
||||||
import { Guard } from '../guard';
|
|
||||||
|
|
||||||
export type CommandProps<T> = Omit<T, 'id' | 'metadata'> & Partial<Command>;
|
|
||||||
|
|
||||||
type CommandMetadata = {
|
|
||||||
/** ID for correlation purposes (for commands that
|
|
||||||
* arrive from other microservices,logs correlation, etc). */
|
|
||||||
readonly correlationId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Causation id to reconstruct execution order if needed
|
|
||||||
*/
|
|
||||||
readonly causationId?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ID of a user who invoker the command. Can be useful for
|
|
||||||
* logging and tracking execution of commands and events
|
|
||||||
*/
|
|
||||||
readonly userId?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time when the command occurred. Mostly for tracing purposes
|
|
||||||
*/
|
|
||||||
readonly timestamp: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Command {
|
|
||||||
/**
|
|
||||||
* Command id, in case if we want to save it
|
|
||||||
* for auditing purposes and create a correlation/causation chain
|
|
||||||
*/
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
readonly metadata: CommandMetadata;
|
|
||||||
|
|
||||||
constructor(props: CommandProps<unknown>) {
|
|
||||||
if (Guard.isEmpty(props)) {
|
|
||||||
throw new ArgumentNotProvidedException(
|
|
||||||
'Command props should not be empty',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.id = props.id || v4();
|
|
||||||
this.metadata = {
|
|
||||||
correlationId: props?.metadata?.correlationId,
|
|
||||||
causationId: props?.metadata?.causationId,
|
|
||||||
timestamp: props?.metadata?.timestamp || Date.now(),
|
|
||||||
userId: props?.metadata?.userId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { ArgumentNotProvidedException } from '../exceptions';
|
|
||||||
import { Guard } from '../guard';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
type DomainEventMetadata = {
|
|
||||||
/** Timestamp when this domain event occurred */
|
|
||||||
readonly timestamp: number;
|
|
||||||
|
|
||||||
/** ID for correlation purposes (for Integration Events,logs correlation, etc).
|
|
||||||
*/
|
|
||||||
readonly correlationId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Causation id used to reconstruct execution order if needed
|
|
||||||
*/
|
|
||||||
readonly causationId?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User ID for debugging and logging purposes
|
|
||||||
*/
|
|
||||||
readonly userId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DomainEventProps<T> = Omit<T, 'id' | 'metadata'> & {
|
|
||||||
aggregateId: string;
|
|
||||||
metadata?: DomainEventMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
export abstract class DomainEvent {
|
|
||||||
public readonly id: string;
|
|
||||||
|
|
||||||
/** Aggregate ID where domain event occurred */
|
|
||||||
public readonly aggregateId: string;
|
|
||||||
|
|
||||||
public readonly metadata: DomainEventMetadata;
|
|
||||||
|
|
||||||
constructor(props: DomainEventProps<unknown>) {
|
|
||||||
if (Guard.isEmpty(props)) {
|
|
||||||
throw new ArgumentNotProvidedException(
|
|
||||||
'DomainEvent props should not be empty',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.id = v4();
|
|
||||||
this.aggregateId = props.aggregateId;
|
|
||||||
this.metadata = {
|
|
||||||
correlationId: props?.metadata?.correlationId,
|
|
||||||
causationId: props?.metadata?.causationId,
|
|
||||||
timestamp: props?.metadata?.timestamp || Date.now(),
|
|
||||||
userId: props?.metadata?.userId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
import {
|
|
||||||
ArgumentNotProvidedException,
|
|
||||||
ArgumentInvalidException,
|
|
||||||
ArgumentOutOfRangeException,
|
|
||||||
} from '../exceptions';
|
|
||||||
import { Guard } from '../guard';
|
|
||||||
import { convertPropsToObject } from '../utils';
|
|
||||||
|
|
||||||
export type AggregateID = string;
|
|
||||||
|
|
||||||
export interface BaseEntityProps {
|
|
||||||
id: AggregateID;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateEntityProps<T> {
|
|
||||||
id: AggregateID;
|
|
||||||
props: T;
|
|
||||||
createdAt?: Date;
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class Entity<EntityProps> {
|
|
||||||
constructor({
|
|
||||||
id,
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
props,
|
|
||||||
}: CreateEntityProps<EntityProps>) {
|
|
||||||
this.setId(id);
|
|
||||||
this.validateProps(props);
|
|
||||||
const now = new Date();
|
|
||||||
this._createdAt = createdAt || now;
|
|
||||||
this._updatedAt = updatedAt || now;
|
|
||||||
this.props = props;
|
|
||||||
this.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly props: EntityProps;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ID is set in the concrete entity implementation to support
|
|
||||||
* different ID types depending on your needs.
|
|
||||||
* For example it could be a UUID for aggregate root,
|
|
||||||
* and shortid / nanoid for child entities.
|
|
||||||
*/
|
|
||||||
protected abstract _id: AggregateID;
|
|
||||||
|
|
||||||
private readonly _createdAt: Date;
|
|
||||||
|
|
||||||
private _updatedAt: Date;
|
|
||||||
|
|
||||||
get id(): AggregateID {
|
|
||||||
return this._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setId(id: AggregateID): void {
|
|
||||||
this._id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
get createdAt(): Date {
|
|
||||||
return this._createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
get updatedAt(): Date {
|
|
||||||
return this._updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
static isEntity(entity: unknown): entity is Entity<unknown> {
|
|
||||||
return entity instanceof Entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if two entities are the same Entity by comparing ID field.
|
|
||||||
* @param object Entity
|
|
||||||
*/
|
|
||||||
public equals(object?: Entity<EntityProps>): boolean {
|
|
||||||
if (object === null || object === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this === object) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Entity.isEntity(object)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.id ? this.id === object.id : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns entity properties.
|
|
||||||
* @return {*} {Props & EntityProps}
|
|
||||||
* @memberof Entity
|
|
||||||
*/
|
|
||||||
public getProps(): EntityProps & BaseEntityProps {
|
|
||||||
const propsCopy = {
|
|
||||||
id: this._id,
|
|
||||||
createdAt: this._createdAt,
|
|
||||||
updatedAt: this._updatedAt,
|
|
||||||
...this.props,
|
|
||||||
};
|
|
||||||
return Object.freeze(propsCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert an Entity and all sub-entities/Value Objects it
|
|
||||||
* contains to a plain object with primitive types. Can be
|
|
||||||
* useful when logging an entity during testing/debugging
|
|
||||||
*/
|
|
||||||
public toObject(): unknown {
|
|
||||||
const plainProps = convertPropsToObject(this.props);
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
id: this._id,
|
|
||||||
createdAt: this._createdAt,
|
|
||||||
updatedAt: this._updatedAt,
|
|
||||||
...plainProps,
|
|
||||||
};
|
|
||||||
return Object.freeze(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There are certain rules that always have to be true (invariants)
|
|
||||||
* for each entity. Validate method is called every time before
|
|
||||||
* saving an entity to the database to make sure those rules are respected.
|
|
||||||
*/
|
|
||||||
public abstract validate(): void;
|
|
||||||
|
|
||||||
private validateProps(props: EntityProps): void {
|
|
||||||
const MAX_PROPS = 50;
|
|
||||||
|
|
||||||
if (Guard.isEmpty(props)) {
|
|
||||||
throw new ArgumentNotProvidedException(
|
|
||||||
'Entity props should not be empty',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof props !== 'object') {
|
|
||||||
throw new ArgumentInvalidException('Entity props should be an object');
|
|
||||||
}
|
|
||||||
if (Object.keys(props as any).length > MAX_PROPS) {
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
`Entity props should not have more than ${MAX_PROPS} properties`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export * from './aggregate-root.base';
|
|
||||||
export * from './command.base';
|
|
||||||
export * from './domain-event.base';
|
|
||||||
export * from './entity.base';
|
|
||||||
export * from './mapper.interface';
|
|
||||||
export * from './repository.port';
|
|
||||||
export * from './value-object.base';
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Entity } from './entity.base';
|
|
||||||
|
|
||||||
export interface Mapper<
|
|
||||||
DomainEntity extends Entity<any>,
|
|
||||||
DbReadRecord,
|
|
||||||
DbWriteRecord,
|
|
||||||
Response = any,
|
|
||||||
> {
|
|
||||||
toPersistence(entity: DomainEntity): DbWriteRecord;
|
|
||||||
toDomain(record: DbReadRecord): DomainEntity;
|
|
||||||
toResponse(entity: DomainEntity): Response;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { OrderBy, PaginatedQueryParams } from './repository.port';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for regular queries
|
|
||||||
*/
|
|
||||||
export abstract class QueryBase {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for paginated queries
|
|
||||||
*/
|
|
||||||
export abstract class PaginatedQueryBase extends QueryBase {
|
|
||||||
perPage: number;
|
|
||||||
offset: number;
|
|
||||||
orderBy: OrderBy;
|
|
||||||
page: number;
|
|
||||||
|
|
||||||
constructor(props: PaginatedParams<PaginatedQueryBase>) {
|
|
||||||
super();
|
|
||||||
this.perPage = props.perPage || 10;
|
|
||||||
this.offset = props.page ? props.page * this.perPage : 0;
|
|
||||||
this.page = props.page || 0;
|
|
||||||
this.orderBy = props.orderBy || { field: true, param: 'desc' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paginated query parameters
|
|
||||||
export type PaginatedParams<T> = Omit<
|
|
||||||
T,
|
|
||||||
'perPage' | 'offset' | 'orderBy' | 'page'
|
|
||||||
> &
|
|
||||||
Partial<Omit<PaginatedQueryParams, 'offset'>>;
|
|
|
@ -1,40 +0,0 @@
|
||||||
/* Most of repositories will probably need generic
|
|
||||||
save/find/delete operations, so it's easier
|
|
||||||
to have some shared interfaces.
|
|
||||||
More specific queries should be defined
|
|
||||||
in a respective repository.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class Paginated<T> {
|
|
||||||
readonly total: number;
|
|
||||||
readonly perPage: number;
|
|
||||||
readonly page: number;
|
|
||||||
readonly data: readonly T[];
|
|
||||||
|
|
||||||
constructor(props: Paginated<T>) {
|
|
||||||
this.total = props.total;
|
|
||||||
this.perPage = props.perPage;
|
|
||||||
this.page = props.page;
|
|
||||||
this.data = props.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OrderBy = { field: string | true; param: 'asc' | 'desc' };
|
|
||||||
|
|
||||||
export type PaginatedQueryParams = {
|
|
||||||
perPage: number;
|
|
||||||
page: number;
|
|
||||||
offset: number;
|
|
||||||
orderBy: OrderBy;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface RepositoryPort<Entity> {
|
|
||||||
insert(entity: Entity | Entity[]): Promise<void>;
|
|
||||||
findOneById(id: string, include?: any): Promise<Entity>;
|
|
||||||
healthCheck(): Promise<boolean>;
|
|
||||||
// findAll(): Promise<Entity[]>;
|
|
||||||
// findAllPaginated(params: PaginatedQueryParams): Promise<Paginated<Entity>>;
|
|
||||||
// delete(entity: Entity): Promise<boolean>;
|
|
||||||
|
|
||||||
// transaction<T>(handler: () => Promise<T>): Promise<T>;
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import { ArgumentNotProvidedException } from '../exceptions';
|
|
||||||
import { Guard } from '../guard';
|
|
||||||
import { convertPropsToObject } from '../utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain Primitive is an object that contains only a single value
|
|
||||||
*/
|
|
||||||
export type Primitives = string | number | boolean;
|
|
||||||
export interface DomainPrimitive<T extends Primitives | Date> {
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValueObjectProps<T> = T extends Primitives | Date ? DomainPrimitive<T> : T;
|
|
||||||
|
|
||||||
export abstract class ValueObject<T> {
|
|
||||||
protected readonly props: ValueObjectProps<T>;
|
|
||||||
|
|
||||||
constructor(props: ValueObjectProps<T>) {
|
|
||||||
this.checkIfEmpty(props);
|
|
||||||
this.validate(props);
|
|
||||||
this.props = props;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract validate(props: ValueObjectProps<T>): void;
|
|
||||||
|
|
||||||
static isValueObject(obj: unknown): obj is ValueObject<unknown> {
|
|
||||||
return obj instanceof ValueObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if two Value Objects are equal. Checks structural equality.
|
|
||||||
* @param vo ValueObject
|
|
||||||
*/
|
|
||||||
public equals(vo?: ValueObject<T>): boolean {
|
|
||||||
if (vo === null || vo === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return JSON.stringify(this) === JSON.stringify(vo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unpack a value object to get its raw properties
|
|
||||||
*/
|
|
||||||
public unpack(): T {
|
|
||||||
if (this.isDomainPrimitive(this.props)) {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const propsCopy = convertPropsToObject(this.props);
|
|
||||||
|
|
||||||
return Object.freeze(propsCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkIfEmpty(props: ValueObjectProps<T>): void {
|
|
||||||
if (
|
|
||||||
Guard.isEmpty(props) ||
|
|
||||||
(this.isDomainPrimitive(props) && Guard.isEmpty(props.value))
|
|
||||||
) {
|
|
||||||
throw new ArgumentNotProvidedException('Property cannot be empty');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isDomainPrimitive(
|
|
||||||
obj: unknown,
|
|
||||||
): obj is DomainPrimitive<T & (Primitives | Date)> {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(obj, 'value')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
export interface SerializedException {
|
|
||||||
message: string;
|
|
||||||
code: string;
|
|
||||||
correlationId: string;
|
|
||||||
stack?: string;
|
|
||||||
cause?: string;
|
|
||||||
metadata?: unknown;
|
|
||||||
/**
|
|
||||||
* ^ Consider adding optional `metadata` object to
|
|
||||||
* exceptions (if language doesn't support anything
|
|
||||||
* similar by default) and pass some useful technical
|
|
||||||
* information about the exception when throwing.
|
|
||||||
* This will make debugging easier.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for custom exceptions.
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
* @class ExceptionBase
|
|
||||||
* @extends {Error}
|
|
||||||
*/
|
|
||||||
export abstract class ExceptionBase extends Error {
|
|
||||||
abstract code: string;
|
|
||||||
|
|
||||||
public readonly correlationId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} message
|
|
||||||
* @param {ObjectLiteral} [metadata={}]
|
|
||||||
* **BE CAREFUL** not to include sensitive info in 'metadata'
|
|
||||||
* to prevent leaks since all exception's data will end up
|
|
||||||
* in application's log files. Only include non-sensitive
|
|
||||||
* info that may help with debugging.
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
readonly message: string,
|
|
||||||
readonly cause?: Error,
|
|
||||||
readonly metadata?: unknown,
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* By default in NodeJS Error objects are not
|
|
||||||
* serialized properly when sending plain objects
|
|
||||||
* to external processes. This method is a workaround.
|
|
||||||
* Keep in mind not to return a stack trace to user when in production.
|
|
||||||
* https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
|
||||||
*/
|
|
||||||
toJSON(): SerializedException {
|
|
||||||
return {
|
|
||||||
message: this.message,
|
|
||||||
code: this.code,
|
|
||||||
stack: this.stack,
|
|
||||||
correlationId: this.correlationId,
|
|
||||||
cause: JSON.stringify(this.cause),
|
|
||||||
metadata: this.metadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
/**
|
|
||||||
* Adding a `code` string with a custom status code for every
|
|
||||||
* exception is a good practice, since when that exception
|
|
||||||
* is transferred to another process `instanceof` check
|
|
||||||
* cannot be performed anymore so a `code` string is used instead.
|
|
||||||
* code constants can be stored in a separate file so they
|
|
||||||
* can be shared and reused on a receiving side (code sharing is
|
|
||||||
* useful when developing fullstack apps or microservices)
|
|
||||||
*/
|
|
||||||
export const ARGUMENT_INVALID = 'GENERIC.ARGUMENT_INVALID';
|
|
||||||
export const ARGUMENT_OUT_OF_RANGE = 'GENERIC.ARGUMENT_OUT_OF_RANGE';
|
|
||||||
export const ARGUMENT_NOT_PROVIDED = 'GENERIC.ARGUMENT_NOT_PROVIDED';
|
|
||||||
export const NOT_FOUND = 'GENERIC.NOT_FOUND';
|
|
||||||
export const CONFLICT = 'GENERIC.CONFLICT';
|
|
||||||
export const INTERNAL_SERVER_ERROR = 'GENERIC.INTERNAL_SERVER_ERROR';
|
|
||||||
export const DATABASE_ERROR = 'GENERIC.DATABASE_ERROR';
|
|
|
@ -1,99 +0,0 @@
|
||||||
import {
|
|
||||||
ARGUMENT_INVALID,
|
|
||||||
ARGUMENT_NOT_PROVIDED,
|
|
||||||
ARGUMENT_OUT_OF_RANGE,
|
|
||||||
CONFLICT,
|
|
||||||
DATABASE_ERROR,
|
|
||||||
INTERNAL_SERVER_ERROR,
|
|
||||||
NOT_FOUND,
|
|
||||||
} from '.';
|
|
||||||
import { ExceptionBase } from './exception.base';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to indicate that an incorrect argument was provided to a method/function/class constructor
|
|
||||||
*
|
|
||||||
* @class ArgumentInvalidException
|
|
||||||
* @extends {ExceptionBase}
|
|
||||||
*/
|
|
||||||
export class ArgumentInvalidException extends ExceptionBase {
|
|
||||||
readonly code = ARGUMENT_INVALID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to indicate that an argument was not provided (is empty object/array, null of undefined).
|
|
||||||
*
|
|
||||||
* @class ArgumentNotProvidedException
|
|
||||||
* @extends {ExceptionBase}
|
|
||||||
*/
|
|
||||||
export class ArgumentNotProvidedException extends ExceptionBase {
|
|
||||||
readonly code = ARGUMENT_NOT_PROVIDED;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to indicate that an argument is out of allowed range
|
|
||||||
* (for example: incorrect string/array length, number not in allowed min/max range etc)
|
|
||||||
*
|
|
||||||
* @class ArgumentOutOfRangeException
|
|
||||||
* @extends {ExceptionBase}
|
|
||||||
*/
|
|
||||||
export class ArgumentOutOfRangeException extends ExceptionBase {
|
|
||||||
readonly code = ARGUMENT_OUT_OF_RANGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to indicate conflicting entities (usually in the database)
|
|
||||||
*
|
|
||||||
* @class ConflictException
|
|
||||||
* @extends {ExceptionBase}
|
|
||||||
*/
|
|
||||||
export class ConflictException extends ExceptionBase {
|
|
||||||
readonly code = CONFLICT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to indicate that entity is not found
|
|
||||||
*
|
|
||||||
* @class NotFoundException
|
|
||||||
* @extends {ExceptionBase}
|
|
||||||
*/
|
|
||||||
export class NotFoundException extends ExceptionBase {
|
|
||||||
static readonly message = 'Not found';
|
|
||||||
|
|
||||||
constructor(message = NotFoundException.message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly code = NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to indicate an internal server error that does not fall under all other errors
|
|
||||||
*
|
|
||||||
* @class InternalServerErrorException
|
|
||||||
* @extends {ExceptionBase}
|
|
||||||
*/
|
|
||||||
export class InternalServerErrorException extends ExceptionBase {
|
|
||||||
static readonly message = 'Internal server error';
|
|
||||||
|
|
||||||
constructor(message = InternalServerErrorException.message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly code = INTERNAL_SERVER_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to indicate a database error
|
|
||||||
*
|
|
||||||
* @class DatabaseErrorException
|
|
||||||
* @extends {ExceptionBase}
|
|
||||||
*/
|
|
||||||
export class DatabaseErrorException extends ExceptionBase {
|
|
||||||
static readonly message = 'Database error';
|
|
||||||
|
|
||||||
constructor(message = DatabaseErrorException.message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly code = DATABASE_ERROR;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './exception.base';
|
|
||||||
export * from './exception.codes';
|
|
||||||
export * from './exceptions';
|
|
||||||
export * from './rpc-exception.codes.enum';
|
|
|
@ -1,19 +0,0 @@
|
||||||
export enum RpcExceptionCode {
|
|
||||||
OK = 0,
|
|
||||||
CANCELLED = 1,
|
|
||||||
UNKNOWN = 2,
|
|
||||||
INVALID_ARGUMENT = 3,
|
|
||||||
DEADLINE_EXCEEDED = 4,
|
|
||||||
NOT_FOUND = 5,
|
|
||||||
ALREADY_EXISTS = 6,
|
|
||||||
PERMISSION_DENIED = 7,
|
|
||||||
RESOURCE_EXHAUSTED = 8,
|
|
||||||
FAILED_PRECONDITION = 9,
|
|
||||||
ABORTED = 10,
|
|
||||||
OUT_OF_RANGE = 11,
|
|
||||||
UNIMPLEMENTED = 12,
|
|
||||||
INTERNAL = 13,
|
|
||||||
UNAVAILABLE = 14,
|
|
||||||
DATA_LOSS = 15,
|
|
||||||
UNAUTHENTICATED = 16,
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
export class Guard {
|
|
||||||
/**
|
|
||||||
* Checks if value is empty. Accepts strings, numbers, booleans, objects and arrays.
|
|
||||||
*/
|
|
||||||
static isEmpty(value: unknown): boolean {
|
|
||||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof value === 'undefined' || value === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (value instanceof Object && !Object.keys(value).length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (value.every((item) => Guard.isEmpty(item))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (value === '') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks length range of a provided number/string/array
|
|
||||||
*/
|
|
||||||
static lengthIsBetween(
|
|
||||||
value: number | string | Array<unknown>,
|
|
||||||
min: number,
|
|
||||||
max: number,
|
|
||||||
): boolean {
|
|
||||||
if (Guard.isEmpty(value)) {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot check length of a value. Provided value is empty',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const valueLength =
|
|
||||||
typeof value === 'number'
|
|
||||||
? Number(value).toString().length
|
|
||||||
: value.length;
|
|
||||||
if (valueLength >= min && valueLength <= max) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export interface LoggerPort {
|
|
||||||
log(message: string, ...meta: unknown[]): void;
|
|
||||||
error(message: string, trace?: unknown, ...meta: unknown[]): void;
|
|
||||||
warn(message: string, ...meta: unknown[]): void;
|
|
||||||
debug(message: string, ...meta: unknown[]): void;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export interface MessagePublisherPort {
|
|
||||||
publish(routingKey: string, message: string): void;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export interface PrismaRepositoryPort<Entity> {
|
|
||||||
findUnique(options: any): Promise<Entity>;
|
|
||||||
create(entity: any): Promise<Entity>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PrismaRawRepositoryPort {
|
|
||||||
$queryRaw<T = unknown>(
|
|
||||||
query: TemplateStringsArray,
|
|
||||||
...values: any[]
|
|
||||||
): Promise<T>;
|
|
||||||
}
|
|
|
@ -1,287 +0,0 @@
|
||||||
import { ResponseBase } from '@libs/api/response.base';
|
|
||||||
import { PrismaRepositoryBase } from '@libs/db/prisma-repository.base';
|
|
||||||
import { PrismaService } from '@libs/db/prisma.service';
|
|
||||||
import { AggregateID, AggregateRoot, Mapper, RepositoryPort } from '@libs/ddd';
|
|
||||||
import {
|
|
||||||
ConflictException,
|
|
||||||
DatabaseErrorException,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@libs/exceptions';
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
interface FakeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateFakeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeEntity extends AggregateRoot<FakeProps> {
|
|
||||||
protected readonly _id: AggregateID;
|
|
||||||
|
|
||||||
static create = (create: CreateFakeProps): FakeEntity => {
|
|
||||||
const id = v4();
|
|
||||||
const props: FakeProps = { ...create };
|
|
||||||
const fake = new FakeEntity({ id, props });
|
|
||||||
return fake;
|
|
||||||
};
|
|
||||||
|
|
||||||
validate(): void {
|
|
||||||
// not implemented
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FakeModel = {
|
|
||||||
uuid: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FakeRepositoryPort = RepositoryPort<FakeEntity>;
|
|
||||||
|
|
||||||
class FakeResponseDto extends ResponseBase {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeRecord: FakeModel = {
|
|
||||||
uuid: 'd567ea3b-4981-43c9-9449-a409b5fa9fed',
|
|
||||||
name: 'fakeName',
|
|
||||||
createdAt: new Date('2023-06-28T16:02:00Z'),
|
|
||||||
updatedAt: new Date('2023-06-28T16:02:00Z'),
|
|
||||||
};
|
|
||||||
|
|
||||||
let recordId = 2;
|
|
||||||
const recordUuid = 'uuid-';
|
|
||||||
const recordName = 'fakeName-';
|
|
||||||
|
|
||||||
const createRandomRecord = (): FakeModel => {
|
|
||||||
const fakeRecord: FakeModel = {
|
|
||||||
uuid: `${recordUuid}${recordId}`,
|
|
||||||
name: `${recordName}${recordId}`,
|
|
||||||
createdAt: new Date('2023-06-30T08:00:00Z'),
|
|
||||||
updatedAt: new Date('2023-06-30T08:00:00Z'),
|
|
||||||
};
|
|
||||||
|
|
||||||
recordId++;
|
|
||||||
|
|
||||||
return fakeRecord;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeRecords: FakeModel[] = [];
|
|
||||||
Array.from({ length: 10 }).forEach(() => {
|
|
||||||
fakeRecords.push(createRandomRecord());
|
|
||||||
});
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class FakeMapper
|
|
||||||
implements Mapper<FakeEntity, FakeModel, FakeModel, FakeResponseDto>
|
|
||||||
{
|
|
||||||
toPersistence = (entity: FakeEntity): FakeModel => {
|
|
||||||
const copy = entity.getProps();
|
|
||||||
const record: FakeModel = {
|
|
||||||
uuid: copy.id,
|
|
||||||
name: copy.name,
|
|
||||||
createdAt: copy.createdAt,
|
|
||||||
updatedAt: copy.updatedAt,
|
|
||||||
};
|
|
||||||
return record;
|
|
||||||
};
|
|
||||||
|
|
||||||
toDomain = (record: FakeModel): FakeEntity => {
|
|
||||||
const entity = new FakeEntity({
|
|
||||||
id: record.uuid,
|
|
||||||
createdAt: new Date(record.createdAt),
|
|
||||||
updatedAt: new Date(record.updatedAt),
|
|
||||||
props: {
|
|
||||||
name: record.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return entity;
|
|
||||||
};
|
|
||||||
|
|
||||||
toResponse = (entity: FakeEntity): FakeResponseDto => {
|
|
||||||
const props = entity.getProps();
|
|
||||||
const response = new FakeResponseDto(entity);
|
|
||||||
response.name = props.name;
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class FakePrismaService extends PrismaService {
|
|
||||||
fake: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockPrismaService = {
|
|
||||||
$queryRaw: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error();
|
|
||||||
}),
|
|
||||||
fake: {
|
|
||||||
create: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(fakeRecord)
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('Already exists', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error('An unknown error');
|
|
||||||
}),
|
|
||||||
|
|
||||||
findUnique: jest.fn().mockImplementation(async (params?: any) => {
|
|
||||||
let record: FakeModel;
|
|
||||||
|
|
||||||
if (params?.where?.uuid) {
|
|
||||||
record = fakeRecords.find(
|
|
||||||
(record) => record.uuid === params?.where?.uuid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!record && params?.where?.uuid == 'uuid-triggering-error') {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return record;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class FakeRepository
|
|
||||||
extends PrismaRepositoryBase<FakeEntity, FakeModel, FakeModel>
|
|
||||||
implements FakeRepositoryPort
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
prisma: FakePrismaService,
|
|
||||||
mapper: FakeMapper,
|
|
||||||
eventEmitter: EventEmitter2,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
prisma.fake,
|
|
||||||
prisma,
|
|
||||||
mapper,
|
|
||||||
eventEmitter,
|
|
||||||
new Logger(FakeRepository.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('PrismaRepositoryBase', () => {
|
|
||||||
let fakeRepository: FakeRepository;
|
|
||||||
let prisma: FakePrismaService;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
EventEmitter2,
|
|
||||||
FakeRepository,
|
|
||||||
FakeMapper,
|
|
||||||
{
|
|
||||||
provide: FakePrismaService,
|
|
||||||
useValue: mockPrismaService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
fakeRepository = module.get<FakeRepository>(FakeRepository);
|
|
||||||
prisma = module.get<FakePrismaService>(FakePrismaService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(fakeRepository).toBeDefined();
|
|
||||||
expect(prisma).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('insert', () => {
|
|
||||||
it('should create a record', async () => {
|
|
||||||
jest.spyOn(prisma.fake, 'create');
|
|
||||||
|
|
||||||
await fakeRepository.insert(
|
|
||||||
FakeEntity.create({
|
|
||||||
name: 'someFakeName',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(prisma.fake.create).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a ConflictException if record already exists', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.insert(
|
|
||||||
FakeEntity.create({
|
|
||||||
name: 'someFakeName',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(ConflictException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an Error if an error occurs', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.insert(
|
|
||||||
FakeEntity.create({
|
|
||||||
name: 'someFakeName',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOneById', () => {
|
|
||||||
it('should find a record by its id', async () => {
|
|
||||||
const record = await fakeRepository.findOneById('uuid-3');
|
|
||||||
expect(record.getProps().name).toBe('fakeName-3');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an Error for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneById('uuid-triggering-error'),
|
|
||||||
).rejects.toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a NotFoundException if id is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneById('wrong-id'),
|
|
||||||
).rejects.toBeInstanceOf(NotFoundException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('healthCheck', () => {
|
|
||||||
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(
|
|
||||||
DatabaseErrorException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseErrorException if an error occurs', async () => {
|
|
||||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
|
||||||
DatabaseErrorException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,76 +0,0 @@
|
||||||
import {
|
|
||||||
AggregateID,
|
|
||||||
AggregateRoot,
|
|
||||||
DomainEvent,
|
|
||||||
DomainEventProps,
|
|
||||||
} from '@libs/ddd';
|
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
interface FakeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateFakeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeRecordCreatedDomainEvent extends DomainEvent {
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
constructor(props: DomainEventProps<FakeRecordCreatedDomainEvent>) {
|
|
||||||
super(props);
|
|
||||||
this.name = props.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeEntity extends AggregateRoot<FakeProps> {
|
|
||||||
protected readonly _id: AggregateID;
|
|
||||||
|
|
||||||
static create = (create: CreateFakeProps): FakeEntity => {
|
|
||||||
const id = v4();
|
|
||||||
const props: FakeProps = { ...create };
|
|
||||||
const fake = new FakeEntity({ id, props });
|
|
||||||
fake.addEvent(
|
|
||||||
new FakeRecordCreatedDomainEvent({
|
|
||||||
aggregateId: id,
|
|
||||||
name: props.name,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return fake;
|
|
||||||
};
|
|
||||||
|
|
||||||
validate(): void {
|
|
||||||
// not implemented
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockLogger = {
|
|
||||||
debug: jest.fn(),
|
|
||||||
log: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('AggregateRoot Base', () => {
|
|
||||||
it('should define an aggregate root based object instance', () => {
|
|
||||||
const fakeInstance = FakeEntity.create({
|
|
||||||
name: 'someFakeName',
|
|
||||||
});
|
|
||||||
expect(fakeInstance).toBeDefined();
|
|
||||||
expect(fakeInstance.domainEvents.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should publish domain events', async () => {
|
|
||||||
jest.spyOn(mockLogger, 'debug');
|
|
||||||
const eventEmitter = new EventEmitter2();
|
|
||||||
jest.spyOn(eventEmitter, 'emitAsync');
|
|
||||||
const fakeInstance = FakeEntity.create({
|
|
||||||
name: 'someFakeName',
|
|
||||||
});
|
|
||||||
await fakeInstance.publishEvents(mockLogger, eventEmitter);
|
|
||||||
expect(mockLogger.debug).toHaveBeenCalledTimes(1);
|
|
||||||
expect(eventEmitter.emitAsync).toHaveBeenCalledTimes(1);
|
|
||||||
expect(fakeInstance.domainEvents.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { Command, CommandProps } from '@libs/ddd';
|
|
||||||
import { ArgumentNotProvidedException } from '@libs/exceptions';
|
|
||||||
|
|
||||||
class FakeCommand extends Command {
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
constructor(props: CommandProps<FakeCommand>) {
|
|
||||||
super(props);
|
|
||||||
this.name = props.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BadFakeCommand extends Command {
|
|
||||||
constructor(props: CommandProps<BadFakeCommand>) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Command Base', () => {
|
|
||||||
it('should define a command based object instance', () => {
|
|
||||||
const fakeCommand = new FakeCommand({ name: 'fakeName' });
|
|
||||||
expect(fakeCommand).toBeDefined();
|
|
||||||
expect(fakeCommand.id.length).toBe(36);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should define a command based object instance with a provided id', () => {
|
|
||||||
const fakeCommand = new FakeCommand({ id: 'some-id', name: 'fakeName' });
|
|
||||||
expect(fakeCommand.id).toBe('some-id');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should define a command based object instance with metadata', () => {
|
|
||||||
const fakeCommand = new FakeCommand({
|
|
||||||
name: 'fakeName',
|
|
||||||
metadata: {
|
|
||||||
correlationId: 'some-correlation-id',
|
|
||||||
causationId: 'some-causation-id',
|
|
||||||
userId: 'some-user-id',
|
|
||||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(fakeCommand.metadata.timestamp).toBe(1687928400000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an exception if props are empty', () => {
|
|
||||||
expect(() => new BadFakeCommand({})).toThrow(ArgumentNotProvidedException);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { DomainEvent, DomainEventProps } from '@libs/ddd';
|
|
||||||
import { ArgumentNotProvidedException } from '@libs/exceptions';
|
|
||||||
|
|
||||||
class FakeDomainEvent extends DomainEvent {
|
|
||||||
readonly name: string;
|
|
||||||
|
|
||||||
constructor(props: DomainEventProps<FakeDomainEvent>) {
|
|
||||||
super(props);
|
|
||||||
this.name = props.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DomainEvent Base', () => {
|
|
||||||
it('should define a domain event based object instance', () => {
|
|
||||||
const fakeDomainEvent = new FakeDomainEvent({
|
|
||||||
aggregateId: 'some-id',
|
|
||||||
name: 'some-name',
|
|
||||||
});
|
|
||||||
expect(fakeDomainEvent).toBeDefined();
|
|
||||||
expect(fakeDomainEvent.id.length).toBe(36);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should define a domain event based object instance with metadata', () => {
|
|
||||||
const fakeDomainEvent = new FakeDomainEvent({
|
|
||||||
aggregateId: 'some-id',
|
|
||||||
name: 'some-name',
|
|
||||||
metadata: {
|
|
||||||
correlationId: 'some-correlation-id',
|
|
||||||
causationId: 'some-causation-id',
|
|
||||||
userId: 'some-user-id',
|
|
||||||
timestamp: new Date('2023-06-28T05:00:00Z').getTime(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(fakeDomainEvent.metadata.timestamp).toBe(1687928400000);
|
|
||||||
});
|
|
||||||
it('should throw an exception if props are empty', () => {
|
|
||||||
const emptyProps: DomainEventProps<FakeDomainEvent> = undefined;
|
|
||||||
expect(() => new FakeDomainEvent(emptyProps)).toThrow(
|
|
||||||
ArgumentNotProvidedException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,209 +0,0 @@
|
||||||
import { Entity } from '@libs/ddd';
|
|
||||||
import { ArgumentOutOfRangeException } from '@libs/exceptions';
|
|
||||||
|
|
||||||
interface FakeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeEntity extends Entity<FakeProps> {
|
|
||||||
protected _id: string;
|
|
||||||
|
|
||||||
validate(): void {
|
|
||||||
// not implemented
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Entity Base', () => {
|
|
||||||
it('should define an entity based object instance', () => {
|
|
||||||
const fakeInstance = new FakeEntity({
|
|
||||||
id: 'some-id',
|
|
||||||
props: {
|
|
||||||
name: 'some-name',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(fakeInstance).toBeDefined();
|
|
||||||
expect(fakeInstance.id).toBe('some-id');
|
|
||||||
expect(fakeInstance.createdAt).toBeInstanceOf(Date);
|
|
||||||
expect(fakeInstance.updatedAt).toBeInstanceOf(Date);
|
|
||||||
expect(FakeEntity.isEntity(fakeInstance)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should define an entity with given created and updated dates', () => {
|
|
||||||
const fakeInstance = new FakeEntity({
|
|
||||||
id: 'some-id',
|
|
||||||
createdAt: new Date('2023-06-28T05:00:00Z'),
|
|
||||||
updatedAt: new Date('2023-06-28T06:00:00Z'),
|
|
||||||
props: {
|
|
||||||
name: 'some-name',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(fakeInstance.createdAt.getUTCHours()).toBe(5);
|
|
||||||
expect(fakeInstance.updatedAt.getUTCHours()).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should compare entities', () => {
|
|
||||||
const fakeInstance = new FakeEntity({
|
|
||||||
id: 'some-id',
|
|
||||||
props: {
|
|
||||||
name: 'some-name',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const fakeInstanceClone = new FakeEntity({
|
|
||||||
id: 'some-id',
|
|
||||||
props: {
|
|
||||||
name: 'some-name',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const fakeInstanceNotReallyClone = new FakeEntity({
|
|
||||||
id: 'some-slightly-different-id',
|
|
||||||
props: {
|
|
||||||
name: 'some-name',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const undefinedFakeInstance: FakeEntity = undefined;
|
|
||||||
expect(fakeInstance.equals(undefinedFakeInstance)).toBeFalsy();
|
|
||||||
expect(fakeInstance.equals(fakeInstance)).toBeTruthy();
|
|
||||||
expect(fakeInstance.equals(fakeInstanceClone)).toBeTruthy();
|
|
||||||
expect(fakeInstance.equals(fakeInstanceNotReallyClone)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert entity to plain object', () => {
|
|
||||||
const fakeInstance = new FakeEntity({
|
|
||||||
id: 'some-id',
|
|
||||||
createdAt: new Date('2023-06-28T05:00:00Z'),
|
|
||||||
updatedAt: new Date('2023-06-28T06:00:00Z'),
|
|
||||||
props: {
|
|
||||||
name: 'some-name',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(fakeInstance.toObject()).toEqual({
|
|
||||||
id: 'some-id',
|
|
||||||
createdAt: new Date('2023-06-28T05:00:00.000Z'),
|
|
||||||
updatedAt: new Date('2023-06-28T06:00:00.000Z'),
|
|
||||||
name: 'some-name',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an exception if props number is too high', () => {
|
|
||||||
interface BigFakeProps {
|
|
||||||
prop1: string;
|
|
||||||
prop2: string;
|
|
||||||
prop3: string;
|
|
||||||
prop4: string;
|
|
||||||
prop5: string;
|
|
||||||
prop6: string;
|
|
||||||
prop7: string;
|
|
||||||
prop8: string;
|
|
||||||
prop9: string;
|
|
||||||
prop10: string;
|
|
||||||
prop11: string;
|
|
||||||
prop12: string;
|
|
||||||
prop13: string;
|
|
||||||
prop14: string;
|
|
||||||
prop15: string;
|
|
||||||
prop16: string;
|
|
||||||
prop17: string;
|
|
||||||
prop18: string;
|
|
||||||
prop19: string;
|
|
||||||
prop20: string;
|
|
||||||
prop21: string;
|
|
||||||
prop22: string;
|
|
||||||
prop23: string;
|
|
||||||
prop24: string;
|
|
||||||
prop25: string;
|
|
||||||
prop26: string;
|
|
||||||
prop27: string;
|
|
||||||
prop28: string;
|
|
||||||
prop29: string;
|
|
||||||
prop30: string;
|
|
||||||
prop31: string;
|
|
||||||
prop32: string;
|
|
||||||
prop33: string;
|
|
||||||
prop34: string;
|
|
||||||
prop35: string;
|
|
||||||
prop36: string;
|
|
||||||
prop37: string;
|
|
||||||
prop38: string;
|
|
||||||
prop39: string;
|
|
||||||
prop40: string;
|
|
||||||
prop41: string;
|
|
||||||
prop42: string;
|
|
||||||
prop43: string;
|
|
||||||
prop44: string;
|
|
||||||
prop45: string;
|
|
||||||
prop46: string;
|
|
||||||
prop47: string;
|
|
||||||
prop48: string;
|
|
||||||
prop49: string;
|
|
||||||
prop50: string;
|
|
||||||
prop51: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BigFakeEntity extends Entity<BigFakeProps> {
|
|
||||||
protected _id: string;
|
|
||||||
|
|
||||||
validate(): void {
|
|
||||||
// not implemented
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(
|
|
||||||
() =>
|
|
||||||
new BigFakeEntity({
|
|
||||||
id: 'some-id',
|
|
||||||
props: {
|
|
||||||
prop1: 'some-name',
|
|
||||||
prop2: 'some-name',
|
|
||||||
prop3: 'some-name',
|
|
||||||
prop4: 'some-name',
|
|
||||||
prop5: 'some-name',
|
|
||||||
prop6: 'some-name',
|
|
||||||
prop7: 'some-name',
|
|
||||||
prop8: 'some-name',
|
|
||||||
prop9: 'some-name',
|
|
||||||
prop10: 'some-name',
|
|
||||||
prop11: 'some-name',
|
|
||||||
prop12: 'some-name',
|
|
||||||
prop13: 'some-name',
|
|
||||||
prop14: 'some-name',
|
|
||||||
prop15: 'some-name',
|
|
||||||
prop16: 'some-name',
|
|
||||||
prop17: 'some-name',
|
|
||||||
prop18: 'some-name',
|
|
||||||
prop19: 'some-name',
|
|
||||||
prop20: 'some-name',
|
|
||||||
prop21: 'some-name',
|
|
||||||
prop22: 'some-name',
|
|
||||||
prop23: 'some-name',
|
|
||||||
prop24: 'some-name',
|
|
||||||
prop25: 'some-name',
|
|
||||||
prop26: 'some-name',
|
|
||||||
prop27: 'some-name',
|
|
||||||
prop28: 'some-name',
|
|
||||||
prop29: 'some-name',
|
|
||||||
prop30: 'some-name',
|
|
||||||
prop31: 'some-name',
|
|
||||||
prop32: 'some-name',
|
|
||||||
prop33: 'some-name',
|
|
||||||
prop34: 'some-name',
|
|
||||||
prop35: 'some-name',
|
|
||||||
prop36: 'some-name',
|
|
||||||
prop37: 'some-name',
|
|
||||||
prop38: 'some-name',
|
|
||||||
prop39: 'some-name',
|
|
||||||
prop40: 'some-name',
|
|
||||||
prop41: 'some-name',
|
|
||||||
prop42: 'some-name',
|
|
||||||
prop43: 'some-name',
|
|
||||||
prop44: 'some-name',
|
|
||||||
prop45: 'some-name',
|
|
||||||
prop46: 'some-name',
|
|
||||||
prop47: 'some-name',
|
|
||||||
prop48: 'some-name',
|
|
||||||
prop49: 'some-name',
|
|
||||||
prop50: 'some-name',
|
|
||||||
prop51: 'some-name',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toThrow(ArgumentOutOfRangeException);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,40 +0,0 @@
|
||||||
import {
|
|
||||||
PaginatedParams,
|
|
||||||
PaginatedQueryBase,
|
|
||||||
QueryBase,
|
|
||||||
} from '@libs/ddd/query.base';
|
|
||||||
|
|
||||||
class FakeQuery extends QueryBase {
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
constructor(id: string) {
|
|
||||||
super();
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Query Base', () => {
|
|
||||||
it('should define a query based object instance', () => {
|
|
||||||
const fakeQuery = new FakeQuery('some-id');
|
|
||||||
expect(fakeQuery).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
class FakePaginatedQuery extends PaginatedQueryBase {
|
|
||||||
readonly id: string;
|
|
||||||
|
|
||||||
constructor(props: PaginatedParams<FakePaginatedQuery>) {
|
|
||||||
super(props);
|
|
||||||
this.id = props.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Paginated Query Base', () => {
|
|
||||||
it('should define a paginated query based object instance', () => {
|
|
||||||
const fakePaginatedQuery = new FakePaginatedQuery({
|
|
||||||
id: 'some-id',
|
|
||||||
page: 1,
|
|
||||||
});
|
|
||||||
expect(fakePaginatedQuery).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { ValueObject } from '@libs/ddd';
|
|
||||||
|
|
||||||
interface FakeProps {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeValueObject extends ValueObject<FakeProps> {
|
|
||||||
get name(): string {
|
|
||||||
return this.props.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
protected validate(props: FakeProps): void {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Value Object Base', () => {
|
|
||||||
it('should create a base value object', () => {
|
|
||||||
const fakeValueObject = new FakeValueObject({ name: 'fakeName' });
|
|
||||||
expect(fakeValueObject).toBeDefined();
|
|
||||||
expect(ValueObject.isValueObject(fakeValueObject)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should compare value objects', () => {
|
|
||||||
const fakeValueObject = new FakeValueObject({ name: 'fakeName' });
|
|
||||||
const fakeValueObjectClone = new FakeValueObject({ name: 'fakeName' });
|
|
||||||
const undefinedFakeValueObject: FakeValueObject = undefined;
|
|
||||||
const nullFakeValueObject: FakeValueObject = null;
|
|
||||||
expect(fakeValueObject.equals(undefinedFakeValueObject)).toBeFalsy();
|
|
||||||
expect(fakeValueObject.equals(nullFakeValueObject)).toBeFalsy();
|
|
||||||
expect(fakeValueObject.equals(fakeValueObject)).toBeTruthy();
|
|
||||||
expect(fakeValueObject.equals(fakeValueObjectClone)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should unpack value object props', () => {
|
|
||||||
const fakeValueObject = new FakeValueObject({ name: 'fakeName' });
|
|
||||||
expect(fakeValueObject.unpack()).toEqual({
|
|
||||||
name: 'fakeName',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { Guard } from '@libs/guard';
|
|
||||||
|
|
||||||
describe('Guard', () => {
|
|
||||||
describe('isEmpty', () => {
|
|
||||||
it('should return false for a number', () => {
|
|
||||||
expect(Guard.isEmpty(1)).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return false for a falsy boolean', () => {
|
|
||||||
expect(Guard.isEmpty(false)).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return false for a truthy boolean', () => {
|
|
||||||
expect(Guard.isEmpty(true)).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return true for undefined', () => {
|
|
||||||
expect(Guard.isEmpty(undefined)).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return true for null', () => {
|
|
||||||
expect(Guard.isEmpty(null)).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return false for a Date', () => {
|
|
||||||
expect(Guard.isEmpty(new Date('2023-06-28'))).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return false for an object with keys', () => {
|
|
||||||
expect(Guard.isEmpty({ key: 'value' })).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return true for an object without keys', () => {
|
|
||||||
expect(Guard.isEmpty({})).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return true for an array without values', () => {
|
|
||||||
expect(Guard.isEmpty([])).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return true for an array with only empty values', () => {
|
|
||||||
expect(Guard.isEmpty([null, undefined])).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return false for an array with some empty values', () => {
|
|
||||||
expect(Guard.isEmpty([1, null, undefined])).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return true for an empty string', () => {
|
|
||||||
expect(Guard.isEmpty('')).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('lengthIsBetween', () => {
|
|
||||||
it('should return true for a string in the range', () => {
|
|
||||||
expect(Guard.lengthIsBetween('test', 0, 4)).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return true for a number in the range', () => {
|
|
||||||
expect(Guard.lengthIsBetween(2, 0, 4)).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return true for an array with number of elements in the range', () => {
|
|
||||||
expect(Guard.lengthIsBetween([1, 2, 3], 0, 4)).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should return false for a string not in the range', () => {
|
|
||||||
expect(Guard.lengthIsBetween('test', 5, 9)).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return false for a number not in the range', () => {
|
|
||||||
expect(Guard.lengthIsBetween(2, 3, 6)).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should return false for an array with number of elements not in the range', () => {
|
|
||||||
expect(Guard.lengthIsBetween([1, 2, 3], 10, 12)).toBeFalsy();
|
|
||||||
});
|
|
||||||
it('should throw an exception if value is empty', () => {
|
|
||||||
expect(() => Guard.lengthIsBetween(undefined, 0, 4)).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { ArgumentMetadata } from '@nestjs/common';
|
|
||||||
import { FindAdByIdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/find-ad-by-id.request.dto';
|
|
||||||
import { RpcValidationPipe } from '@libs/utils/pipes/rpc.validation-pipe';
|
|
||||||
|
|
||||||
describe('RpcValidationPipe', () => {
|
|
||||||
it('should not validate request', async () => {
|
|
||||||
const target: RpcValidationPipe = new RpcValidationPipe({
|
|
||||||
whitelist: true,
|
|
||||||
forbidUnknownValues: false,
|
|
||||||
});
|
|
||||||
const metadata: ArgumentMetadata = {
|
|
||||||
type: 'body',
|
|
||||||
metatype: FindAdByIdRequestDto,
|
|
||||||
data: '',
|
|
||||||
};
|
|
||||||
await target.transform(<FindAdByIdRequestDto>{}, metadata).catch((err) => {
|
|
||||||
expect(err.message).toEqual('Rpc Exception');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,6 +0,0 @@
|
||||||
/** Consider creating a bunch of shared custom utility
|
|
||||||
* types for different situations.
|
|
||||||
* Alternatively you can use a library like
|
|
||||||
* https://github.com/andnp/SimplyTyped
|
|
||||||
*/
|
|
||||||
export * from './object-literal.type';
|
|
|
@ -1,6 +0,0 @@
|
||||||
/**
|
|
||||||
* Interface of the simple literal object with any string keys.
|
|
||||||
*/
|
|
||||||
export interface ObjectLiteral {
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
import { Entity } from '../ddd/entity.base';
|
|
||||||
import { ValueObject } from '../ddd/value-object.base';
|
|
||||||
|
|
||||||
function isEntity(obj: unknown): obj is Entity<unknown> {
|
|
||||||
/**
|
|
||||||
* 'instanceof Entity' causes error here for some reason.
|
|
||||||
* Probably creates some circular dependency. This is a workaround
|
|
||||||
* until I find a solution :)
|
|
||||||
*/
|
|
||||||
return (
|
|
||||||
Object.prototype.hasOwnProperty.call(obj, 'toObject') &&
|
|
||||||
Object.prototype.hasOwnProperty.call(obj, 'id') &&
|
|
||||||
ValueObject.isValueObject((obj as Entity<unknown>).id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToPlainObject(item: any): any {
|
|
||||||
if (ValueObject.isValueObject(item)) {
|
|
||||||
return item.unpack();
|
|
||||||
}
|
|
||||||
if (isEntity(item)) {
|
|
||||||
return item.toObject();
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts Entity/Value Objects props to a plain object.
|
|
||||||
* Useful for testing and debugging.
|
|
||||||
* @param props
|
|
||||||
*/
|
|
||||||
export function convertPropsToObject(props: any): any {
|
|
||||||
const propsCopy = structuredClone(props);
|
|
||||||
|
|
||||||
// eslint-disable-next-line guard-for-in
|
|
||||||
for (const prop in propsCopy) {
|
|
||||||
if (Array.isArray(propsCopy[prop])) {
|
|
||||||
propsCopy[prop] = (propsCopy[prop] as Array<unknown>).map((item) => {
|
|
||||||
return convertToPlainObject(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
propsCopy[prop] = convertToPlainObject(propsCopy[prop]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return propsCopy;
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './convert-props-to-object.util';
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { RpcExceptionCode } from '@libs/exceptions/rpc-exception.codes.enum';
|
|
||||||
import { Injectable, ValidationPipe } from '@nestjs/common';
|
|
||||||
import { RpcException } from '@nestjs/microservices';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RpcValidationPipe extends ValidationPipe {
|
|
||||||
createExceptionFactory() {
|
|
||||||
return (validationErrors = []) => {
|
|
||||||
return new RpcException({
|
|
||||||
code: RpcExceptionCode.INVALID_ARGUMENT,
|
|
||||||
message: this.flattenValidationErrors(validationErrors),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Mapper } from '@libs/ddd';
|
import { Mapper } from '@mobicoop/ddd-library';
|
||||||
import { AdResponseDto } from './interface/dtos/ad.response.dto';
|
import { AdResponseDto } from './interface/dtos/ad.response.dto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AdEntity } from './core/domain/ad.entity';
|
import { AdEntity } from './core/domain/ad.entity';
|
||||||
|
|
|
@ -18,12 +18,12 @@ import { MessagePublisher } from './infrastructure/message-publisher';
|
||||||
import { AdMapper } from './ad.mapper';
|
import { AdMapper } from './ad.mapper';
|
||||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||||
import { PrismaService } from '@libs/db/prisma.service';
|
|
||||||
import { TimeConverter } from './infrastructure/time-converter';
|
import { TimeConverter } from './infrastructure/time-converter';
|
||||||
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
||||||
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
||||||
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
|
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
|
||||||
import { PublishLogMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-log-message-when-ad-is-created.domain-event-handler';
|
import { PublishLogMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-log-message-when-ad-is-created.domain-event-handler';
|
||||||
|
import { PrismaService } from './infrastructure/prisma.service';
|
||||||
|
|
||||||
const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
|
const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Command, CommandProps } from '@libs/ddd';
|
|
||||||
import { Schedule } from '../../types/schedule';
|
import { Schedule } from '../../types/schedule';
|
||||||
import { MarginDurations } from '../../types/margin-durations';
|
import { MarginDurations } from '../../types/margin-durations';
|
||||||
import { Waypoint } from '../../types/waypoint';
|
import { Waypoint } from '../../types/waypoint';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
export class CreateAdCommand extends Command {
|
export class CreateAdCommand extends Command {
|
||||||
readonly userId: string;
|
readonly userId: string;
|
||||||
|
|
|
@ -2,14 +2,13 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
import { CreateAdCommand } from './create-ad.command';
|
import { CreateAdCommand } from './create-ad.command';
|
||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { AggregateID } from '@libs/ddd';
|
|
||||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { ConflictException } from '@libs/exceptions';
|
|
||||||
import { Waypoint } from '../../types/waypoint';
|
import { Waypoint } from '../../types/waypoint';
|
||||||
import { DefaultParams } from '../../ports/default-params.type';
|
import { DefaultParams } from '../../ports/default-params.type';
|
||||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||||
import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port';
|
import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
|
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@CommandHandler(CreateAdCommand)
|
@CommandHandler(CreateAdCommand)
|
||||||
export class CreateAdService implements ICommandHandler {
|
export class CreateAdService implements ICommandHandler {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { MessagePublisherPort } from '@libs/ports/message-publisher.port';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
||||||
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
|
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
|
||||||
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PublishLogMessageWhenAdIsCreatedDomainEventHandler {
|
export class PublishLogMessageWhenAdIsCreatedDomainEventHandler {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { MessagePublisherPort } from '@libs/ports/message-publisher.port';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
||||||
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
|
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
|
||||||
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PublishMessageWhenAdIsCreatedDomainEventHandler {
|
export class PublishMessageWhenAdIsCreatedDomainEventHandler {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RepositoryPort } from '@libs/ddd';
|
import { RepositoryPort } from '@mobicoop/ddd-library';
|
||||||
import { AdEntity } from '../../domain/ad.entity';
|
import { AdEntity } from '../../domain/ad.entity';
|
||||||
|
|
||||||
export type AdRepositoryPort = RepositoryPort<AdEntity>;
|
export type AdRepositoryPort = RepositoryPort<AdEntity>;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { QueryBase } from '@libs/ddd/query.base';
|
import { QueryBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
export class FindAdByIdQuery extends QueryBase {
|
export class FindAdByIdQuery extends QueryBase {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AggregateRoot, AggregateID } from '@libs/ddd';
|
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
|
import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
|
||||||
import { AdProps, CreateAdProps, DefaultAdProps } from './ad.types';
|
import { AdProps, CreateAdProps, DefaultAdProps } from './ad.types';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ExceptionBase } from '@libs/exceptions';
|
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
export class AdAlreadyExistsException extends ExceptionBase {
|
export class AdAlreadyExistsException extends ExceptionBase {
|
||||||
static readonly message = 'Ad already exists';
|
static readonly message = 'Ad already exists';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { DomainEvent, DomainEventProps } from '@libs/ddd';
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
export class AdCreatedDomainEvent extends DomainEvent {
|
export class AdCreatedDomainEvent extends DomainEvent {
|
||||||
readonly userId: string;
|
readonly userId: string;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ValueObject } from '@libs/ddd';
|
import { ValueObject } from '@mobicoop/ddd-library';
|
||||||
import { CoordinatesProps } from './coordinates.value-object';
|
import { CoordinatesProps } from './coordinates.value-object';
|
||||||
|
|
||||||
/** Note:
|
/** Note:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ValueObject } from '@libs/ddd';
|
import { ValueObject } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
/** Note:
|
/** Note:
|
||||||
* Value Objects with multiple properties can contain
|
* Value Objects with multiple properties can contain
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ValueObject } from '@libs/ddd';
|
import { ValueObject } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
/** Note:
|
/** Note:
|
||||||
* Value Objects with multiple properties can contain
|
* Value Objects with multiple properties can contain
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ValueObject } from '@libs/ddd';
|
import { ValueObject } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
/** Note:
|
/** Note:
|
||||||
* Value Objects with multiple properties can contain
|
* Value Objects with multiple properties can contain
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ValueObject } from '@libs/ddd';
|
import { ValueObject } from '@mobicoop/ddd-library';
|
||||||
import { AddressProps } from './address.value-object';
|
import { AddressProps } from './address.value-object';
|
||||||
|
|
||||||
/** Note:
|
/** Note:
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { AdEntity } from '../core/domain/ad.entity';
|
import { AdEntity } from '../core/domain/ad.entity';
|
||||||
import { PrismaService } from '@libs/db/prisma.service';
|
|
||||||
import { AdMapper } from '../ad.mapper';
|
import { AdMapper } from '../ad.mapper';
|
||||||
import { PrismaRepositoryBase } from '@libs/db/prisma-repository.base';
|
|
||||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||||
|
import { PrismaRepositoryBase } from '@mobicoop/ddd-library';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
export type AdBaseModel = {
|
export type AdBaseModel = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
import { MESSAGE_BROKER_PUBLISHER } from '@src/app.constants';
|
import { MESSAGE_BROKER_PUBLISHER } from '@src/app.constants';
|
||||||
import { MessagePublisherPort } from '@libs/ports/message-publisher.port';
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessagePublisher implements MessagePublisherPort {
|
export class MessagePublisher implements MessagePublisherPort {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PaginatedResponseDto } from '@libs/api/paginated.response.base';
|
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
||||||
import { AdResponseDto } from './ad.response.dto';
|
import { AdResponseDto } from './ad.response.dto';
|
||||||
|
|
||||||
export class AdPaginatedResponseDto extends PaginatedResponseDto<AdResponseDto> {
|
export class AdPaginatedResponseDto extends PaginatedResponseDto<AdResponseDto> {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ResponseBase } from '@libs/api/response.base';
|
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
|
||||||
export class AdResponseDto extends ResponseBase {
|
export class AdResponseDto extends ResponseBase {
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { Controller, UsePipes } from '@nestjs/common';
|
||||||
import { CommandBus } from '@nestjs/cqrs';
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
import { CreateAdRequestDto } from './dtos/create-ad.request.dto';
|
import { CreateAdRequestDto } from './dtos/create-ad.request.dto';
|
||||||
import { AggregateID } from '@libs/ddd';
|
import { AggregateID } from '@mobicoop/ddd-library';
|
||||||
import { IdResponse } from '@libs/api/id.response.dto';
|
import { IdResponse } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@libs/exceptions/rpc-exception.codes.enum';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { RpcValidationPipe } from '@libs/utils/pipes/rpc.validation-pipe';
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by
|
||||||
import { AdResponseDto } from '../dtos/ad.response.dto';
|
import { AdResponseDto } from '../dtos/ad.response.dto';
|
||||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
import { NotFoundException } from '@libs/exceptions';
|
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@libs/exceptions/rpc-exception.codes.enum';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { RpcValidationPipe } from '@libs/utils/pipes/rpc.validation-pipe';
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@UsePipes(
|
@UsePipes(
|
||||||
new RpcValidationPipe({
|
new RpcValidationPipe({
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { PrismaService } from '@libs/db/prisma.service';
|
|
||||||
import {
|
import {
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
PARAMS_PROVIDER,
|
PARAMS_PROVIDER,
|
||||||
|
@ -8,6 +7,7 @@ import {
|
||||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider';
|
import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider';
|
||||||
|
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
|
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
|
||||||
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
|
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
||||||
import { AggregateID } from '@libs/ddd';
|
import { AggregateID } from '@mobicoop/ddd-library';
|
||||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { ConflictException } from '@libs/exceptions';
|
import { ConflictException } from '@mobicoop/ddd-library';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
|
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
|
||||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { PrismaService } from '@libs/db/prisma.service';
|
|
||||||
import {
|
import {
|
||||||
PARAMS_PROVIDER,
|
PARAMS_PROVIDER,
|
||||||
TIMEZONE_FINDER,
|
TIMEZONE_FINDER,
|
||||||
|
@ -9,6 +8,7 @@ import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/de
|
||||||
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
|
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
|
||||||
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
|
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
|
||||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
|
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { IdResponse } from '@libs/api/id.response.dto';
|
import { IdResponse } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@libs/exceptions/rpc-exception.codes.enum';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller';
|
import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { NotFoundException } from '@libs/exceptions';
|
import { NotFoundException } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@libs/exceptions/rpc-exception.codes.enum';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { FindAdByIdGrpcController } from '@modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
import { FindAdByIdGrpcController } from '@modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { AD_REPOSITORY } from '@modules/health/health.di-tokens';
|
||||||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
||||||
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
||||||
import { LOGGING_AD_HEALTH_CRIT } from '@modules/health/health.constants';
|
import { LOGGING_AD_HEALTH_CRIT } from '@modules/health/health.constants';
|
||||||
import { MessagePublisherPort } from '@libs/ports/message-publisher.port';
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RepositoriesHealthIndicatorUseCase extends HealthIndicator {
|
export class RepositoriesHealthIndicatorUseCase extends HealthIndicator {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../app.constants';
|
import { MESSAGE_BROKER_PUBLISHER } from '../../../app.constants';
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
import { MessagePublisherPort } from '@libs/ports/message-publisher.port';
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessagePublisher implements MessagePublisherPort {
|
export class MessagePublisher implements MessagePublisherPort {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
|
||||||
import { RepositoriesHealthIndicatorUseCase } from '../../core/aplication/usecases/repositories.health-indicator.usecase';
|
import { RepositoriesHealthIndicatorUseCase } from '../../core/aplication/usecases/repositories.health-indicator.usecase';
|
||||||
import { AD_REPOSITORY } from '@modules/health/health.di-tokens';
|
import { AD_REPOSITORY } from '@modules/health/health.di-tokens';
|
||||||
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
import { MESSAGE_PUBLISHER } from '@src/app.constants';
|
||||||
import { DatabaseErrorException } from '@libs/exceptions';
|
import { DatabaseErrorException } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
const mockAdRepository = {
|
const mockAdRepository = {
|
||||||
healthCheck: jest
|
healthCheck: jest
|
||||||
|
|
Loading…
Reference in New Issue