| name | mbc-generate |
| description | Generate MBC CQRS Serverless boilerplate code. Use this when the user wants to create a new module, service, controller, command, query, event handler, or data sync handler for MBC CQRS Serverless framework. |
Pre-flight Check (Version Update)
Before executing this skill, check for updates:
- Run
mbc install-skills --check to check if a newer version is available
- If the output shows "Update available: X.Y.Z → A.B.C", ask the user:
- "A newer version of MBC skills is available (X.Y.Z → A.B.C). Would you like to update before proceeding?"
- If the user agrees, run
mbc install-skills --force to update
- If the user declines or skills are up-to-date, proceed with the skill
Note: Skip this check if the user explicitly says to skip updates or if you've already checked in this session.
MBC CQRS Serverless Code Generator
This skill generates boilerplate code following MBC CQRS Serverless best practices.
Usage
When the user requests code generation, follow these patterns:
Module Generation
When user says: "create a module for [Entity]" or "generate [Entity] module"
Generate the following files:
1. Module File ([entity].module.ts)
import { Module } from '@nestjs/common';
import { CommandModule } from '@mbc-cqrs-serverless/core';
import { [Entity]Controller } from './[entity].controller';
import { [Entity]Service } from './[entity].service';
import { [Entity]DataSyncRdsHandler } from './handler/[entity]-rds.handler';
@Module({
imports: [
CommandModule.register({
tableName: '[entity]',
dataSyncHandlers: [[Entity]DataSyncRdsHandler],
}),
],
controllers: [[Entity]Controller],
providers: [[Entity]Service],
exports: [[Entity]Service],
})
export class [Entity]Module {}
2. Controller File ([entity].controller.ts)
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { [Entity]Service } from './[entity].service';
import { Create[Entity]Dto } from './dto/create-[entity].dto';
import { Update[Entity]Dto } from './dto/update-[entity].dto';
import { Search[Entity]Dto } from './dto/search-[entity].dto';
import { INVOKE_CONTEXT, IInvoke } from '@mbc-cqrs-serverless/core';
@ApiTags('[entity]')
@Controller('[entity]')
export class [Entity]Controller {
constructor(private readonly [entity]Service: [Entity]Service) {}
@Post()
async create(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Body() dto: Create[Entity]Dto,
) {
return this.[entity]Service.create(dto, invokeContext);
}
@Get()
async search(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Query() dto: Search[Entity]Dto,
) {
return this.[entity]Service.search(dto, invokeContext);
}
@Get(':pk/:sk')
async findOne(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Param('pk') pk: string,
@Param('sk') sk: string,
) {
return this.[entity]Service.findOne({ pk, sk }, invokeContext);
}
@Put(':pk/:sk')
async update(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Param('pk') pk: string,
@Param('sk') sk: string,
@Body() dto: Update[Entity]Dto,
) {
return this.[entity]Service.update({ pk, sk }, dto, invokeContext);
}
@Delete(':pk/:sk')
async remove(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Param('pk') pk: string,
@Param('sk') sk: string,
) {
return this.[entity]Service.remove({ pk, sk }, invokeContext);
}
}
3. Service File ([entity].service.ts)
import { Injectable } from '@nestjs/common';
import { basename } from 'path';
import {
CommandService,
DataService,
DetailKey,
generateId,
getCommandSource,
getUserContext,
IInvoke,
VERSION_FIRST,
} from '@mbc-cqrs-serverless/core';
import { Create[Entity]Dto } from './dto/create-[entity].dto';
import { Update[Entity]Dto } from './dto/update-[entity].dto';
import { Search[Entity]Dto } from './dto/search-[entity].dto';
import { [Entity]CommandDto } from './dto/[entity]-command.dto';
@Injectable()
export class [Entity]Service {
constructor(
private readonly commandService: CommandService,
private readonly dataService: DataService,
) {}
async create(dto: Create[Entity]Dto, invokeContext: IInvoke) {
const { tenantCode } = getUserContext(invokeContext);
const pk = `[ENTITY]#${tenantCode}`;
const sk = `[ENTITY]#${dto.code}`;
const command = new [Entity]CommandDto({
pk,
sk,
id: generateId(pk, sk),
tenantCode,
code: dto.code,
name: dto.name,
type: '[ENTITY]',
version: VERSION_FIRST,
attributes: dto.attributes,
});
const commandSource = getCommandSource(
basename(__dirname),
this.constructor.name,
'create',
);
return this.commandService.publishAsync(command, {
source: commandSource,
invokeContext,
});
}
async search(dto: Search[Entity]Dto, invokeContext: IInvoke) {
const { tenantCode } = getUserContext(invokeContext);
return this.dataService.listByPk({
pk: `[ENTITY]#${tenantCode}`,
limit: dto.limit,
cursor: dto.cursor,
});
}
async findOne(key: DetailKey, invokeContext: IInvoke) {
return this.dataService.getItem(key);
}
async update(key: DetailKey, dto: Update[Entity]Dto, invokeContext: IInvoke) {
const existingItem = await this.dataService.getItem(key);
const commandSource = getCommandSource(
basename(__dirname),
this.constructor.name,
'update',
);
return this.commandService.publishPartialUpdateAsync(
{
pk: key.pk,
sk: key.sk,
version: existingItem.version,
name: dto.name,
attributes: { ...existingItem.attributes, ...dto.attributes },
},
{
source: commandSource,
invokeContext,
},
);
}
async remove(key: DetailKey, invokeContext: IInvoke) {
const existingItem = await this.dataService.getItem(key);
const commandSource = getCommandSource(
basename(__dirname),
this.constructor.name,
'remove',
);
return this.commandService.publishPartialUpdateAsync(
{
pk: key.pk,
sk: key.sk,
version: existingItem.version,
isDeleted: true,
},
{
source: commandSource,
invokeContext,
},
);
}
}
4. DTOs (dto/ directory)
create-[entity].dto.ts:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
export class Create[Entity]Dto {
@ApiProperty({ description: 'Unique code for the [entity]' })
@IsString()
@IsNotEmpty()
code: string;
@ApiProperty({ description: 'Name of the [entity]' })
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({ description: 'Additional attributes' })
@IsObject()
@IsOptional()
attributes?: Record<string, any>;
}
update-[entity].dto.ts:
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsObject, IsOptional, IsString } from 'class-validator';
export class Update[Entity]Dto {
@ApiPropertyOptional({ description: 'Name of the [entity]' })
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({ description: 'Additional attributes' })
@IsObject()
@IsOptional()
attributes?: Record<string, any>;
}
search-[entity].dto.ts:
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsNumber, IsOptional, IsString } from 'class-validator';
import { Type } from 'class-transformer';
export class Search[Entity]Dto {
@ApiPropertyOptional({ description: 'Maximum number of items to return' })
@IsNumber()
@IsOptional()
@Type(() => Number)
limit?: number;
@ApiPropertyOptional({ description: 'Cursor for pagination' })
@IsString()
@IsOptional()
cursor?: string;
}
[entity]-command.dto.ts:
import { CommandDto } from '@mbc-cqrs-serverless/core';
export class [Entity]CommandDto extends CommandDto {
constructor(partial: Partial<[Entity]CommandDto>) {
super();
Object.assign(this, partial);
}
}
5. Data Sync Handler (handler/[entity]-rds.handler.ts)
import { DataSyncHandler, IDataSyncHandler } from '@mbc-cqrs-serverless/core';
import { CommandModel, DataModel } from '@mbc-cqrs-serverless/core';
import { PrismaService } from '../../prisma/prisma.service';
@DataSyncHandler({ type: '[ENTITY]' })
export class [Entity]DataSyncRdsHandler implements IDataSyncHandler {
constructor(private readonly prismaService: PrismaService) {}
async up(cmd: CommandModel, data: DataModel): Promise<void> {
if (data.isDeleted) {
await this.prismaService.[entity].delete({
where: { id: data.id },
});
return;
}
await this.prismaService.[entity].upsert({
where: { id: data.id },
create: {
id: data.id,
tenantCode: data.tenantCode,
code: data.code,
name: data.name,
attributes: data.attributes,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
},
update: {
name: data.name,
attributes: data.attributes,
updatedAt: data.updatedAt,
},
});
}
async down(cmd: CommandModel, data: DataModel): Promise<void> {
}
}
Naming Conventions
When generating code, follow these naming conventions:
| Item | Convention | Example |
|---|
| Module | PascalCase + "Module" | OrderModule |
| Controller | PascalCase + "Controller" | OrderController |
| Service | PascalCase + "Service" | OrderService |
| DTO | PascalCase + "Dto" | CreateOrderDto |
| Handler | PascalCase + "Handler" | OrderDataSyncRdsHandler |
| File name | kebab-case | order.module.ts |
| DynamoDB PK/SK | SCREAMING_SNAKE_CASE | ORDER#tenantCode |
| Variable | camelCase | orderService |
Directory Structure
Generate files in this structure:
src/
└── [entity]/
├── [entity].module.ts
├── [entity].controller.ts
├── [entity].service.ts
├── dto/
│ ├── create-[entity].dto.ts
│ ├── update-[entity].dto.ts
│ ├── search-[entity].dto.ts
│ └── [entity]-command.dto.ts
└── handler/
└── [entity]-rds.handler.ts
Important Notes
- Always use
publishAsync for command publishing (not publishSync) unless immediate consistency is required
- Include
tenantCode from getUserContext() for multi-tenant support
- Use
VERSION_FIRST (0) for new entities, existing version for updates
- Generate unique IDs using
generateId(pk, sk)
- Use class-validator decorators for DTO validation
- Register DataSyncHandler in the module's
CommandModule.register()
Additional Templates
Event Handler (handler/[entity]-event.handler.ts)
For custom event processing:
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CommandModel, DataModel } from '@mbc-cqrs-serverless/core';
@Injectable()
export class [Entity]EventHandler {
private readonly logger = new Logger([Entity]EventHandler.name);
@OnEvent('[ENTITY].created')
async handleCreated(payload: { command: CommandModel; data: DataModel }) {
this.logger.log(`[Entity] created: ${payload.data.id}`);
}
@OnEvent('[ENTITY].updated')
async handleUpdated(payload: { command: CommandModel; data: DataModel }) {
this.logger.log(`[Entity] updated: ${payload.data.id}`);
}
@OnEvent('[ENTITY].deleted')
async handleDeleted(payload: { command: CommandModel; data: DataModel }) {
this.logger.log(`[Entity] deleted: ${payload.data.id}`);
}
}
Custom Notification Transport (notifications/[name].transport.ts)
For adding a custom pub/sub transport alongside or instead of the built-in AppSync transports:
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NotificationTransport } from '@mbc-cqrs-serverless/core';
import { INotification, INotificationTransport } from '@mbc-cqrs-serverless/core';
@NotificationTransport('[name]')
export class [Name]NotificationTransport implements INotificationTransport {
private readonly logger = new Logger([Name]NotificationTransport.name);
constructor(private readonly config: ConfigService) {}
async sendMessage(notification: INotification): Promise<void> {
this.logger.debug(`sendMessage:: ${notification.action} for ${notification.tenantCode}`);
}
}
Register in NotificationModule (or your app module):
import { NotificationModule } from '@mbc-cqrs-serverless/core';
@Module({
imports: [NotificationModule],
providers: [[Name]NotificationTransport],
})
export class AppModule {}
Activate via environment variable (comma-separated, order does not matter):
NOTIFICATION_TRANSPORTS=[name]
NOTIFICATION_TRANSPORTS=appsync-graphql,[name]
Note: The decorator name '[name]' must exactly match the value in NOTIFICATION_TRANSPORTS. If it does not match, the transport is silently ignored (AP026).
Query Handler for Complex Searches ([entity].query.ts)
For advanced query operations:
import { Injectable } from '@nestjs/common';
import {
DataService,
getUserContext,
IInvoke,
} from '@mbc-cqrs-serverless/core';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class [Entity]QueryService {
constructor(
private readonly dataService: DataService,
private readonly prismaService: PrismaService,
) {}
async searchAdvanced(
dto: AdvancedSearch[Entity]Dto,
invokeContext: IInvoke,
) {
const { tenantCode } = getUserContext(invokeContext);
return this.prismaService.[entity].findMany({
where: {
tenantCode,
...(dto.name && { name: { contains: dto.name } }),
...(dto.status && { status: dto.status }),
...(dto.fromDate && { createdAt: { gte: dto.fromDate } }),
...(dto.toDate && { createdAt: { lte: dto.toDate } }),
},
orderBy: { [dto.sortBy || 'createdAt']: dto.sortOrder || 'desc' },
skip: dto.offset || 0,
take: dto.limit || 20,
});
}
async getHistory(key: DetailKey, invokeContext: IInvoke) {
return this.dataService.listVersions(key);
}
async getStatistics(invokeContext: IInvoke) {
const { tenantCode } = getUserContext(invokeContext);
const [total, byStatus] = await Promise.all([
this.prismaService.[entity].count({ where: { tenantCode } }),
this.prismaService.[entity].groupBy({
by: ['status'],
where: { tenantCode },
_count: true,
}),
]);
return { total, byStatus };
}
}
Elasticsearch Sync Handler (handler/[entity]-es.handler.ts)
For full-text search synchronization:
import { DataSyncHandler, IDataSyncHandler } from '@mbc-cqrs-serverless/core';
import { CommandModel, DataModel } from '@mbc-cqrs-serverless/core';
import { ElasticsearchService } from '../elasticsearch/elasticsearch.service';
@DataSyncHandler({ type: '[ENTITY]' })
export class [Entity]DataSyncEsHandler implements IDataSyncHandler {
constructor(private readonly esService: ElasticsearchService) {}
async up(cmd: CommandModel, data: DataModel): Promise<void> {
if (data.isDeleted) {
await this.esService.delete({
index: '[entity]',
id: data.id,
});
return;
}
await this.esService.index({
index: '[entity]',
id: data.id,
document: {
id: data.id,
tenantCode: data.tenantCode,
code: data.code,
name: data.name,
attributes: data.attributes,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
},
});
}
async down(cmd: CommandModel, data: DataModel): Promise<void> {
}
}
GraphQL Resolver ([entity].resolver.ts)
For GraphQL API:
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { [Entity]Service } from './[entity].service';
import { [Entity] } from './entities/[entity].entity';
import { Create[Entity]Input } from './dto/create-[entity].input';
import { Update[Entity]Input } from './dto/update-[entity].input';
import { INVOKE_CONTEXT, IInvoke } from '@mbc-cqrs-serverless/core';
@Resolver(() => [Entity])
export class [Entity]Resolver {
constructor(private readonly [entity]Service: [Entity]Service) {}
@Query(() => [[Entity]], { name: '[entity]s' })
findAll(@INVOKE_CONTEXT() invokeContext: IInvoke) {
return this.[entity]Service.search({}, invokeContext);
}
@Query(() => [Entity], { name: '[entity]' })
findOne(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Args('pk') pk: string,
@Args('sk') sk: string,
) {
return this.[entity]Service.findOne({ pk, sk }, invokeContext);
}
@Mutation(() => [Entity])
create[Entity](
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Args('input') input: Create[Entity]Input,
) {
return this.[entity]Service.create(input, invokeContext);
}
@Mutation(() => [Entity])
update[Entity](
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Args('pk') pk: string,
@Args('sk') sk: string,
@Args('input') input: Update[Entity]Input,
) {
return this.[entity]Service.update({ pk, sk }, input, invokeContext);
}
@Mutation(() => Boolean)
remove[Entity](
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Args('pk') pk: string,
@Args('sk') sk: string,
) {
return this.[entity]Service.remove({ pk, sk }, invokeContext);
}
}
Group Role Resolver (auth/app-group-role.resolver.ts) — since v1.3.1
Generate this only when the app uses group-based roles. RolesGuard checks direct roles from the JWT custom:roles first, then roles derived from the user's groups in custom:groups. The group → role mapping is not in the JWT — you implement it here. Exactly one resolver is allowed per application.
import {
GroupRoleResolver,
IGroupRoleResolver,
ResolveGroupRolesInput,
} from '@mbc-cqrs-serverless/core';
@GroupRoleResolver()
export class AppGroupRoleResolver implements IGroupRoleResolver {
async resolveRoles({
tenantCode,
groupIds,
claims,
}: ResolveGroupRolesInput): Promise<string[]> {
return [];
}
}
Rules:
- Register the class in your NestJS module
providers. AuthModule is imported automatically via AppModule.forRoot().
- The resolver must be a singleton (resolved once at bootstrap). Do not use
REQUEST/TRANSIENT scope.
- A resolver throw propagates as a 5xx (not a silent 403), so a backend outage is distinguishable from a real access denial.
- Role-name matching is case-sensitive — keep the casing consistent with
@Roles(...).
Customization Questions
Before generating, ask the user these questions to customize the output:
Required Questions
-
"What is the entity name?"
- Example: "Order", "Product", "Customer"
- Used for all file names and class names
-
"What attributes should the entity have?"
- List the fields with types
- Example:
code: string, name: string, price: number, status: OrderStatus
Optional Questions
-
"Do you need RDS synchronization?"
- Yes → Generate
[entity]-rds.handler.ts
- No → Skip DataSyncHandler
-
"Do you need Elasticsearch synchronization?"
- Yes → Generate
[entity]-es.handler.ts
- No → Skip ES handler
-
"Do you need GraphQL support?"
- Yes → Generate
[entity].resolver.ts and GraphQL input types
- No → REST API only
-
"Do you need soft delete or hard delete?"
- Soft delete (default) → Set
isDeleted: true
- Hard delete → Actually delete the record
-
"Do you need event handlers for custom logic?"
- Yes → Generate
[entity]-event.handler.ts
- No → Skip event handler
-
"Do you need advanced query support?"
- Yes → Generate
[entity].query.ts with search, history, statistics
- No → Basic CRUD only
Generation Workflow
┌─────────────────────────────────────────────────────────────┐
│ User Request: "Create Order module" │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Ask Customization Questions │
│ • Entity attributes? │
│ • RDS sync needed? │
│ • GraphQL support? │
│ • Soft/Hard delete? │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Generate Files Based on Answers │
│ │
│ Required: Optional: │
│ ├── module.ts ├── [entity]-rds.handler.ts │
│ ├── controller.ts ├── [entity]-es.handler.ts │
│ ├── service.ts ├── [entity].resolver.ts │
│ └── dto/ ├── [entity]-event.handler.ts │
│ ├── create.dto └── [entity].query.ts │
│ ├── update.dto │
│ └── search.dto │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Post-Generation Instructions │
│ • Register module in AppModule │
│ • Add Prisma model (if RDS) │
│ • Run migrations │
└─────────────────────────────────────────────────────────────┘