| name | mbc-migrate |
| description | Guide version migrations for MBC CQRS Serverless framework. Use this when upgrading framework versions, migrating from deprecated APIs, or understanding breaking changes between versions. |
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 Migration Guide
This skill helps migrate MBC CQRS Serverless projects between versions.
Version Migration Matrix
| 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) |
| v1.3.2 | v1.3.3 | Low | ATTRIBUTE_LIMIT_SIZE default lowered 389120 → 102400 for Step Functions payload limit (transparent unless overridden) |
Migration Guides
v1.0.16 → v1.0.17
Breaking Change: MasterDataService.search() settingCode behavior
Before (v1.0.16):
const results = await masterDataService.search({
settingCode: 'CONFIG',
});
After (v1.0.17):
const results = await masterDataService.search({
settingCode: 'CONFIG',
});
Migration Steps:
- Review all
MasterDataService.search() calls
- If you relied on partial matching, update to explicit searches:
const configs = await Promise.all([
masterDataService.search({ settingCode: 'CONFIG_A' }),
masterDataService.search({ settingCode: 'CONFIG_B' }),
]);
v1.0.17 → v1.0.18
New Feature: ImportStatusHandler.sendTaskFailure()
Addition:
await importStatusHandler.sendTaskFailure({
taskToken: event.taskToken,
error: 'ValidationError',
cause: 'Invalid CSV format',
});
Migration Steps:
- No breaking changes
- Optional: Use
sendTaskFailure() for better Step Functions integration
v1.0.18 → v1.0.19
Bug Fix: ImportQueueEventHandler error handling
Before (v1.0.18):
- Parent job status not updated correctly on child failures
After (v1.0.19):
- Parent job status properly reflects child job failures
Migration Steps:
- No code changes required
- Verify existing import workflows work correctly
v1.0.19 → v1.0.20
Bug Fix: CsvImportSfnEventHandler status determination
Before (v1.0.19):
- Incorrect status when all rows failed
After (v1.0.20):
- Correct
FAILURE status when processedRows === 0
Migration Steps:
- No code changes required
- Review error handling in import workflows
v1.0.20 → v1.0.21
New Feature: ZIP Finalization Hooks
Addition:
interface IZipFinalizationHook {
afterFinalize(context: ZipFinalizationContext): Promise<void>;
}
@Module({
imports: [
ImportModule.register({
zipFinalizationHooks: [MyCustomFinalizationHook],
}),
],
})
export class AppModule {}
Migration Steps:
- No breaking changes
- Optional: Implement custom finalization hooks for post-import processing
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`,
});
}
}
}
v1.0.21 → v1.0.22
Bug Fix: DynamoDB export S3 filter
Before (v1.0.21):
- Export filter not applied correctly
After (v1.0.22):
- S3 export filter works as expected
Migration Steps:
- No code changes required
v1.0.22 → v1.0.23
New Feature: sendInlineTemplateEmail()
Addition:
await notificationService.sendInlineTemplateEmail({
to: ['user@example.com'],
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:
- No breaking changes
- Optional: Use inline templates instead of SES templates for simple emails
v1.1.0 — Breaking Changes (data migration required)
1. TENANT_COMMON renamed to lowercase
const pk = `MASTER_SETTING#COMMON#${settingCode}`
const pk = `MASTER_SETTING#common#${settingCode}`
- All DynamoDB keys using
#COMMON must be migrated to #common
- New utilities:
normalizeTenantCode(), isCommonTenant()
2. publish() and publishPartialUpdate() removed
this.commandService.publish(...)
this.commandService.publishPartialUpdate(...)
this.commandService.publishAsync(...)
this.commandService.publishPartialUpdateAsync(...)
v1.1.4 — publishSync audit trail (no code changes required)
publishSync now writes a full audit trail to Command and History tables (parity with async pipeline). No migration required; behavior is transparent.
v1.1.5 — CSV Import v2 batch architecture
Step Functions state machine changes required:
finalize_parent_job state is now required
- Row-level progress tracking via
import_tmp table is removed
- Counters (
processedRows, succeededRows, failedRows) are aggregated at completion
v1.2.0 — Breaking Changes
1. publishSync / publishPartialUpdateSync return type change
const result = await commandService.publishSync(entity, options)
console.log(result.pk)
const result = await commandService.publishSync(entity, options)
if (!result) return
console.log(result.pk)
2. SequenceService.genNewSequence() removed
await sequenceService.genNewSequence(...)
await sequenceService.generateSequenceItem(...)
await sequenceService.generateSequenceItemWithProvideSetting(...)
3. Read-Your-Writes (RYW) consistency (new optional feature)
import { DetailKey, Repository } from '@mbc-cqrs-serverless/core'
v1.2.2 — Bug fixes (no code changes required)
- CsvBatchProcessor Poison Pill fix: Per-row try/catch now prevents Head-of-Line Blocking in SQS batches
- ImportQueueEventHandler:
singleImportProcessor.process() now receives event.importEvent instead of event.payload
Both fixes are transparent — no migration steps needed.
v1.2.4 — TaskModule global registration
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.
@Module({
imports: [MasterModule.register({ enableController: true, prismaService: PrismaService })],
})
export class AppModule {}
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:
- Create a factory extending
TaskQueueEventFactory from @mbc-cqrs-serverless/master
- Call
TaskModule.register() once in AppModule
- Remove
TaskModule.register() from all feature modules
Symptom if skipped: App crashes at startup with Nest can't resolve dependencies of MyTaskService (?).
v1.2.5 — ZIP import refactor + MCP anti-pattern expansion (no migration steps required)
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.
- Enhanced ZIP import validation in
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):
- AP016: Missing Error Logging Before Rethrow (High)
- AP017: Incorrect Attribute Merging on Partial Update (High)
- AP018: Missing Swagger Documentation /
@ApiTags (Low)
- AP019: Missing Pagination in List Queries (High)
- AP020: Missing
getCommandSource for Tracing (Low)
@modelcontextprotocol/sdk updated 1.26.0 → 1.29.0.
v1.2.6 — Repository RYW improvements (no migration steps required)
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:
await repository.listItems(
() => rdsQuery(),
{
latestFlg: true,
transformCommand: (cmd) => ({
id: cmd.id,
version: cmd.version,
}),
getVersion: (item) => item.version,
},
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 API Migration
publish() → publishAsync()
Deprecated in: v1.0.0
Removed in: TBD
Before:
await this.commandService.publish(command, options);
After:
await this.commandService.publishAsync(command, options);
Migration Script:
grep -r "\.publish(" --include="*.ts" src/
find src/ -name "*.ts" -exec sed -i '' 's/\.publish(/\.publishAsync(/g' {} \;
publishPartialUpdate() → publishPartialUpdateAsync()
Deprecated in: v1.0.0
Removed in: TBD
Before:
await this.commandService.publishPartialUpdate(command, options);
After:
await this.commandService.publishPartialUpdateAsync(command, options);
publishSync() Considerations
Status: Not deprecated, but use sparingly
Recommendation:
const result = await this.commandService.publishSync(command, options);
await this.commandService.publishAsync(command, options);
Migration Checklist
Before Migration
During Migration
After Migration
Automated Migration Checks
When using this skill, Claude will:
-
Analyze Current Version:
npm list @mbc-cqrs-serverless/core
-
Check for Deprecated Usage:
grep -r "\.publish(" --include="*.ts" src/
grep -r "\.publishPartialUpdate(" --include="*.ts" src/
grep -r "publishSync" --include="*.ts" src/
-
Identify Breaking Changes:
- Compare current usage against migration guide
- Flag any patterns that need updating
-
Generate Migration Plan:
- List all files requiring changes
- Provide specific code updates needed
- Estimate migration effort
Common Migration Issues
Issue: ConditionalCheckFailedException after update
Cause: Version field handling changed
Solution:
const existing = await this.dataService.getItem(key);
await this.commandService.publishPartialUpdateAsync({
...updateData,
version: existing.version,
}, options);
Issue: DataSyncHandler not triggering
Cause: Type mismatch in decorator
Solution:
@DataSyncHandler({ type: 'ORDER' })
export class OrderDataSyncRdsHandler implements IDataSyncHandler {}
Issue: Import job stuck in PROCESSING
Cause: Missing error handling in v1.0.18
Solution: Upgrade to v1.0.19+ for proper error propagation
v1.2.7 → v1.3.0
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:
export const config: Config = {
appsyncEvents: {
enabled: true,
namespace: 'default',
apiKeyExpireDays: 365,
},
notificationTransports: 'appsync-graphql,appsync-event',
}
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
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
Step 3 — Switch to Events API only
Once all clients have migrated to the Events API, update notificationTransports in CDK Config:
notificationTransports: 'appsync-event',
Then redeploy. APPSYNC_ENDPOINT can be removed from the environment.
Channel subscription patterns (for client-side code):
appsyncClient.subscribe(`/${namespace}/${tenantCode}/*`)
appsyncClient.subscribe(`/${namespace}/${tenantCode}/${action}/*`)
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:
- Add
appsyncEvents and notificationTransports to CDK Config and deploy
- Verify events arrive via both transports (dual-publish phase)
- Migrate frontend clients to subscribe via AppSync Events API channels
- Once all clients migrated, set
notificationTransports: 'appsync-event' and redeploy
v1.3.0 → v1.3.1
Change: 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';
@GroupRoleResolver()
export class AppGroupRoleResolver implements IGroupRoleResolver {
async resolveRoles({ tenantCode, groupIds, claims }) {
return ['viewer', 'reporter'];
}
}
Register the class in your module providers. AuthModule is imported automatically via AppModule.forRoot().
Notes / gotchas:
- The resolver must be a singleton — it is resolved once at bootstrap. Avoid
REQUEST/TRANSIENT scope.
- A resolver failure (e.g. DB outage) propagates as a 5xx, not a silent 403, so a backend outage is distinguishable from a genuine access denial. Make the resolver resilient (timeouts/retries) if its backing store can be unavailable.
- Malformed
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.
- Role-name matching is case-sensitive; keep the casing in
custom:roles consistent with your @Roles(...) values.
Migration Steps:
- Upgrade to v1.3.1 — no code changes required; existing
@Roles() checks keep working.
- (Optional) Populate
custom:groups in the JWT and implement one @GroupRoleResolver().
- Register the resolver in your NestJS module
providers.
v1.3.2 → v1.3.3 (no code changes required, unless you hardcoded the old default)
Change: The framework's default ATTRIBUTE_LIMIT_SIZE in generated CDK templates (infra/libs/infra-stack.ts) and env examples (.env.local, .env.example) is lowered from 389120 (380 KB) to 102400 (100 KB).
Why: The old default was sized for DynamoDB's 400 KB item limit, but AWS Step Functions enforces a 256 KB payload limit per state. The command state machine passes the DynamoDB stream event twice per state (input.$ + context.$), so inline attributes above ~110 KB could trigger States.DataLimitExceeded in production. See Debug Guide — States.DataLimitExceeded for the full symptom/cause/fix.
Action required:
- If you never customized
ATTRIBUTE_LIMIT_SIZE and redeploy your CDK stack, the new lower default takes effect automatically — no code changes needed.
- If you explicitly set
ATTRIBUTE_LIMIT_SIZE=389120 (or another value above ~110 KB) in your own .env or CDK stack, lower it to 102400 or less to avoid States.DataLimitExceeded. Attributes above the limit are automatically offloaded to S3, so lowering the value does not lose data.
Version Compatibility Matrix
| 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 |
Getting Help
If you encounter migration issues:
- Check the changelog for detailed notes
- Search GitHub issues
- Review error catalog for specific errors
- Ask Claude Code for specific migration assistance