Menyelesaikan object user dan init base
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
src/ (overview)
|
||||
|
||||
Penjelasan singkat:
|
||||
Folder src adalah root source code aplikasi. Berikut child-folder utama yang ada saat ini:
|
||||
- core/: framework-level utilities (events, middleware, logger, exceptions, queue)
|
||||
- shared/: shared utilities, types, decorators
|
||||
- config/: configuration module dan service
|
||||
- adapters/: external integrations
|
||||
- modules/: domain modules (identity, authorization, media, storage, ...)
|
||||
|
||||
Contoh file entrypoint:
|
||||
- src/main.ts: bootstrap aplikasi, register global middleware, setup swagger
|
||||
- src/app.module.ts: root module yang mengimpor module lainnya
|
||||
|
||||
Panduan singkat:
|
||||
- Ikuti struktur Clean Architecture untuk modul-modul dalam modules/.
|
||||
- Jangan menaruh business logic di shared/ atau core/.
|
||||
- Gunakan event-driven dan queue untuk proses lintas module.
|
||||
@@ -0,0 +1,18 @@
|
||||
adapters/
|
||||
|
||||
Penjelasan singkat:
|
||||
Folder adapters berisi integrasi ke layanan eksternal. Adapter bertanggung jawab sebagai "translator" antara API eksternal dan interface aplikasi.
|
||||
|
||||
Struktur contoh:
|
||||
- adapters/identity/
|
||||
- adapters/media/ (jellyfin, immich)
|
||||
- adapters/storage/ (filesystem, nextcloud, s3)
|
||||
- adapters/notification/ (email, webhook)
|
||||
- adapters/integration/ (3rd-party integrations)
|
||||
|
||||
Aturan:
|
||||
- Adapter tidak mengandung business logic. Business logic tetap berada di Application/Domain layer.
|
||||
- Adapter harus mengimplementasikan interface yang didefinisikan di module/domain yang memerlukannya.
|
||||
|
||||
Contoh file:
|
||||
- src/adapters/media/jellyfin/jellyfin.adapter.ts
|
||||
@@ -0,0 +1,11 @@
|
||||
adapters/identity/
|
||||
|
||||
Penjelasan:
|
||||
Adapter untuk layanan identity eksternal (jika ada). Implementasi translation antara eksternal API dan internal interface.
|
||||
|
||||
Contoh file:
|
||||
- identity.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Jangan letakkan business logic di adapter.
|
||||
- Adapter harus mengimplementasikan interface yang dibutuhkan module.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
adapters/immich/
|
||||
|
||||
Penjelasan:
|
||||
Adapter spesifik untuk Immich integration.
|
||||
|
||||
Contoh file:
|
||||
- immich.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Adapter harus stateless dan dapat digunakan kembali.
|
||||
@@ -0,0 +1,10 @@
|
||||
adapters/integration/
|
||||
|
||||
Penjelasan:
|
||||
Adapter untuk integrasi third-party yang lebih spesifik (mis. external API vendors).
|
||||
|
||||
Contoh file:
|
||||
- vendor-a.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Jangan masukkan business rules di sini; hanya mapping dan komunikasi.
|
||||
@@ -0,0 +1,10 @@
|
||||
adapters/jellyfin/
|
||||
|
||||
Penjelasan:
|
||||
Adapter spesifik untuk Jellyfin (trigger library refresh, fetch metadata).
|
||||
|
||||
Contoh file:
|
||||
- jellyfin.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Jangan masukkan orchestration di adapter; gunakan service/module yang memanggil adapter.
|
||||
@@ -0,0 +1,12 @@
|
||||
adapters/media/
|
||||
|
||||
Penjelasan:
|
||||
Adapters untuk media services seperti Jellyfin atau Immich.
|
||||
|
||||
Contoh file:
|
||||
- jellyfin.adapter.ts
|
||||
- immich.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Adapter hanya wrap API calls dan mapping data.
|
||||
- Integrasi heavy operations diserahkan ke queue/worker.
|
||||
@@ -0,0 +1,10 @@
|
||||
adapters/nextcloud/
|
||||
|
||||
Penjelasan:
|
||||
Adapter untuk Nextcloud (file ops, permissions).
|
||||
|
||||
Contoh file:
|
||||
- nextcloud.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Pastikan error mapping agar client mendapatkan pesan yang konsisten.
|
||||
@@ -0,0 +1,11 @@
|
||||
adapters/notification/
|
||||
|
||||
Penjelasan:
|
||||
Adapter untuk sistem notifikasi (email, webhook).
|
||||
|
||||
Contoh file:
|
||||
- email.adapter.ts
|
||||
- webhook.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Notification adapter bersifat best-effort; retry dan persistence dikelola oleh queue/worker.
|
||||
@@ -0,0 +1,13 @@
|
||||
adapters/storage/
|
||||
|
||||
Penjelasan:
|
||||
Adapters untuk storage backends: filesystem, nextcloud, s3, minio.
|
||||
|
||||
Contoh file:
|
||||
- filesystem.adapter.ts
|
||||
- nextcloud.adapter.ts
|
||||
- s3.adapter.ts
|
||||
|
||||
Aturan:
|
||||
- Adapter expose interface seperti readFile, writeFile, listFolder.
|
||||
- Business logic integrasi tetap berada di module/storage.
|
||||
+15
-1
@@ -1 +1,15 @@
|
||||
// placeholder app.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { IdentityModule } from './modules/identity/identity.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
|
||||
IdentityModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/core and src/shared
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/core/guards or module-specific guards
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/core/exceptions
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/core/guards or module-specific guards
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/core/interceptors or module-specific interceptors
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/core/logger
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/core/middleware
|
||||
@@ -0,0 +1 @@
|
||||
// MOVED: see src/shared/utils
|
||||
@@ -1 +0,0 @@
|
||||
// placeholder config.module.ts
|
||||
@@ -1 +0,0 @@
|
||||
// placeholder config.service.ts
|
||||
@@ -0,0 +1,23 @@
|
||||
core/
|
||||
|
||||
Penjelasan singkat:
|
||||
Folder core berisi fasilitas framework-level yang dipakai lintas modul. Jangan tempatkan business logic domain di sini. Core menyediakan building block seperti:
|
||||
|
||||
- Event bus / dispatcher
|
||||
- Global middleware (request-id, correlation id)
|
||||
- Global exception filters
|
||||
- Logger (pino wrapper)
|
||||
- Queue foundation (struktur jobs/workers/processors)
|
||||
|
||||
Contoh file yang ada di folder ini:
|
||||
- src/core/events/event.interface.ts
|
||||
- src/core/events/event-publisher.ts
|
||||
- src/core/logger/logger.service.ts
|
||||
- src/core/middleware/request-id.middleware.ts
|
||||
- src/core/exceptions/http-exception.filter.ts
|
||||
- src/core/queue/*
|
||||
|
||||
Panduan singkat:
|
||||
- Semua event domain didefinisikan di core/events. Module boleh publish/subscribe.
|
||||
- Logger tersedia sebagai utilitas global (injectable or exported instance).
|
||||
- Queue hanya menyediakan pondasi; integrasi spesifik (BullMQ, Redis) dilakukan pada fase infrastructure atau worker.
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
throw new UnauthorizedException('Authorization header is missing.');
|
||||
}
|
||||
|
||||
const [type, token] = authHeader.split(' ');
|
||||
|
||||
if (type !== 'Bearer' || !token) {
|
||||
throw new UnauthorizedException('Invalid authorization header.');
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token);
|
||||
|
||||
request['user'] = payload;
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
throw new UnauthorizedException('Invalid or expired token.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user: JwtPayload;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default () => ({
|
||||
database: {
|
||||
url:
|
||||
`postgresql://` +
|
||||
`${process.env.DB_USER}:` +
|
||||
`${process.env.DB_PASSWORD}@` +
|
||||
`${process.env.SERVER_HOST}:` +
|
||||
`${process.env.DB_PORT}/` +
|
||||
`${process.env.DB_NAME}?schema=public`
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
core/events/
|
||||
|
||||
Penjelasan:
|
||||
Folder ini berisi pondasi untuk domain events: definisi event, publisher, dispatcher, dan handler.
|
||||
|
||||
Isi yang direkomendasikan:
|
||||
- event.interface.ts (DomainEvent)
|
||||
- event-publisher.ts (interface dan implementasi sederhana)
|
||||
- event-dispatcher.ts (dispatcher untuk publish/dispatch event)
|
||||
- event-handler.ts (type alias untuk handler)
|
||||
|
||||
Aturan:
|
||||
- Event hanya bermakna untuk komunikasi lintas module atau untuk memisahkan side-effect.
|
||||
- Hindari business logic kompleks di handler; handler sebaiknya mendelegasikan ke application service atau job queue.
|
||||
@@ -0,0 +1,13 @@
|
||||
import { DomainEvent } from './event.interface';
|
||||
|
||||
export class EventDispatcher {
|
||||
private publisher = new (require('./event-publisher').InMemoryEventPublisher)();
|
||||
|
||||
register(handler: (e: DomainEvent) => Promise<void>) {
|
||||
this.publisher.register(handler);
|
||||
}
|
||||
|
||||
async dispatch(event: DomainEvent) {
|
||||
await this.publisher.publish(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DomainEvent } from './event.interface';
|
||||
|
||||
export type EventHandler = (event: DomainEvent) => Promise<void>;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { DomainEvent } from './event.interface';
|
||||
|
||||
export interface EventPublisher {
|
||||
publish(event: DomainEvent): Promise<void>;
|
||||
}
|
||||
|
||||
export class InMemoryEventPublisher implements EventPublisher {
|
||||
private handlers: Array<(e: DomainEvent) => Promise<void>> = [];
|
||||
|
||||
register(handler: (e: DomainEvent) => Promise<void>) {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
async publish(event: DomainEvent) {
|
||||
await Promise.all(this.handlers.map((h) => h(event)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface DomainEvent {
|
||||
readonly name: string;
|
||||
readonly payload: any;
|
||||
readonly occurredAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
core/exceptions/
|
||||
|
||||
Penjelasan:
|
||||
Berisi global exception filters dan custom exception classes.
|
||||
|
||||
Isi yang direkomendasikan:
|
||||
- http-exception.filter.ts
|
||||
- domain-exception.ts (custom domain exception base)
|
||||
|
||||
Aturan:
|
||||
- Exception filter mengubah exception menjadi response standar API.
|
||||
- Gunakan kode error konsisten (error code constant) agar client dapat menangani error.
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: any, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const status = exception.getStatus ? exception.getStatus() : 500;
|
||||
|
||||
response.status(status).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: exception.code || 'INTERNAL_ERROR',
|
||||
message: exception.message || 'Internal server error',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
core/logger/
|
||||
|
||||
Penjelasan:
|
||||
Folder ini berisi wrapper logger (mis. pino) dan utilitas logging global.
|
||||
|
||||
Isi yang direkomendasikan:
|
||||
- logger.service.ts / logger.instance.ts (exported pino instance or injectable wrapper)
|
||||
- logger.interceptor.ts (optional HTTP logging interceptor)
|
||||
|
||||
Aturan:
|
||||
- Gunakan structured logging (json) dengan field requestId, module, level.
|
||||
- Jangan menambahkan business logic ke logger.
|
||||
@@ -0,0 +1,5 @@
|
||||
import pino from 'pino';
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
core/middleware/
|
||||
|
||||
Penjelasan:
|
||||
Berisi middleware global seperti request-id, correlation id, timing, rate-limit glue (middleware only, implementasi rate-limit ada di infra).
|
||||
|
||||
Isi yang direkomendasikan:
|
||||
- request-id.middleware.ts
|
||||
- correlation-id.middleware.ts
|
||||
- timing.middleware.ts
|
||||
|
||||
Aturan:
|
||||
- Middleware harus ringan dan synchronous jika memungkinkan.
|
||||
- Jangan letakkan business logic di middleware.
|
||||
- Middleware global didaftarkan di main.ts atau CoreModule.
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class RequestIdMiddleware implements NestMiddleware {
|
||||
use(req: any, res: any, next: () => void) {
|
||||
req.requestId = req.headers['x-request-id'] || uuidv4();
|
||||
res.setHeader('X-Request-ID', req.requestId);
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Queue layer placeholder.
|
||||
|
||||
Structure:
|
||||
- jobs/: job definitions
|
||||
- workers/: worker implementations
|
||||
- processors/: processors that execute jobs
|
||||
|
||||
Queue integration (e.g. BullMQ) should be added in infrastructure phase.
|
||||
@@ -0,0 +1,11 @@
|
||||
core/queue/jobs/
|
||||
|
||||
Penjelasan:
|
||||
Tempat definisi job (job payload shape, job name constants). Job definitions hanya mendefinisikan contract untuk job.
|
||||
|
||||
Contoh:
|
||||
- media-scan.job.ts
|
||||
- thumbnail.job.ts
|
||||
|
||||
Aturan:
|
||||
- Job definition tidak mengeksekusi logic; worker/processors yang mengeksekusi.
|
||||
@@ -0,0 +1,12 @@
|
||||
core/queue/processors/
|
||||
|
||||
Penjelasan:
|
||||
Processor berisi fungsi yang mengeksekusi pekerjaan sebenarnya (CPU/IO heavy). Processor harus idempotent dan memiliki retry strategy.
|
||||
|
||||
Contoh:
|
||||
- media-scan.processor.ts
|
||||
- thumbnail.processor.ts
|
||||
|
||||
Aturan:
|
||||
- Processor harus bisa dijalankan terpisah (CLI/worker process).
|
||||
- Jangan panggil controller dari processor; gunakan service/infrastructure.
|
||||
@@ -0,0 +1,12 @@
|
||||
core/queue/workers/
|
||||
|
||||
Penjelasan:
|
||||
Worker mengkonsumsi job dari queue dan memanggil processor yang sesuai.
|
||||
|
||||
Contoh:
|
||||
- media-scan.worker.ts
|
||||
- thumbnail.worker.ts
|
||||
|
||||
Aturan:
|
||||
- Worker hanya fokus pada retry/failure handling dan delegasi ke processor.
|
||||
- Implementasi queue (BullMQ, Bee-Queue) ditaruh di infra/worker deployment.
|
||||
+48
-1
@@ -1 +1,48 @@
|
||||
// placeholder main.ts
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const config = app.get(ConfigService);
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.enableCors();
|
||||
|
||||
const swaggerEnabled =
|
||||
config.get<string>('SWAGGER_ENABLED') === 'true';
|
||||
|
||||
if (swaggerEnabled) {
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('RayLab Core API')
|
||||
.setDescription('RayLab Core REST API')
|
||||
.setVersion('1.0.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||
|
||||
SwaggerModule.setup('docs', app, document);
|
||||
}
|
||||
|
||||
const port = config.get<number>('PORT') || 3000;
|
||||
|
||||
await app.listen(port);
|
||||
|
||||
console.log(`Server running on http://localhost:${port}`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -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.
|
||||
@@ -0,0 +1,20 @@
|
||||
shared/
|
||||
|
||||
Penjelasan singkat:
|
||||
Folder shared berisi utilitas ringan, tipe, dan interface yang dapat digunakan lintas module tanpa memuat business logic.
|
||||
|
||||
Subfolder dan kegunaan:
|
||||
- constants/: konstanta aplikasi
|
||||
- decorators/: custom decorators yang dipakai lintas module
|
||||
- dto/: DTO yang dipakai bersama
|
||||
- interfaces/: interface yang dipakai lintas module
|
||||
- types/: type aliases
|
||||
- utils/: helper pure functions (pure utilities tanpa efek samping)
|
||||
|
||||
Contoh file:
|
||||
- src/shared/utils/index.ts
|
||||
|
||||
Panduan singkat:
|
||||
- Jangan letakkan business logic di sini.
|
||||
- Gunakan shared untuk menghindari duplicate code yang murni utility.
|
||||
- Jika utilitas mulai mengandung dependency heavy (Prisma, Adapter), pindahkan ke module yang sesuai.
|
||||
@@ -0,0 +1,11 @@
|
||||
shared/constants/
|
||||
|
||||
Penjelasan:
|
||||
Berisi konstanta yang digunakan lintas module, mis. DEFAULT_PAGINATION, API_VERSION, movement type ids.
|
||||
|
||||
Contoh file:
|
||||
- api.constants.ts
|
||||
- pagination.constants.ts
|
||||
|
||||
Aturan:
|
||||
- Gunakan constant class atau exported const. Hindari magic string di codebase.
|
||||
@@ -0,0 +1,11 @@
|
||||
shared/decorators/
|
||||
|
||||
Penjelasan:
|
||||
Custom decorators yang dapat digunakan pada controllers atau services, mis. @CurrentUser(), @RequestId(), @Public().
|
||||
|
||||
Contoh file:
|
||||
- current-user.decorator.ts
|
||||
- request-id.decorator.ts
|
||||
|
||||
Aturan:
|
||||
- Dekorator hanya menyediakan sintaks sugar; logika harus sederhana dan tidak memuat side-effect kompleks.
|
||||
@@ -0,0 +1,11 @@
|
||||
shared/dto/
|
||||
|
||||
Penjelasan:
|
||||
DTO yang bersifat umum dan dipakai lintas module. Misalnya StandardApiResponse, PaginationDto.
|
||||
|
||||
Contoh file:
|
||||
- standard-response.dto.ts
|
||||
- pagination.dto.ts
|
||||
|
||||
Aturan:
|
||||
- DTO shared hanya untuk struktur data umum. Spesifik domain DTO ditempatkan di module/presentation.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user