packages/mcp-server/skills/mbc-migrate/SKILL.md
Guide version migrations for MBC CQRS Serverless framework. Use this when upgrading framework versions, migrating from deprecated APIs, or understanding breaking changes between versions.
npx skillsauth add mbc-net/mbc-cqrs-serverless mbc-migrateInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Before executing this skill, check for updates:
mbc install-skills --check to check if a newer version is availablembc install-skills --force to updateNote: Skip this check if the user explicitly says to skip updates or if you've already checked in this session.
This skill helps migrate MBC CQRS Serverless projects between versions.
| From Version | To Version | Migration Complexity | Key Changes |
|--------------|------------|---------------------|-------------|
| v1.0.16 | v1.0.17 | Low | MasterDataService.search() fix |
| v1.0.17 | v1.0.18 | Low | ImportStatusHandler.sendTaskFailure() |
| v1.0.18 | v1.0.19 | Low | ImportQueueEventHandler error handling |
| v1.0.19 | v1.0.20 | Low | CsvImportSfnEventHandler status fix |
| v1.0.20 | v1.0.21 | Medium | ZIP Finalization Hooks |
| v1.0.21 | v1.0.22 | Low | DynamoDB export filter fix |
| v1.0.22 | v1.0.23 | Low | sendInlineTemplateEmail() |
| v1.0.x | v1.1.0 | High | TENANT_COMMON → lowercase; publish() removed |
| v1.1.x | v1.1.4 | Low | publishSync audit trail (transparent) |
| v1.1.x | v1.1.5 | Medium | CSV import v2 batch architecture |
| v1.1.x | v1.2.0 | High | publishSync null return; genNewSequence() removed |
| v1.2.0 | v1.2.1 | Low | SqsService added (new feature) |
| v1.2.1 | v1.2.2 | Low | CsvBatchProcessor Poison Pill fix (transparent) |
| v1.2.x | v1.2.4 | Medium | TaskModule.register() must be in AppModule |
| v1.2.4 | v1.2.5 | Low | ZIP import refactor (transparent); MCP AP016–AP020 added |
| v1.2.5 | v1.2.6 | Low | Repository RYW improvements (transparent); getVersion API |
| v1.2.7 | v1.3.0 | Low | AppSync Events API support (opt-in, no breaking changes) |
| v1.3.0 | v1.3.1 | Low | Group-based roles (@GroupRoleResolver, custom:groups); UserContext.tenantRoles/tenantGroupIds added (opt-in, no breaking changes) |
Breaking Change: MasterDataService.search() settingCode behavior
Before (v1.0.16):
// settingCode was incorrectly using partial match
const results = await masterDataService.search({
settingCode: 'CONFIG', // Matched 'CONFIG', 'CONFIG_A', 'CONFIG_B'
});
After (v1.0.17):
// settingCode now uses exact match
const results = await masterDataService.search({
settingCode: 'CONFIG', // Only matches 'CONFIG' exactly
});
Migration Steps:
MasterDataService.search() calls// If you need multiple settings
const configs = await Promise.all([
masterDataService.search({ settingCode: 'CONFIG_A' }),
masterDataService.search({ settingCode: 'CONFIG_B' }),
]);
New Feature: ImportStatusHandler.sendTaskFailure()
Addition:
// New method available for explicit failure signaling
await importStatusHandler.sendTaskFailure({
taskToken: event.taskToken,
error: 'ValidationError',
cause: 'Invalid CSV format',
});
Migration Steps:
sendTaskFailure() for better Step Functions integrationBug Fix: ImportQueueEventHandler error handling
Before (v1.0.18):
After (v1.0.19):
Migration Steps:
Bug Fix: CsvImportSfnEventHandler status determination
Before (v1.0.19):
After (v1.0.20):
FAILURE status when processedRows === 0Migration Steps:
New Feature: ZIP Finalization Hooks
Addition:
// New interface for ZIP import finalization
interface IZipFinalizationHook {
afterFinalize(context: ZipFinalizationContext): Promise<void>;
}
// Module registration
@Module({
imports: [
ImportModule.register({
zipFinalizationHooks: [MyCustomFinalizationHook],
}),
],
})
export class AppModule {}
Migration Steps:
Example Implementation:
@Injectable()
export class NotificationFinalizationHook implements IZipFinalizationHook {
constructor(private readonly notificationService: NotificationService) {}
async afterFinalize(context: ZipFinalizationContext): Promise<void> {
const { results, status } = context;
if (status === ImportStatusEnum.SUCCESS) {
await this.notificationService.sendSuccess({
message: `Import completed: ${results.processedRows} rows processed`,
});
} else {
await this.notificationService.sendFailure({
message: `Import failed: ${results.failedRows} rows failed`,
});
}
}
}
Bug Fix: DynamoDB export S3 filter
Before (v1.0.21):
After (v1.0.22):
Migration Steps:
New Feature: sendInlineTemplateEmail()
Addition:
// New method for inline email templates
await notificationService.sendInlineTemplateEmail({
to: ['[email protected]'],
subject: 'Order Confirmation - {{orderId}}',
htmlBody: '<h1>Thank you for your order!</h1><p>Order ID: {{orderId}}</p>',
textBody: 'Thank you for your order! Order ID: {{orderId}}',
templateData: {
orderId: 'ORD-12345',
},
});
Migration Steps:
1. TENANT_COMMON renamed to lowercase
// Before (v1.0.x)
const pk = `MASTER_SETTING#COMMON#${settingCode}`
// After (v1.1.0+)
const pk = `MASTER_SETTING#common#${settingCode}`
#COMMON must be migrated to #commonnormalizeTenantCode(), isCommonTenant()2. publish() and publishPartialUpdate() removed
// Removed — compilation error in v1.1.0+
this.commandService.publish(...)
this.commandService.publishPartialUpdate(...)
// Use instead
this.commandService.publishAsync(...)
this.commandService.publishPartialUpdateAsync(...)
publishSync now writes a full audit trail to Command and History tables (parity with async pipeline). No migration required; behavior is transparent.
Step Functions state machine changes required:
finalize_parent_job state is now requiredimport_tmp table is removedprocessedRows, succeededRows, failedRows) are aggregated at completion1. publishSync / publishPartialUpdateSync return type change
// Before (v1.1.x) — always returned CommandModel
const result = await commandService.publishSync(entity, options)
console.log(result.pk) // safe
// After (v1.2.0+) — returns null when command is not dirty (no-op)
const result = await commandService.publishSync(entity, options)
if (!result) return // no-op
console.log(result.pk) // safe after null check
2. SequenceService.genNewSequence() removed
// Removed — compilation error in v1.2.0+
await sequenceService.genNewSequence(...)
// Use instead
await sequenceService.generateSequenceItem(...)
// or
await sequenceService.generateSequenceItemWithProvideSetting(...)
3. Read-Your-Writes (RYW) consistency (new optional feature)
import { DetailKey, Repository } from '@mbc-cqrs-serverless/core'
// Enable: set RYW_SESSION_TTL_MINUTES env var (e.g. "5")
// Session table {NODE_ENV}-{APP_NAME}-session must be created
// No effect if unset — zero impact on existing projects
singleImportProcessor.process() now receives event.importEvent instead of event.payloadBoth fixes are transparent — no migration steps needed.
Breaking for apps using @mbc-cqrs-serverless/master
TaskModule.register() is now global (global: true) and must be called exactly once in the host AppModule. MasterModule no longer registers TaskModule internally.
// Before (v1.2.3 and earlier) — worked automatically
@Module({
imports: [MasterModule.register({ enableController: true, prismaService: PrismaService })],
})
export class AppModule {}
// After (v1.2.4+) — must register explicitly
import { TaskModule, TaskQueueEventFactory } from '@mbc-cqrs-serverless/master'
@Module({
imports: [
TaskModule.register({ taskQueueEventFactory: MyTaskQueueEventFactory }),
MasterModule.register({ enableController: true, prismaService: PrismaService }),
],
})
export class AppModule {}
Migration steps:
TaskQueueEventFactory from @mbc-cqrs-serverless/masterTaskModule.register() once in AppModuleTaskModule.register() from all feature modulesSymptom if skipped: App crashes at startup with Nest can't resolve dependencies of MyTaskService (?).
Framework changes (transparent):
ZipImportQueueEventHandler removed; ZIP import jobs are now processed directly inside ImportService.ImportEventHandler no longer publishes SQS messages for ZIP_MASTER_JOB events.CreateZipImportDto and improved error handling/logging.No application code changes are required. If you previously imported ZipImportQueueEventHandler directly (uncommon), remove the import.
MCP server changes:
mbc_check_anti_patterns tool expanded from 15 to 20 detectors (AP016–AP020 added):
@ApiTags (Low)getCommandSource for Tracing (Low)@modelcontextprotocol/sdk updated 1.26.0 → 1.29.0.The Repository class now actively purges stale RYW sessions when the data table catches up to or surpasses the session version. This eliminates the "stale override" issue where a user could see their own older write even after an external update was already absorbed into the data table.
Behavior changes (transparent — no code changes required):
getItem: when existing.version >= session.version, the session is purged in the background and the persisted data is returned directly (skipping the unnecessary command-table read).listItemsByPk: synchronized sessions are cleaned up in-place during the merge loop.listItems (RDS path): per-session checks are now parallelized via Promise.all, eliminating sequential N+1 latency.New optional optimization for listItems:
// If your RDS rows already carry `version`, supply `getVersion` to skip the
// extra DynamoDB GetItem entirely when the existing RDS row proves caught-up.
await repository.listItems(
() => rdsQuery(),
{
latestFlg: true,
transformCommand: (cmd) => ({
id: cmd.id,
version: cmd.version,
// ...map other CommandModel fields to your RDS row shape
}),
getVersion: (item) => item.version, // ← new in v1.2.6
},
options,
)
Note: getVersion only short-circuits the update path (when the session's itemId matches an existing RDS row). Create-new items (session present but not yet reflected in RDS) still fetch the command as before — there is no existing row to derive a version from.
All changes are backward-compatible: getVersion is optional, and the cleanup behavior is automatic when RYW_SESSION_TTL_MINUTES is enabled. Projects with RYW disabled (env var unset) are unaffected.
Deprecated in: v1.0.0 Removed in: TBD
Before:
await this.commandService.publish(command, options);
After:
await this.commandService.publishAsync(command, options);
Migration Script:
# Find all occurrences
grep -r "\.publish(" --include="*.ts" src/
# Replace (use with caution)
find src/ -name "*.ts" -exec sed -i '' 's/\.publish(/\.publishAsync(/g' {} \;
Deprecated in: v1.0.0 Removed in: TBD
Before:
await this.commandService.publishPartialUpdate(command, options);
After:
await this.commandService.publishPartialUpdateAsync(command, options);
Status: Not deprecated, but use sparingly
Recommendation:
// Only use publishSync when immediate consistency is required
// Example: User registration where we need the user ID immediately
const result = await this.commandService.publishSync(command, options);
// For most cases, prefer publishAsync
await this.commandService.publishAsync(command, options);
npm installWhen using this skill, Claude will:
Analyze Current Version:
npm list @mbc-cqrs-serverless/core
Check for Deprecated Usage:
# Search for deprecated patterns
grep -r "\.publish(" --include="*.ts" src/
grep -r "\.publishPartialUpdate(" --include="*.ts" src/
grep -r "publishSync" --include="*.ts" src/
Identify Breaking Changes:
Generate Migration Plan:
Cause: Version field handling changed Solution:
// Always fetch current version before update
const existing = await this.dataService.getItem(key);
await this.commandService.publishPartialUpdateAsync({
...updateData,
version: existing.version,
}, options);
Cause: Type mismatch in decorator Solution:
// Ensure type matches entity's type field
@DataSyncHandler({ type: 'ORDER' }) // Must match command.type
export class OrderDataSyncRdsHandler implements IDataSyncHandler {}
Cause: Missing error handling in v1.0.18 Solution: Upgrade to v1.0.19+ for proper error propagation
Change: AppSync Events API support added (opt-in, no breaking changes)
No code changes are required. AppSyncEventsService is registered in NotificationModule automatically but does nothing unless NOTIFICATION_TRANSPORTS includes appsync-event.
To adopt the new Events API transport:
Step 1 — Provision infrastructure (CDK)
Add appsyncEvents and notificationTransports to your Config:
// infra/config/config.ts
export const config: Config = {
appsyncEvents: {
enabled: true,
namespace: 'default', // must match the ChannelNamespace created in AppSync
apiKeyExpireDays: 365,
},
notificationTransports: 'appsync-graphql,appsync-event', // dual-publish during migration
}
Run cdk deploy to create the EventApi and ChannelNamespace. The stack outputs AppSyncEventsHttpEndpoint and AppSyncEventsNamespace.
Step 2 — Enable dual-publish (migration phase)
When appsyncEvents.enabled: true, the CDK stack automatically injects these env vars into Lambda/ECS:
NOTIFICATION_TRANSPORTS=appsync-graphql,appsync-event # dual-publish
APPSYNC_EVENTS_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/event
APPSYNC_EVENTS_NAMESPACE=default
APPSYNC_ENDPOINT=https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql # keep existing
Step 3 — Switch to Events API only
Once all clients have migrated to the Events API, update notificationTransports in CDK Config:
notificationTransports: 'appsync-event', // Events API only
Then redeploy. APPSYNC_ENDPOINT can be removed from the environment.
Channel subscription patterns (for client-side code):
// All events for a tenant
appsyncClient.subscribe(`/${namespace}/${tenantCode}/*`)
// Filtered by action
appsyncClient.subscribe(`/${namespace}/${tenantCode}/${action}/*`)
// Track one specific command result
appsyncClient.subscribe(`/${namespace}/${tenantCode}/${action}/${sanitizedId}`)
Non-alphanumeric characters in id (e.g. #, @ from DynamoDB pk/sk) are sanitized to - automatically on the server side. Client-side code using EventsSubscriptionClientImpl from the example app applies the same sanitization.
Migration Steps:
appsyncEvents and notificationTransports to CDK Config and deploynotificationTransports: 'appsync-event' and redeployChange: Group-based roles added (opt-in, no breaking changes).
RolesGuard now checks direct roles from custom:roles first, then roles derived from the user's groups in custom:groups. UserContext gains two fields — tenantRoles (direct roles array) and tenantGroupIds (group IDs for the active tenant) — while tenantRole (singular) is kept for backward compatibility. No code changes are required to upgrade; existing role checks keep working.
To adopt group-based roles:
Step 1 — Add custom:groups to the JWT (tenant-scoped; group → role mappings are NOT in the token):
{
"custom:roles": "[{\"tenant\":\"tenant-a\",\"role\":\"admin\"}]",
"custom:groups": "[{\"tenant\":\"tenant-a\",\"groups\":[\"sales-team\"]}]"
}
Step 2 — Implement exactly one resolver that maps group IDs to roles (loaded from DynamoDB/RDS/config):
import {
GroupRoleResolver,
IGroupRoleResolver,
} from '@mbc-cqrs-serverless/core';
// Do NOT add @Injectable() — @GroupRoleResolver() already registers the provider
// as a singleton. A second @Injectable() can override the scope and break bootstrap.
@GroupRoleResolver()
export class AppGroupRoleResolver implements IGroupRoleResolver {
async resolveRoles({ tenantCode, groupIds, claims }) {
// Map the user's group IDs to roles for this tenant
return ['viewer', 'reporter'];
}
}
Register the class in your module providers. AuthModule is imported automatically via AppModule.forRoot().
Notes / gotchas:
REQUEST/TRANSIENT scope.custom:groups is tolerated (fail-closed → no group roles), so a bad token degrades to direct-role-only checks rather than crashing.getUserRole() on RolesGuard is deprecated and no longer called by the default guard. For custom authorization, override verifyRole, resolveGroupRoles, or canOverrideTenant instead.custom:roles consistent with your @Roles(...) values.Migration Steps:
@Roles() checks keep working.custom:groups in the JWT and implement one @GroupRoleResolver().providers.| Core Version | CLI Version | NestJS | Node.js | TypeScript | |--------------|-------------|--------|---------|------------| | v1.3.0 | v1.3.0 | 10.x | 18+ | 5.x | | v1.0.23 | v1.0.23 | 10.x | 18+ | 5.x | | v1.0.22 | v1.0.22 | 10.x | 18+ | 5.x | | v1.0.21 | v1.0.21 | 10.x | 18+ | 5.x | | v1.0.20 | v1.0.20 | 10.x | 18+ | 5.x |
If you encounter migration issues:
development
Review code for MBC CQRS Serverless best practices and anti-patterns. Use this when reviewing code that uses MBC CQRS Serverless framework, checking for common mistakes, or validating implementation patterns.
development
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.
development
Debug and troubleshoot MBC CQRS Serverless applications. Use this when encountering errors, investigating issues, or optimizing performance in MBC CQRS Serverless projects.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.