entity-regask
Skill for creating and managing repositories in NestJS with MongoDB, following best practices and
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Skill for creating and managing repositories in NestJS with MongoDB, following best practices and
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Best practices for creating NestJS routers/controllers in Regask, including usecase integration, response mapping, error handling, and testing.
Best practices for creating NestJS usecases in Regask, including structure, error handling
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 | entity-regask |
| description | Skill for creating and managing repositories in NestJS with MongoDB, following best practices and |
Every repository must:
BaseRepositoryAbstract<Entity>@Injectable() decorator@InjectDefaultModelClsService for context managementimport { Injectable } from '@nestjs/common';
import { BaseRepositoryAbstract } from '@regask/common-lib/mongo/base.repository';
import { Document, Model } from 'mongoose';
import { ClsService } from 'nestjs-cls';
import { MyEntity } from '@/cores/entities/my-entity/entity';
import { InjectDefaultModel } from '@/infra/database/inject-default-model.decorator';
@Injectable()
export class MyRepository extends BaseRepositoryAbstract<MyEntity> {
constructor(
@InjectDefaultModel(MyEntity.name)
entity: Model<Document<MyEntity>>,
private cls: ClsService,
) {
super(entity);
}
}
For entities that extend BaseSoftDeleteEntity, use BaseSoftDeleteRepositoryAbstract:
import { Injectable } from '@nestjs/common';
import { BaseSoftDeleteRepositoryAbstract } from '@regask/common-lib/mongo/base-soft-delete.repository';
import { Document } from 'mongoose';
import { SoftDeleteModel } from 'mongoose-delete';
import { ClsService } from 'nestjs-cls';
import { MyEntity } from '@/cores/entities/my-entity/entity';
import { InjectDefaultModel } from '@/infra/database/inject-default-model.decorator';
@Injectable()
export class MyRepository extends BaseSoftDeleteRepositoryAbstract<MyEntity> {
constructor(
@InjectDefaultModel(MyEntity.name)
entity: SoftDeleteModel<Document<MyEntity>>,
protected cls: ClsService,
) {
super(entity, cls);
}
}
Key differences from regular repository:
BaseSoftDeleteRepositoryAbstract instead of BaseRepositoryAbstractSoftDeleteModel<Document<MyEntity>> instead of Model<Document<MyEntity>>cls to super constructor for audit trail (deletedBy)src/cores/
├── entities/
│ └── my-entity/
│ ├── entity.ts
│ └── enums.ts
├── repositories/
│ └── my-entity/
│ ├── repository.ts
│ └── query-builders/ (Optional)
│ └── get-details.query-builder.ts
└── repositories.module.ts
Find a single document by query conditions:
const entity = await this.myRepository.findOneByCondition({
condition: {
_id: input.id,
companyId: input.companyId,
},
});
if (!entity) {
throw new NotFoundException('entity_not_found').withErrorCode('ENTITY_NOT_FOUND');
}
Atomic find and update operation:
const updatedEntity = await this.myRepository.findOneAndUpdate(
// Query condition
{
_id: input.id,
status: StatusEnum.Pending,
},
// Update data
{
status: StatusEnum.Approved,
review: {
reviewer: {
id: input.reviewer.userId,
name: `${input.reviewer.firstName} ${input.reviewer.lastName}`,
},
reviewedAt: new Date(),
action: ReviewActionEnum.Approve,
remarks: input.remarks,
},
},
);
Advanced MongoDB aggregation with Atlas Search:
// Get count
const countResult = await this.myRepository.aggregateWithSearch({
search: this.#buildSearchPipelineForCount(searchFilter) as any,
pipeline: [
{
$project: {
searchMeta: '$$SEARCH_META',
},
},
{
$limit: 1,
},
],
});
const total = countResult[0]?.searchMeta?.count?.total ?? 0;
// Get paginated results
const items = await this.myRepository.aggregateWithSearch<MyEntity>({
search: this.#buildSearchPipeline(searchFilter, sortConfig) as any,
pipeline: [
...this.#skipPipeline(input),
...this.#limitPipeline(input),
...this.#lookupOriginalDocuments(),
],
});
Access the MongoDB collection name:
const lookup = {
$lookup: {
from: this.myRepository.collectionName,
localField: '_id',
foreignField: '_id',
as: 'originalDocument',
},
};
Find by MongoDB ObjectId:
const entity = await this.myRepository.findOneById(`${entityId}`);
These methods are available when extending BaseSoftDeleteRepositoryAbstract:
The delete method performs soft delete instead of hard delete:
// Marks record as deleted, sets deletedAt and deletedBy
await this.myRepository.delete(entityId);
Behind the scenes:
deleted: truedeletedAt: new Date()deletedBy: auditUser.userId (from CLS context)Soft delete multiple records:
await this.myRepository.deleteMany({
companyId: input.companyId,
status: StatusEnum.Archived,
});
Find records including soft-deleted ones:
// Include soft-deleted records in results
const allRecords = await this.myRepository.findAllWithDeleted(
{ companyId: input.companyId },
{ projection: { displayId: 1, status: 1, deleted: 1 } },
);
Restore soft-deleted records:
// Restore specific records
await this.model.restore({
_id: { $in: idsToRestore },
});
@Injectable()
export class MyRepository extends BaseSoftDeleteRepositoryAbstract<MyEntity> {
// Find with optional inclusion of deleted records
async findAllByCompany({
companyId,
projection = {},
withDeleted = false,
}: {
companyId: string;
projection?: Record<string, 1 | 0>;
withDeleted?: boolean;
}): Promise<MyEntity[]> {
const conditions = { companyId };
if (withDeleted) {
return await this.findAllWithDeleted(conditions, { projection });
} else {
return await this.findAll(conditions, { projection });
}
}
// Restore records by condition
async restoreByIds(ids: string[]) {
await this.model.restore({
_id: { $in: ids },
});
}
}
The soft delete repository automatically excludes deleted records in Atlas Search:
// Deleted records are automatically filtered out
const items = await this.myRepository.aggregateWithSearch<MyEntity>({
search: searchPipeline,
pipeline: aggregationPipeline,
});
The base class adds this filter automatically:
{
operator: AtlasSearchFilterOperator.NotEqual,
attribute: 'deleted',
value: true,
}
Register all repositories in a global module:
import { Global, Module } from '@nestjs/common';
import { CONNECTION_NAME } from '@/infra/database/constant';
import { getMongooseModule } from '@/infra/database/mongo/helpers';
import { MyEntity } from './entities/my-entity/entity';
import { AnotherEntity } from './entities/another-entity/entity';
import { MyRepository } from './repositories/my-entity/repository';
import { AnotherRepository } from './repositories/another-entity/repository';
import { MyQueryBuilder } from './repositories/my-entity/query-builders/get-details.query-builder';
@Global()
@Module({
imports: [
getMongooseModule(CONNECTION_NAME, MyEntity),
getMongooseModule(CONNECTION_NAME, AnotherEntity),
],
controllers: [],
providers: [
MyRepository,
MyQueryBuilder,
AnotherRepository,
],
exports: [
MyRepository,
MyQueryBuilder,
AnotherRepository,
],
})
export class RepositoriesModule {}
Key features:
@Global() makes repositories available application-widegetMongooseModule sets up Mongoose schemas with connectionFor complex aggregation pipelines, use query builders:
import { Injectable } from '@nestjs/common';
@Injectable()
export class MyQueryBuilder {
execute() {
return [
{
$project: {
password: 0, // Exclude sensitive fields
resetPasswordToken: 0,
internalNotes: 0,
},
},
];
}
}
Usage in usecase:
@Injectable()
export class GetDetailsUseCase extends BaseUseCase<Input, Output> {
constructor(
private myRepository: MyRepository,
private myQueryBuilder: MyQueryBuilder,
) {
super();
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
const pipeline = this.myQueryBuilder.execute();
const result = await this.myRepository.aggregate(pipeline);
// ...
}
}
@Injectable()
export class GetDetailsUseCase extends BaseUseCase<Input, Output> {
constructor(private myRepository: MyRepository) {
super();
this.logger = new Logger(GetDetailsUseCase.name);
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
const entity = await this.myRepository.findOneByCondition({
condition: {
_id: input.id,
companyId: input.companyId,
},
});
if (!entity) {
throw new NotFoundException('entity_not_found').withErrorCode(
'ENTITY_NOT_FOUND',
);
}
return this.success(entity);
}
}
@Injectable()
export class ApproveUseCase extends BaseUseCase<Input, Output> {
constructor(private myRepository: MyRepository) {
super();
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
// Guard check
const entity = await this.myRepository.findOneByCondition({
condition: {
_id: input.id,
companyId: input.reviewer.userCompany,
},
});
if (!entity) {
throw new NotFoundException('entity_not_found');
}
if (entity.status !== StatusEnum.Pending) {
throw new BusinessRuleException('entity_not_pending');
}
// Atomic update
const updatedEntity = await this.myRepository.findOneAndUpdate(
{
_id: input.id,
status: StatusEnum.Pending,
},
{
status: StatusEnum.Approved,
review: {
reviewer: {
id: input.reviewer.userId,
name: `${input.reviewer.firstName} ${input.reviewer.lastName}`,
},
reviewedAt: new Date(),
action: ReviewActionEnum.Approve,
},
},
);
return { data: updatedEntity, error: null };
}
}
@Injectable()
export class FetchDataGridUseCase extends BaseUseCase<Input, Output> {
constructor(
private myRepository: MyRepository,
private filterConcern: MapFilterConcern,
) {
super();
}
async handle(input: Input): Promise<UseCaseResult<Output>> {
const searchFilter = this.#buildSearchFilter(input);
// Parallel count and fetch
const [total, items] = await Promise.all([
this.#getFilteredRecordCount(searchFilter),
this.#executeSearchQuery(input, searchFilter),
]);
return {
error: null,
data: { total, items },
};
}
async #getFilteredRecordCount(searchFilter: SearchOperatorQueryType): Promise<number> {
const countResult = await this.myRepository.aggregateWithSearch({
search: this.#buildSearchPipelineForCount(searchFilter) as any,
pipeline: [
{ $project: { searchMeta: '$$SEARCH_META' } },
{ $limit: 1 },
],
});
return countResult[0]?.searchMeta?.count?.total ?? 0;
}
async #executeSearchQuery(
input: Input,
searchFilter: SearchOperatorQueryType,
): Promise<MyEntity[]> {
return await this.myRepository.aggregateWithSearch<MyEntity>({
search: this.#buildSearchPipeline(searchFilter) as any,
pipeline: [
...this.#skipPipeline(input),
...this.#limitPipeline(input),
...this.#lookupOriginalDocuments(),
],
});
}
#lookupOriginalDocuments(): PipelineStage[] {
return [
{
$lookup: {
from: this.myRepository.collectionName,
localField: '_id',
foreignField: '_id',
as: 'originalDocument',
},
},
{ $unwind: '$originalDocument' },
{ $replaceRoot: { newRoot: '$originalDocument' } },
];
}
}
Repositories are tested through integration tests with usecases:
describe('@workflows/my-workflow/usecase', () => {
const testHelper = new CustomTestHelper(CONNECTION_NAME, AppModule);
beforeAll(async () => {
await testHelper.beforeAll();
});
afterAll(() => testHelper.afterAll());
afterEach(() => testHelper.cleanUp());
describe('#handle', () => {
it('should find and return entity', async () => {
// Create test data using factory
const entity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
// Execute usecase (which uses repository)
const { error, data } = await testHelper
.get(GetDetailsUseCase)
.execute({
id: entity._id.toString(),
companyId: entity.companyId,
});
expect(error).toBeNull();
expect(data._id.toString()).toBe(entity._id.toString());
});
it('should update entity status', async () => {
const entity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
const { error, data } = await testHelper
.get(ApproveUseCase)
.execute({
id: entity._id.toString(),
reviewer,
});
expect(error).toBeNull();
expect(data.status).toBe(StatusEnum.Approved);
// Verify in database
const updatedEntity = await testHelper
.get(MyRepository)
.findOneById(`${entity._id}`);
expect(updatedEntity?.status).toBe(StatusEnum.Approved);
});
});
});
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(4)}`,
companyId: new mongoose.Types.ObjectId().toString(),
status: faker.helpers.enumValue(StatusEnum),
createdBy: {
id: new mongoose.Types.ObjectId().toString(),
name: faker.person.fullName(),
},
updatedBy: {
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));
}
}
Usage:
// Create single entity in database
const entity = await testHelper
.factoryBuilder(MyEntityFactory)
.create({
companyId,
status: StatusEnum.Pending,
});
// Create multiple entities
await testHelper
.factoryBuilder(MyEntityFactory)
.createMany(5, {
companyId,
});