usecase-regask
Best practices for creating NestJS usecases in Regask, including structure, error handling
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Best practices for creating NestJS usecases in Regask, including structure, error handling
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Skill for creating and managing repositories in NestJS with MongoDB, following best practices and
Best practices for creating NestJS routers/controllers in Regask, including usecase integration, response mapping, error handling, and testing.
Backend engineering specialist for APIs, databases, system architecture, and server-side development
Frontend development specialist for UI/UX, frameworks, responsive design, and web application architecture
Solution architecture reviewer for design patterns, scalability, maintainability, and hexagonal architecture
Technology research and evaluation specialist for comparing frameworks, libraries, and adoption strategies
| name | usecase-regask |
| description | Best practices for creating NestJS usecases in Regask, including structure, error handling |
types.ts file using namespaceshandle() method, organizing code with private methods by concern (e.g. guard checks, business logic, side effects).Every usecase must:
BaseUseCase<Input, Output>handle() method with private methods by concernUseCaseResult<Output> with structured error handlingimport { Injectable } from '@nestjs/common';
import { Logger } from '@regask/common-lib/logger/logger.service';
import { BaseUseCase, UseCaseResult } from '@regask/common-lib/workflow';
import {
BusinessRuleException,
NotFoundException,
PermissionDeniedException,
} from '@regask/common-lib/workflow/types';
import { MyUseCaseTypes } from './types';
type Input = MyUseCaseTypes.Input;
type Output = MyUseCaseTypes.Output;
@Injectable()
export class MyUseCase extends BaseUseCase<Input, Output> {
constructor(
private myRepository: MyRepository,
private myConcern: MyConcern,
) {
super();
this.logger = new Logger(MyUseCase.name);
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
// 1. Guard checks (validation, authorization)
await this.#guardChecks(input);
// 2. Main business logic
const result = await this.#executeBusinessLogic(input);
// 3. Side effects (events, notifications)
await this.#publishEvents(result);
return {
data: result,
error: null,
};
}
async #guardChecks(input: Input): Promise<void> {
// Validation and authorization logic
}
async #executeBusinessLogic(input: Input): Promise<Output> {
// Core business logic
}
async #publishEvents(data: Output): Promise<void> {
// Event publishing / side effects
}
}
Define types in a separate types.ts file using namespaces:
import { AuditUser } from '@regask/common-lib/microservices/identity/types/audit-user.type';
import { MyEntity } from '@/cores/entities/my-entity/entity';
export namespace MyUseCaseTypes {
export type Input = {
id: string; // Required fields first
userId: string;
companyId: string;
remarks?: string; // Optional fields with ?
reviewer?: AuditUser; // Use AuditUser for user context
};
export type Output = MyEntity;
}
Important: Use AuditUser for User Context
For actors like reviewer, approver, submitter, or any user performing an action, always use AuditUser from common-lib instead of creating custom types. AuditUser is the standard user context type across the system and includes:
userId, userCompany, userRolefirstName, lastName, emailpermission (RBAC permissions)// GOOD: Use AuditUser for user actors
export type Input = {
id: string;
reviewer: AuditUser; // Standard user context
};
// BAD: Don't create custom user types for actors
export type Input = {
id: string;
reviewer: { // Avoid custom actor types
id: string;
name: string;
};
};
For complex inputs with pagination/sorting:
export namespace FetchDataGridUseCaseTypes {
export type Sort = {
sortBy?: string;
sortDirection?: 'asc' | 'desc';
};
export type Pagination = {
page?: number;
limit?: number;
};
export type Input = {
companyId: string;
userId: string;
filter?: GridFilterModel;
searchKey?: string;
pagination?: Pagination;
sorts?: Sort[];
};
export type Output = {
total: number;
items: MyEntity[];
};
}
Organize code in handle() with clear separation:
async handle(input: Input): Promise<UseCaseResult<Output>> {
this.logger.info(`Executing usecase ${MyUseCase.name}`, { input });
// 1. Build filters and search criteria
const searchFilter = this.#buildSearchFilter(input);
// 2. Parallelize independent operations
const [total, items] = await Promise.all([
this.#getFilteredRecordCount(searchFilter),
this.#executeSearchQuery(input, searchFilter),
]);
// 3. Return result
return {
error: null,
data: { total, items },
};
}
Use specific exception types with error codes:
// Not found
throw new NotFoundException('entity_not_found').withErrorCode('ENTITY_NOT_FOUND');
// Business rule violation
throw new BusinessRuleException('entity_not_pending').withErrorCode('ENTITY_NOT_PENDING');
// Permission denied
throw new PermissionDeniedException('user_not_allowed').withErrorCode('USER_NOT_ALLOWED');
Extract logic into concerns when:
Place concerns in:
workflows/shared/concerns/ - for cross-workflow concernsworkflows/{workflow-name}/concerns/ - for workflow-specific concernsimport { Injectable } from '@nestjs/common';
import { PermissionDeniedException } from '@regask/common-lib/workflow/types';
@Injectable()
export class RbacPermissionConcern {
guardApproval(reviewer: AuditUser, purpose: PurposeEnum): void {
if (!this.#hasPermissionForPurpose(reviewer, purpose)) {
throw new PermissionDeniedException(
'user_not_allowed_to_approve',
).withErrorCode('USER_NOT_ALLOWED_TO_APPROVE');
}
}
#hasPermissionForPurpose(reviewer: AuditUser, purpose: PurposeEnum): boolean {
const permissionMapping = PURPOSE_PERMISSION_MAP[purpose];
if (!permissionMapping) return false;
const { module, action } = permissionMapping;
return reviewer.permission?.[module]?.[action] === true;
}
}
@Injectable()
export class ApproveUseCase extends BaseUseCase<Input, Output> {
constructor(
private rbacPermissionConcern: RbacPermissionConcern,
) {
super();
}
async #guardCheckApproval(input: Input): Promise<void> {
this.rbacPermissionConcern.guardApproval(
input.reviewer,
input.purpose,
);
}
}
When a usecase does too many things:
// Instead of one large usecase doing everything:
// ProcessOrderUseCase (validates, creates, notifies, updates inventory)
// Break into:
// ValidateOrderUseCase
// CreateOrderUseCase
// NotifyOrderCreatedUseCase
// UpdateInventoryUseCase
// Orchestrate if needed:
@Injectable()
export class ProcessOrderUseCase extends BaseUseCase<Input, Output> {
constructor(
private validateOrderUseCase: ValidateOrderUseCase,
private createOrderUseCase: CreateOrderUseCase,
private notifyOrderCreatedUseCase: NotifyOrderCreatedUseCase,
) {
super();
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
await this.validateOrderUseCase.executeOrThrowHttpError(input);
const order = await this.createOrderUseCase.executeOrThrowHttpError(input);
await this.notifyOrderCreatedUseCase.execute({ order });
return { data: order, error: null };
}
}
import { CustomTestHelper } from '@/test-helper/custom.test-helper';
import { AppModule } from '@/app.module';
import { CONNECTION_NAME } from '@/infra/database/constant';
describe('@workflows/my-workflow/my-usecase', () => {
const testHelper = new CustomTestHelper(CONNECTION_NAME, AppModule);
beforeAll(async () => {
await testHelper.beforeAll();
});
afterAll(() => testHelper.afterAll());
afterEach(() => testHelper.cleanUp());
});
ALWAYS use factoryBuilder for database interactions. NEVER use toBeDefined() for assertions.
// Create single entity
const entity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
// Create multiple entities
await testHelper
.factoryBuilder(MyEntityFactory)
.createMany(3, {
companyId,
});
describe('when entity exists and user has permission', () => {
it('should update status to Approved', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const pendingEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
const { error, data } = await testHelper
.get(ApproveUseCase)
.execute({
id: pendingEntity._id.toString(),
reviewer,
});
expect(error).toBeNull();
expect(data.status).toBe(StatusEnum.Approved);
expect(data.review.reviewer.id).toBe(reviewer.userId);
});
});
describe('when entity does not exist', () => {
it('should return error code ENTITY_NOT_FOUND', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const nonExistentId = new mongoose.Types.ObjectId().toString();
const { error } = await testHelper
.get(ApproveUseCase)
.execute({
id: nonExistentId,
reviewer,
});
expect(error.type).toBe(UseCaseErrorType.NOT_FOUND);
expect(error.data.errorCode).toBe('entity_not_found');
});
});
describe('when entity is not pending', () => {
it('should return error code ENTITY_NOT_PENDING', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const approvedEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Approved,
});
const { error } = await testHelper
.get(ApproveUseCase)
.execute({
id: approvedEntity._id.toString(),
reviewer,
});
expect(error.type).toBe(UseCaseErrorType.BUSINESS_RULE);
expect(error.data.errorCode).toBe('entity_not_pending');
});
});
describe('when user does not have permission', () => {
it('should return error code USER_NOT_ALLOWED', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const reviewerWithoutPermission = AuditUserFactory.create({
userCompany: companyId,
permission: {
[ClientModuleEnum.CLIENT__ALERT]: {
[ClientAlertActionEnum.APPROVE_ALERTS]: false,
},
} as PermissionValueObject,
});
const pendingEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
const { error } = await testHelper
.get(ApproveUseCase)
.execute({
id: pendingEntity._id.toString(),
reviewer: reviewerWithoutPermission,
});
expect(error.type).toBe(UseCaseErrorType.PERMISSION_DENIED);
expect(error.data.errorCode).toBe('user_not_allowed');
});
});
describe('when signaling event', () => {
it('should send Kafka message', async () => {
const companyId = new mongoose.Types.ObjectId().toString();
const entityId = new mongoose.Types.ObjectId().toString();
const reviewer = createUserWithPermission({ userCompany: companyId });
const pendingEntity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
entityId,
status: StatusEnum.Pending,
});
const sendKafkaMessageSpy = jest.spyOn(
testHelper.get(KafkaProducerV2Service),
'sendMessage',
);
const { error, data } = await testHelper
.get(ApproveUseCase)
.execute({
id: pendingEntity._id.toString(),
reviewer,
});
expect(error).toBeNull();
expect(sendKafkaMessageSpy).toHaveBeenCalledTimes(1);
expect(sendKafkaMessageSpy).toHaveBeenCalledWith(
EventTopicEnumV2.MyEventTopic,
{
eventBody: {
entityId: data._id.toString(),
companyId: entityId,
},
eventMetadata: {},
},
);
});
});
import { faker } from '@faker-js/faker';
import mongoose from 'mongoose';
import { MyEntity } from '@/cores/entities/my-entity/entity';
import { StatusEnum } from '@/cores/entities/my-entity/enums';
import { MyRepository } from '@/cores/repositories/my-entity/repository';
import { InjectRepository } from '../decorators/inject-repository.decorator';
@InjectRepository(MyRepository)
export class MyEntityFactory {
static create(override?: Partial<MyEntity>): MyEntity {
const entity: MyEntity = {
_id: new mongoose.Types.ObjectId(),
displayId: `MY${faker.string.numeric(2)}`,
companyId: new mongoose.Types.ObjectId().toString(),
status: faker.helpers.enumValue(StatusEnum),
createdBy: {
id: new mongoose.Types.ObjectId().toString(),
name: faker.person.fullName(),
},
createdAt: faker.date.past(),
updatedAt: faker.date.past(),
...override,
};
return entity;
}
static createMany(num: number, override?: Partial<MyEntity>): MyEntity[] {
return Array.from({ length: num }, () => this.create(override));
}
}
Create reusable helper functions for common test setups:
const createUserWithPermission = (
overrides: Partial<AuditUser> = {},
): AuditUser =>
AuditUserFactory.create({
...overrides,
permission: {
[ClientModuleEnum.CLIENT__ALERT]: {
[ClientAlertActionEnum.APPROVE_ALERTS]: true,
},
} as PermissionValueObject,
});