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