Menyelesaikan object user dan init base
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
modules/
|
||||
|
||||
Penjelasan singkat:
|
||||
Folder modules berisi domain modules. Setiap module merepresentasikan satu domain bisnis dan mengikuti prinsip Clean Architecture dengan folder:
|
||||
- presentation/ # controllers, dto, response
|
||||
- application/ # use-cases, commands, queries
|
||||
- domain/ # entities, value objects, repository interfaces, domain services, events
|
||||
- infrastructure/ # prisma repositories, adapter implementations
|
||||
|
||||
Contoh module: identity
|
||||
- src/modules/identity/presentation/controllers/users.controller.ts
|
||||
- src/modules/identity/application/services/get-user.service.ts
|
||||
- src/modules/identity/domain/entities/user.entity.ts
|
||||
- src/modules/identity/infrastructure/repositories/user.repository.ts
|
||||
|
||||
Panduan singkat:
|
||||
- Controller tipis: lakukan validasi dan panggil application service.
|
||||
- Business logic harus berada di application/domain.
|
||||
- Infrastruktur mengimplementasikan interface domain (dependency inversion).
|
||||
- Gunakan event untuk komunikasi lintas module bila memungkinkan.
|
||||
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const Permissions = (...permissions: string[]) =>
|
||||
SetMetadata('permissions', permissions);
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const permissions =
|
||||
this.reflector.getAllAndOverride<string[]>(
|
||||
'permissions',
|
||||
[
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
],
|
||||
);
|
||||
|
||||
if (!permissions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const user = request.user;
|
||||
|
||||
return permissions.every(permission =>
|
||||
user.permissions.includes(permission),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
modules/identity/application/
|
||||
|
||||
Penjelasan:
|
||||
Layer application berisi use-case (services/commands/queries) yang mengorkestrasi domain dan infrastruktur.
|
||||
|
||||
Contoh file:
|
||||
- services/get-user.service.ts
|
||||
- commands/create-user.command.ts
|
||||
|
||||
Aturan:
|
||||
- Application service boleh memanggil repository interface, domain services, dan event publisher.
|
||||
- Application menangani transaksi jika diperlukan.
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
Injectable,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { CreateUserDto } from '../../presentation/dto/create-user.dto';
|
||||
import { User } from '../../domain/entities/user.entity';
|
||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
||||
|
||||
@Injectable()
|
||||
export class CreateUserHandler {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateUserDto): Promise<User> {
|
||||
const exists = await this.userRepository.existsByEmail(dto.email);
|
||||
|
||||
if (exists) {
|
||||
throw new ConflictException('Email already exists.');
|
||||
}
|
||||
|
||||
const user = User.create({
|
||||
name: dto.name,
|
||||
email: dto.email,
|
||||
password: dto.password,
|
||||
metadata: dto.metadata,
|
||||
});
|
||||
|
||||
await this.userRepository.create(user);
|
||||
|
||||
// TODO:
|
||||
// this.eventDispatcher.publish(new UserCreatedEvent(user));
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteUserHandler {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async execute(id: string): Promise<void> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found.');
|
||||
}
|
||||
|
||||
user.delete();
|
||||
|
||||
await this.userRepository.update(user);
|
||||
|
||||
// TODO:
|
||||
// Publish UserDeletedEvent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
||||
import { User } from '../../domain/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FindUserHandler {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async execute(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found.');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
||||
import { User } from '../../domain/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FindUsersHandler {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<User[]> {
|
||||
return await this.userRepository.findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
Injectable,
|
||||
ConflictException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { UpdateUserDto } from '../../presentation/dto/update-user.dto';
|
||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
||||
import { User } from '../../domain/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateUserHandler {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
id: string,
|
||||
dto: UpdateUserDto,
|
||||
): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found.');
|
||||
}
|
||||
|
||||
if (
|
||||
dto.email &&
|
||||
dto.email !== user.email
|
||||
) {
|
||||
const exists =
|
||||
await this.userRepository.existsByEmail(dto.email);
|
||||
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
'Email already exists.',
|
||||
);
|
||||
}
|
||||
|
||||
user.changeEmail(dto.email);
|
||||
}
|
||||
|
||||
if (dto.name) {
|
||||
user.changeName(dto.name);
|
||||
}
|
||||
|
||||
await this.userRepository.update(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
services/
|
||||
|
||||
Penjelasan:
|
||||
Application services (use-cases) untuk module identity.
|
||||
|
||||
Contoh file:
|
||||
- get-user.service.ts
|
||||
- create-user.service.ts
|
||||
|
||||
Aturan:
|
||||
- Application service mengorkestrasi domain services dan repository.
|
||||
- Menangani transaction boundary jika perlu.
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { UserService } from '../services/user.service';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('api/v1/users')
|
||||
export class UsersController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get user by id' })
|
||||
async findById(@Param('id') id: string) {
|
||||
const data = await this.userService.findById(id);
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
meta: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
modules/identity/domain/
|
||||
|
||||
Penjelasan:
|
||||
Layer domain berisi entity, value objects, repository interfaces, domain services, dan domain events.
|
||||
|
||||
Contoh file:
|
||||
- entities/user.entity.ts
|
||||
- events/user-created.event.ts
|
||||
- repositories/user.repository.interface.ts
|
||||
|
||||
Aturan:
|
||||
- Semua aturan bisnis inti berada di domain.
|
||||
- Infrastruktur mengimplementasikan interface yang didefinisikan di domain.
|
||||
@@ -0,0 +1,11 @@
|
||||
entities/
|
||||
|
||||
Penjelasan:
|
||||
Entity domain untuk identity, mis. User, Profile.
|
||||
|
||||
Contoh file:
|
||||
- user.entity.ts
|
||||
- profile.entity.ts
|
||||
|
||||
Aturan:
|
||||
- Entity berisi atribut dan mungkin method domain kecil (invariants), bukan orchestration.
|
||||
@@ -0,0 +1,52 @@
|
||||
export class User {
|
||||
private constructor(
|
||||
public readonly id: string,
|
||||
public name: string,
|
||||
public email: string,
|
||||
public password: string,
|
||||
public metadata?: Record<string, any>,
|
||||
) {}
|
||||
|
||||
static create(data: {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
metadata?: Record<string, any>;
|
||||
}): User {
|
||||
return new User(
|
||||
crypto.randomUUID(),
|
||||
data.name,
|
||||
data.email,
|
||||
data.password,
|
||||
data.metadata,
|
||||
);
|
||||
}
|
||||
|
||||
delete() {
|
||||
// Business Rule
|
||||
}
|
||||
|
||||
changeEmail(email: string) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
changeName(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
static restore(data: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
metadata?: Record<string, any>;
|
||||
}): User {
|
||||
return new User(
|
||||
data.id,
|
||||
data.name,
|
||||
data.email,
|
||||
data.password,
|
||||
data.metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DomainEvent } from '../../../../core/events/event.interface';
|
||||
|
||||
export class UserCreatedEvent implements DomainEvent {
|
||||
readonly name = 'UserCreated';
|
||||
constructor(public readonly payload: any, public readonly occurredAt: Date = new Date()) {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DomainEvent } from '../../../../core/events/event.interface';
|
||||
|
||||
export class UserDeletedEvent implements DomainEvent {
|
||||
readonly name = 'UserDeleted';
|
||||
constructor(public readonly payload: any, public readonly occurredAt: Date = new Date()) {}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { User } from '../entities/user.entity';
|
||||
|
||||
export abstract class UserRepository {
|
||||
abstract create(user: User): Promise<User>;
|
||||
|
||||
abstract update(user: User): Promise<User>;
|
||||
|
||||
abstract findById(
|
||||
id: string,
|
||||
): Promise<User | null>;
|
||||
|
||||
abstract findAll(): Promise<User[]>;
|
||||
|
||||
abstract existsByEmail(
|
||||
email: string,
|
||||
): Promise<boolean>;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// REMOVED: file content deleted per user request
|
||||
@@ -1 +0,0 @@
|
||||
// REMOVED: file content deleted per user request
|
||||
@@ -1,9 +0,0 @@
|
||||
export class UserEntity {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
metadata?: Record<string, any>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt?: Date | null;
|
||||
}
|
||||
@@ -1 +1,52 @@
|
||||
// REMOVED: file content deleted per user request
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersController } from './presentation/controllers/users.controller';
|
||||
import { PrismaService } from '../../shared/prisma.service';
|
||||
import { PrismaUserRepository } from './infrastructure/repositories/prisma-user.repository';
|
||||
import { UserRepository } from './domain/repositories/user.repository.interface';
|
||||
import { EventDispatcher } from '../../core/events/event-dispatcher';
|
||||
import { JwtAuthGuard } from '../../core/auth/guards/jwt-auth.guard';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { CreateUserHandler } from './application/handlers/create-user.handler';
|
||||
import { FindUserHandler } from './application/handlers/find-user.handler';
|
||||
import { DeleteUserHandler } from './application/handlers/delete-user.handler';
|
||||
import { FindUsersHandler } from './application/handlers/find-users.handler';
|
||||
import { UpdateUserHandler } from './application/handlers/update-user.handler';
|
||||
import { PermissionGuard } from '../authorization/presentation/guards/permission.guard';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
signOptions: {
|
||||
expiresIn: '1d',
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [UsersController],
|
||||
providers: [
|
||||
//#region User
|
||||
CreateUserHandler,
|
||||
FindUserHandler,
|
||||
FindUsersHandler,
|
||||
UpdateUserHandler,
|
||||
DeleteUserHandler,
|
||||
//#endregion
|
||||
|
||||
JwtAuthGuard,
|
||||
PermissionGuard,
|
||||
Reflector,
|
||||
PrismaService,
|
||||
PrismaUserRepository,
|
||||
{
|
||||
provide: UserRepository,
|
||||
useClass: PrismaUserRepository,
|
||||
},
|
||||
{
|
||||
provide: 'EVENT_DISPATCHER',
|
||||
useValue: new EventDispatcher(),
|
||||
},
|
||||
],
|
||||
})
|
||||
export class IdentityModule {}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
modules/identity/infrastructure/
|
||||
|
||||
Penjelasan:
|
||||
Implementasi teknis untuk module identity, seperti Prisma repository, adapter implementations, dan data mappers.
|
||||
|
||||
Contoh file:
|
||||
- prisma/user.repository.ts (mengimplementasikan domain repository interface)
|
||||
- adapter/identity-adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Infrastruktur hanya mengimplementasikan interface domain; jangan memuat business rules.
|
||||
- Import dari infrastructure ke domain harus satu arah: infrastructure -> domain (implementasi).
|
||||
@@ -0,0 +1,27 @@
|
||||
import { User } from '../../domain/entities/user.entity';
|
||||
|
||||
export class PrismaUserMapper {
|
||||
static toDomain(model: any): User | null {
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return User.restore({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
email: model.email,
|
||||
password: model.password,
|
||||
metadata: model.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
static toPersistence(user: User) {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
metadata: user.metadata ?? {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
repositories/
|
||||
|
||||
Penjelasan:
|
||||
Implementasi repository di layer infrastructure. Biasanya berisi Prisma queries dan mapping antara DB model dan domain entity.
|
||||
|
||||
Contoh file:
|
||||
- prisma/user.repository.ts
|
||||
|
||||
Aturan:
|
||||
- Repository mengimplementasikan interface di domain layer.
|
||||
- Hindari business logic di repository.
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../../shared/prisma.service';
|
||||
import { UserRepository } from '../../domain/repositories/user.repository.interface';
|
||||
import { User } from '../../domain/entities/user.entity';
|
||||
import { PrismaUserMapper } from '../mappers/prisma-user.mapper';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaUserRepository implements UserRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findById(id: string) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return PrismaUserMapper.toDomain(user);
|
||||
}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: { deleted_at: null },
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
|
||||
return users.map(PrismaUserMapper.toDomain);
|
||||
}
|
||||
|
||||
async create(user: User): Promise<User> {
|
||||
const created = await this.prisma.user.create({
|
||||
data: PrismaUserMapper.toPersistence(user),
|
||||
});
|
||||
|
||||
return PrismaUserMapper.toDomain(created)!;
|
||||
}
|
||||
|
||||
async update(user: User): Promise<User> {
|
||||
const updated = await this.prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: PrismaUserMapper.toPersistence(user),
|
||||
});
|
||||
|
||||
return PrismaUserMapper.toDomain(updated)!;
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { deleted_at: new Date() },
|
||||
});
|
||||
return PrismaUserMapper.toDomain(user);
|
||||
}
|
||||
|
||||
async existsByEmail(email: string, excludeId?: string | null) {
|
||||
const where: any = { email };
|
||||
if (excludeId) {
|
||||
where.id = { not: excludeId };
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.findFirst({ where });
|
||||
return !!user;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface IUserService {
|
||||
findById(id: string): Promise<any | null>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
modules/identity/presentation/
|
||||
|
||||
Penjelasan:
|
||||
Layer presentation untuk module identity. Berisi controller, request/response DTO, dan mapping ke HTTP.
|
||||
|
||||
Contoh file:
|
||||
- controllers/users.controller.ts
|
||||
- dto/create-user.dto.ts
|
||||
- dto/update-user.dto.ts
|
||||
|
||||
Aturan:
|
||||
- Controller tipis: lakukan validasi DTO dan panggil application service.
|
||||
- Jangan menaruh business logic di controller.
|
||||
@@ -0,0 +1,10 @@
|
||||
controllers/
|
||||
|
||||
Penjelasan:
|
||||
Folder untuk controller HTTP endpoint module identity.
|
||||
|
||||
Contoh file:
|
||||
- users.controller.ts
|
||||
|
||||
Aturan:
|
||||
- Controller hanya menerima request, validasi DTO, panggil application service, kembalikan response.
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
UseGuards,
|
||||
Body,
|
||||
Patch
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { JwtAuthGuard } from '../../../../core/auth/guards/jwt-auth.guard';
|
||||
import { PermissionGuard } from '../../../authorization/presentation/guards/permission.guard';
|
||||
import { Permissions } from '../../../authorization/presentation/decorators/permission.decorator';
|
||||
|
||||
import { FindUserHandler } from '../../application/handlers/find-user.handler';
|
||||
import { FindUsersHandler } from '../../application/handlers/find-users.handler';
|
||||
import { CreateUserHandler } from '../../application/handlers/create-user.handler';
|
||||
import { UpdateUserHandler } from '../../application/handlers/update-user.handler';
|
||||
import { DeleteUserHandler } from '../../application/handlers/delete-user.handler';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
|
||||
@ApiTags('Users')
|
||||
@Controller('api/v1/users')
|
||||
@UseGuards(JwtAuthGuard, PermissionGuard)
|
||||
export class UsersController {
|
||||
constructor(
|
||||
private readonly createUserHandler: CreateUserHandler,
|
||||
private readonly findUserHandler: FindUserHandler,
|
||||
private readonly findUsersHandler: FindUsersHandler,
|
||||
private readonly updateUserHandler: UpdateUserHandler,
|
||||
private readonly deleteUserHandler: DeleteUserHandler,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@Permissions('USER_CREATE')
|
||||
@ApiOperation({ summary: 'Create user' })
|
||||
async create(@Body() dto: CreateUserDto) {
|
||||
const data = await this.createUserHandler.execute(dto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
meta: {},
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Permissions('USER_READ')
|
||||
@ApiOperation({ summary: 'Get user by id' })
|
||||
async getById(@Param('id') id: string) {
|
||||
const data = await this.findUserHandler.execute(id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
meta: {},
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Permissions('USER_READ_ADMIN')
|
||||
@ApiOperation({ summary: 'Get All User' })
|
||||
async getAll() {
|
||||
const data = await this.findUsersHandler.execute();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
meta: {},
|
||||
};
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Permissions('USER_UPDATE')
|
||||
@ApiOperation({ summary: 'Update User' })
|
||||
async update(@Param('id') id:string, @Body() dto:UpdateUserDto) {
|
||||
const data = await this.updateUserHandler.execute(id, dto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
meta: {},
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Permissions('USER_DELETE')
|
||||
@ApiOperation({ summary: 'Delete user by id' })
|
||||
async delete(@Param('id') id: string) {
|
||||
await this.deleteUserHandler.execute(id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: null,
|
||||
meta: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IsEmail, IsNotEmpty, IsString, Length } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty({ example: 'John Doe' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: 'user@example.com' })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: 'strongpassword' })
|
||||
@IsString()
|
||||
@Length(8, 128)
|
||||
password!: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString, IsEmail, Length } from 'class-validator';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@Length(8, 128)
|
||||
password?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../shared/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findById(id: string) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
// map DB fields (snake_case) to camelCase entity
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
displayName: (user as any).display_name ?? (user as any).displayName ?? null,
|
||||
metadata: (user as any).metadata ?? {},
|
||||
createdAt: (user as any).created_at ?? (user as any).createdAt,
|
||||
updatedAt: (user as any).updated_at ?? (user as any).updatedAt,
|
||||
deletedAt: (user as any).deleted_at ?? (user as any).deletedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { IUserService } from '../interfaces/iuser.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserService implements IUserService {
|
||||
constructor(private readonly userRepository: UserRepository) {}
|
||||
|
||||
async findById(id: string) {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
modules/workflow/application/
|
||||
|
||||
Penjelasan:
|
||||
Use-cases yang mengorkestrasi proses lintas module, mis. CreateUserWorkflowService.
|
||||
|
||||
Contoh file:
|
||||
- services/create-user-workflow.service.ts
|
||||
|
||||
Aturan:
|
||||
- Workflow service mem-publish domain events dan/atau menjadwalkan job di queue.
|
||||
- Workflow tidak boleh berada di controller.
|
||||
@@ -0,0 +1,10 @@
|
||||
modules/workflow/domain/
|
||||
|
||||
Penjelasan:
|
||||
Domain objects & events yang berkaitan dengan workflow orchestration.
|
||||
|
||||
Contoh file:
|
||||
- events/workflow-executed.event.ts
|
||||
|
||||
Aturan:
|
||||
- Domain tetap fokus pada konsep bisnis workflow.
|
||||
@@ -0,0 +1,10 @@
|
||||
modules/workflow/infrastructure/
|
||||
|
||||
Penjelasan:
|
||||
Implementasi teknis untuk workflow: scheduler, job enqueueing, worker integrations.
|
||||
|
||||
Contoh file:
|
||||
- queue/workflow-processor.ts
|
||||
|
||||
Aturan:
|
||||
- Infrastruktur harus mengimplementasikan contract yang didefinisikan application/domain.
|
||||
@@ -0,0 +1,10 @@
|
||||
modules/workflow/presentation/
|
||||
|
||||
Penjelasan:
|
||||
Controller atau API endpoint yang memicu atau mengecek status workflow.
|
||||
|
||||
Contoh file:
|
||||
- controllers/workflows.controller.ts
|
||||
|
||||
Aturan:
|
||||
- Presentation hanya memicu workflow request, detail orchestrasi tetap di application layer.
|
||||
Reference in New Issue
Block a user