Menyelesaikan object user dan init base

This commit is contained in:
Mr. Rayyan
2026-07-30 22:47:18 +07:00
parent 3efac50f39
commit fdbfb34842
128 changed files with 6785 additions and 3156 deletions
+18
View File
@@ -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.
+18
View File
@@ -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
+11
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
+10
View File
@@ -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.
+10
View File
@@ -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.
+10
View File
@@ -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.
+12
View File
@@ -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.
+10
View File
@@ -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.
+11
View File
@@ -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.
+13
View File
@@ -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
View File
@@ -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 {}
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/core and src/shared
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/core/guards or module-specific guards
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/core/exceptions
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/core/guards or module-specific guards
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/core/interceptors or module-specific interceptors
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/core/logger
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/core/middleware
+1
View File
@@ -0,0 +1 @@
// MOVED: see src/shared/utils
-1
View File
@@ -1 +0,0 @@
// placeholder config.module.ts
-1
View File
@@ -1 +0,0 @@
// placeholder config.service.ts
+23
View File
@@ -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.
+41
View File
@@ -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;
}
+11
View File
@@ -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`
}
});
+14
View File
@@ -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.
+13
View File
@@ -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);
}
}
+3
View File
@@ -0,0 +1,3 @@
import { DomainEvent } from './event.interface';
export type EventHandler = (event: DomainEvent) => Promise<void>;
+17
View File
@@ -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)));
}
}
+5
View File
@@ -0,0 +1,5 @@
export interface DomainEvent {
readonly name: string;
readonly payload: any;
readonly occurredAt: Date;
}
+12
View File
@@ -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',
},
});
}
}
+12
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
});
+14
View File
@@ -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();
}
}
+8
View File
@@ -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.
+11
View File
@@ -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.
+12
View File
@@ -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.
+12
View File
@@ -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
View File
@@ -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();
+20
View File
@@ -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: {},
};
}
}
+13
View File
@@ -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;
}
+52 -1
View File
@@ -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.
+10
View File
@@ -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.
+20
View File
@@ -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.
View File
+11
View File
@@ -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.
View File
+11
View File
@@ -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.
View File
+11
View File
@@ -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