スキル一覧に戻る
avvale

aurora-cqrs

by avvale

0🍴 0📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


name: aurora-cqrs description: > Aurora CQRS Architecture Reference - Understanding the CQRS structure, component relationships, editable zones, and data flow in Aurora/NestJS projects. Trigger: When needing to understand CQRS architecture, component structure, or editable zones. license: MIT metadata: author: aurora version: '1.1' auto_invoke: 'Understanding CQRS architecture, component structure, editable zones'

When to Use

Use this skill as a REFERENCE when:

  • Understanding CQRS architecture and component relationships
  • Learning what Commands, Queries, Handlers, Events, Sagas are
  • Identifying which zones in generated files are editable
  • Understanding the data flow between layers
  • Learning the structure of Services, Repositories, Aggregates, Mappers

⚠️ For IMPLEMENTING business logic in handlers, use aurora-development skill instead.

This skill is for UNDERSTANDING the architecture. The aurora-development skill is for WRITING code.

What is CQRS in Aurora?

CQRS (Command Query Responsibility Segregation) separates read operations (Queries) from write operations (Commands).

Aurora implements CQRS using NestJS CQRS module with:

  • Commands → Change state (Create, Update, Delete)
  • Queries → Read state (Find, Get, Paginate, Count, etc.)
  • Handlers → Execute commands/queries
  • Events → Domain events triggered by aggregates
  • Sagas → Coordinate complex workflows

Architecture Layers

┌─────────────────────────────────────────────────────────────┐
│                    @api Layer (REST/GraphQL)                 │
│  Controllers/Resolvers → Handlers → dispatch Commands/Queries│
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                   @app/application Layer                     │
│  Commands → CommandHandlers → Services                       │
│  Queries  → QueryHandlers   → Services                       │
│  Events   → EventHandlers                                    │
│  Sagas    → React to events                                  │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    @app/domain Layer                         │
│  Aggregates (Entities with events)                           │
│  Value Objects (Immutable types)                             │
│  Repository Interfaces                                       │
│  Mappers (Domain ↔ Response)                                 │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                @app/infrastructure Layer                     │
│  Repository Implementations (Sequelize/TypeORM)              │
│  Seeders, Models                                             │
└─────────────────────────────────────────────────────────────┘

Critical Patterns

⚠️ EDITABLE ZONES (CRITICAL!)

In Aurora-generated files, you can ONLY edit these zones:

// ✅ Command Handler - ONLY the execute() method body
@CommandHandler(CreateTeslaModelCommand)
export class CreateTeslaModelCommandHandler {
    async execute(command: CreateTeslaModelCommand): Promise<void> {
        // ✅ ADD YOUR CUSTOM LOGIC HERE
        // Example: validations, business rules, external calls

        if (command.payload.year < 2008) {
            throw new TeslaYearInvalidException('Tesla first car was in 2008');
        }

        // The rest is generated code (don't modify)
        await this.service.main(payload, command.cQMetadata);
    }
}

DO NOT modify:

  • ❌ Command/Query class definitions
  • ❌ Service main() methods (unless custom service)
  • ❌ Repository interfaces
  • ❌ Aggregates (entities)
  • ❌ Value Objects
  • ❌ Mappers

Marking Custom Code

Always mark custom code with AI-generated comments:

async execute(command: CreateTeslaModelCommand): Promise<void> {
    /* #region AI-generated code */
    // Custom validation
    if (command.payload.year < 2008) {
        throw new TeslaYearInvalidException('Tesla first car was in 2008');
    }

    // Custom business logic
    const isValidPrice = await this.priceValidator.validate(command.payload.price);
    if (!isValidPrice) {
        throw new TeslaPriceInvalidException();
    }
    /* #endregion AI-generated code */

    // Generated code continues...
    await this.service.main(payload, command.cQMetadata);
}

Commands

Command Structure

export class CreateTeslaModelCommand {
    constructor(
        public readonly payload: {
            id: string;
            name: string;
            status: string;
            year: number;
            isActive: boolean;
        },
        public readonly cQMetadata?: CQMetadata,
    ) {}
}

Characteristics:

  • Immutable (readonly)
  • Contains payload (data to create/update)
  • Contains cQMetadata (context: timezone, user, tenant, etc.)
  • Represents an intention to change state

Command Types

// Create single record
CreateTeslaModelCommand;

// Create multiple records
CreateTeslaModelsCommand;

// Update by ID
UpdateTeslaModelByIdCommand;

// Update multiple (by query)
UpdateTeslaModelsCommand;

// Delete by ID
DeleteTeslaModelByIdCommand;

// Delete multiple (by query)
DeleteTeslaModelsCommand;

// Upsert (insert or update)
UpsertTeslaModelCommand;

// Custom command with increment
UpdateAndIncrementTeslaModelsCommand;

Command Handlers

Basic Structure

@CommandHandler(CreateTeslaModelCommand)
export class CreateTeslaModelCommandHandler implements ICommandHandler<CreateTeslaModelCommand> {
    constructor(private readonly service: CreateTeslaModelService) {}

    async execute(command: CreateTeslaModelCommand): Promise<void> {
        // ✅ EDITABLE ZONE - Add custom logic here

        // Call the service (generated)
        await this.service.main(
            {
                id: new TeslaModelId(command.payload.id),
                name: new TeslaModelName(command.payload.name),
                status: new TeslaModelStatus(command.payload.status),
                year: new TeslaModelYear(command.payload.year),
                isActive: new TeslaModelIsActive(command.payload.isActive),
            },
            command.cQMetadata,
        );
    }
}

Handler with Custom Logic

@CommandHandler(CreateTeslaModelCommand)
export class CreateTeslaModelCommandHandler implements ICommandHandler<CreateTeslaModelCommand> {
    constructor(
        private readonly service: CreateTeslaModelService,
        private readonly validator: TeslaModelValidator, // Custom service
        private readonly notifier: NotificationService, // Custom service
    ) {}

    async execute(command: CreateTeslaModelCommand): Promise<void> {
        /* #region AI-generated code */
        // Step 1: Custom validation
        await this.validator.validate(command.payload);

        // Step 2: Business rule
        if (command.payload.year < 2008) {
            throw new TeslaYearInvalidException(
                'Tesla first production car (Roadster) was in 2008',
            );
        }

        // Step 3: Check duplicates (custom logic)
        const exists = await this.service.repository.find({
            queryStatement: {
                where: { name: command.payload.name },
            },
        });

        if (exists) {
            throw new TeslaModelAlreadyExistsException();
        }
        /* #endregion AI-generated code */

        // Step 4: Execute service (generated)
        await this.service.main(
            {
                id: new TeslaModelId(command.payload.id),
                name: new TeslaModelName(command.payload.name),
                status: new TeslaModelStatus(command.payload.status),
                year: new TeslaModelYear(command.payload.year),
                isActive: new TeslaModelIsActive(command.payload.isActive),
            },
            command.cQMetadata,
        );

        /* #region AI-generated code */
        // Step 5: Post-creation actions
        await this.notifier.notifyNewModel(command.payload);
        /* #endregion AI-generated code */
    }
}

Handler with Return Value

// Some handlers return data (like upsert, update)
@CommandHandler(UpsertTeslaModelCommand)
export class UpsertTeslaModelCommandHandler implements ICommandHandler<UpsertTeslaModelCommand> {
    async execute(
        command: UpsertTeslaModelCommand,
    ): Promise<TeslaModelResponse> {
        const model = await this.service.main(payload, command.cQMetadata);

        return this.mapper.mapAggregateToResponse(model);
    }
}

Queries

Query Structure

export class GetTeslaModelsQuery {
    constructor(
        public readonly queryStatement?: QueryStatement,
        public readonly constraint?: QueryStatement,
        public readonly cQMetadata?: CQMetadata,
    ) {}
}

Characteristics:

  • Contains queryStatement (user filters, pagination, sorting)
  • Contains constraint (system/security filters)
  • Contains cQMetadata (context)
  • Used for read operations

Query Types

// Find single record
FindTeslaModelQuery;

// Find by ID
FindTeslaModelByIdQuery;

// Get multiple records
GetTeslaModelsQuery;

// Paginate records
PaginateTeslaModelsQuery;

// Count records
CountTeslaModelQuery;

// Max value
MaxTeslaModelQuery;

// Min value
MinTeslaModelQuery;

// Sum values
SumTeslaModelQuery;

// Raw SQL
RawSQLTeslaModelsQuery;

Query Handlers

Basic Structure

@QueryHandler(GetTeslaModelsQuery)
export class GetTeslaModelsQueryHandler implements IQueryHandler<GetTeslaModelsQuery> {
    private readonly mapper: TeslaModelMapper = new TeslaModelMapper();

    constructor(private readonly service: GetTeslaModelsService) {}

    async execute(query: GetTeslaModelsQuery): Promise<TeslaModelResponse[]> {
        // Get models from service
        const models = await this.service.main(
            query.queryStatement,
            query.constraint,
            query.cQMetadata,
        );

        // Skip mapping if requested
        if (query.cQMetadata?.excludeMapModelToAggregate) {
            return models;
        }

        // Map aggregates to responses
        return this.mapper.mapAggregatesToResponses(models);
    }
}

Handler with Custom Logic

@QueryHandler(GetTeslaModelsQuery)
export class GetTeslaModelsQueryHandler implements IQueryHandler<GetTeslaModelsQuery> {
    private readonly mapper: TeslaModelMapper = new TeslaModelMapper();

    constructor(
        private readonly service: GetTeslaModelsService,
        private readonly cache: CacheService, // Custom service
    ) {}

    async execute(query: GetTeslaModelsQuery): Promise<TeslaModelResponse[]> {
        /* #region AI-generated code */
        // Try cache first
        const cacheKey = this.buildCacheKey(query);
        const cached = await this.cache.get(cacheKey);
        if (cached) return cached;
        /* #endregion AI-generated code */

        // Get from database
        const models = await this.service.main(
            query.queryStatement,
            query.constraint,
            query.cQMetadata,
        );

        if (query.cQMetadata?.excludeMapModelToAggregate) {
            return models;
        }

        const responses = this.mapper.mapAggregatesToResponses(models);

        /* #region AI-generated code */
        // Cache results
        await this.cache.set(cacheKey, responses, 3600);
        /* #endregion AI-generated code */

        return responses;
    }
}

Services

Command Service Structure

@Injectable()
export class CreateTeslaModelService {
    constructor(
        private readonly publisher: EventPublisher,
        private readonly repository: TeslaIModelRepository,
    ) {}

    async main(
        payload: {
            id: TeslaModelId;
            name: TeslaModelName;
            status: TeslaModelStatus;
            year: TeslaModelYear;
            isActive: TeslaModelIsActive;
        },
        cQMetadata?: CQMetadata,
    ): Promise<void> {
        // 1. Create aggregate with factory pattern
        const model = TeslaModel.register(
            payload.id,
            payload.name,
            payload.status,
            payload.year,
            payload.isActive,
            new TeslaModelCreatedAt({ currentTimestamp: true }),
            new TeslaModelUpdatedAt({ currentTimestamp: true }),
            null, // deletedAt
        );

        // 2. Persist to database
        await this.repository.create(model, {
            createOptions: cQMetadata?.repositoryOptions,
        });

        // 3. Merge EventBus with aggregate
        const modelRegister = this.publisher.mergeObjectContext(model);

        // 4. Apply and commit events
        modelRegister.created({
            payload: model,
            cQMetadata,
        });
        modelRegister.commit();
    }
}

Query Service Structure

@Injectable()
export class GetTeslaModelsService {
    constructor(private readonly repository: TeslaIModelRepository) {}

    async main(
        queryStatement?: QueryStatement,
        constraint?: QueryStatement,
        cQMetadata?: CQMetadata,
    ): Promise<TeslaModel[]> {
        return await this.repository.get({
            queryStatement,
            constraint,
            cQMetadata,
        });
    }
}

Custom Service (Not Generated)

@Injectable()
export class TeslaModelValidator {
    constructor(private readonly repository: TeslaIModelRepository) {}

    async validate(payload: any): Promise<void> {
        // Custom validation logic
        if (!payload.name || payload.name.length < 3) {
            throw new TeslaModelNameInvalidException();
        }

        // Check unique constraints
        const exists = await this.repository.find({
            queryStatement: {
                where: { name: payload.name },
            },
        });

        if (exists) {
            throw new TeslaModelAlreadyExistsException();
        }
    }
}

Events

Event Structure

export class CreatedTeslaModelEvent {
    constructor(
        public readonly event: {
            payload: {
                id: string;
                name: string;
                status: string;
                year: number;
                isActive: boolean;
                createdAt?: string;
                updatedAt?: string;
                deletedAt?: string;
            };
            cQMetadata?: CQMetadata;
        },
    ) {}
}

Aggregate Event Methods

export class TeslaModel extends AggregateRoot {
    // Apply created event
    created(event: { payload: TeslaModel; cQMetadata?: CQMetadata }): void {
        this.apply(
            new CreatedTeslaModelEvent({
                payload: {
                    id: event.payload.id.value,
                    name: event.payload.name.value,
                    status: event.payload.status.value,
                    year: event.payload.year.value,
                    isActive: event.payload.isActive.value,
                    createdAt: event.payload.createdAt?.value,
                    updatedAt: event.payload.updatedAt?.value,
                    deletedAt: event.payload.deletedAt?.value,
                },
                cQMetadata: event.cQMetadata,
            }),
        );
    }

    // Apply updated event
    updated(event: { payload: TeslaModel; cQMetadata?: CQMetadata }): void {
        this.apply(new UpdatedTeslaModelEvent({...}));
    }

    // Apply deleted event
    deleted(event: { payload: TeslaModel; cQMetadata?: CQMetadata }): void {
        this.apply(new DeletedTeslaModelEvent({...}));
    }
}

Event Handlers

Basic Structure

@EventsHandler(CreatedTeslaModelEvent)
export class CreatedTeslaModelEventHandler implements IEventHandler<CreatedTeslaModelEvent> {
    handle(event: CreatedTeslaModelEvent): void {
        // ✅ EDITABLE ZONE - Implement event reaction here
        console.log('Tesla model created:', event.event.payload.id);
    }
}

Handler with Custom Logic

@EventsHandler(CreatedTeslaModelEvent)
export class CreatedTeslaModelEventHandler implements IEventHandler<CreatedTeslaModelEvent> {
    constructor(
        private readonly notifier: NotificationService,
        private readonly analytics: AnalyticsService,
    ) {}

    async handle(event: CreatedTeslaModelEvent): Promise<void> {
        /* #region AI-generated code */
        // Send notification
        await this.notifier.notify({
            type: 'MODEL_CREATED',
            data: event.event.payload,
        });

        // Track analytics
        await this.analytics.track('model.created', {
            modelId: event.event.payload.id,
            modelName: event.event.payload.name,
        });

        // Log to audit system
        console.log('New Tesla model created:', {
            id: event.event.payload.id,
            name: event.event.payload.name,
            timestamp: new Date().toISOString(),
        });
        /* #endregion AI-generated code */
    }
}

Sagas

Saga Structure

@Injectable()
export class TeslaModelSagas {
    // Example saga: When model is created, create initial inventory
    @Saga()
    modelCreated = (events$: Observable<any>): Observable<ICommand> => {
        return events$.pipe(
            ofType(CreatedTeslaModelEvent),
            delay(1000),
            map((event) => {
                // Return command to execute
                return new CreateInitialInventoryCommand({
                    modelId: event.event.payload.id,
                    quantity: 0,
                });
            }),
        );
    };

    // Example saga: When model is deleted, delete related records
    @Saga()
    modelDeleted = (events$: Observable<any>): Observable<ICommand> => {
        return events$.pipe(
            ofType(DeletedTeslaModelEvent),
            map((event) => {
                return new DeleteUnitsCommand({
                    modelId: event.event.payload.id,
                });
            }),
        );
    };
}

Aggregates

Aggregate Structure

export class TeslaModel extends AggregateRoot {
    // Properties (Value Objects)
    id: TeslaModelId;
    name: TeslaModelName;
    status: TeslaModelStatus;
    year: TeslaModelYear;
    isActive: TeslaModelIsActive;
    createdAt: TeslaModelCreatedAt;
    updatedAt: TeslaModelUpdatedAt;
    deletedAt: TeslaModelDeletedAt;

    constructor(...) {
        super();
        // Assign properties
    }

    // Factory method
    static register(...): TeslaModel {
        return new TeslaModel(...);
    }

    // Event methods
    created(event: {...}): void { this.apply(new CreatedEvent(...)); }
    updated(event: {...}): void { this.apply(new UpdatedEvent(...)); }
    deleted(event: {...}): void { this.apply(new DeletedEvent(...)); }
}

Repositories

Repository Interface

export abstract class TeslaIModelRepository implements IRepository<TeslaModel> {
    abstract readonly repository: any;

    // Query methods
    abstract paginate(options?: {...}): Promise<Pagination<TeslaModel>>;
    abstract find(options?: {...}): Promise<TeslaModel | null>;
    abstract findById(id: TeslaModelId, options?: {...}): Promise<TeslaModel | null>;
    abstract get(options?: {...}): Promise<TeslaModel[]>;
    abstract count(options?: {...}): Promise<number>;
    abstract max(column: string, options?: {...}): Promise<number>;
    abstract min(column: string, options?: {...}): Promise<number>;
    abstract sum(column: string, options?: {...}): Promise<number>;

    // Command methods
    abstract create(model: TeslaModel, options?: {...}): Promise<void>;
    abstract insert(models: TeslaModel[], options?: {...}): Promise<void>;
    abstract updateById(model: TeslaModel, options?: {...}): Promise<void>;
    abstract update(model: TeslaModel, options?: {...}): Promise<void>;
    abstract upsert(model: TeslaModel, options?: {...}): Promise<void>;
    abstract deleteById(id: TeslaModelId, options?: {...}): Promise<void>;
    abstract delete(options?: {...}): Promise<void>;
}

Mappers

Mapper Structure

export class TeslaModelMapper implements IMapper {
    constructor(public options: MapperOptions = { eagerLoading: true }) {}

    // Model (DB) → Aggregate (Domain)
    mapModelToAggregate(
        model: LiteralObject,
        cQMetadata?: CQMetadata,
    ): TeslaModel {
        return TeslaModel.register(
            new TeslaModelId(model.id),
            new TeslaModelName(model.name),
            new TeslaModelStatus(model.status),
            new TeslaModelYear(model.year),
            new TeslaModelIsActive(model.isActive),
            new TeslaModelCreatedAt(model.createdAt),
            new TeslaModelUpdatedAt(model.updatedAt),
            new TeslaModelDeletedAt(model.deletedAt),
        );
    }

    // Aggregate (Domain) → Response (DTO)
    mapAggregateToResponse(model: TeslaModel): TeslaModelResponse {
        return {
            id: model.id.value,
            name: model.name.value,
            status: model.status.value,
            year: model.year.value,
            isActive: model.isActive.value,
            createdAt: model.createdAt?.value,
            updatedAt: model.updatedAt?.value,
            deletedAt: model.deletedAt?.value,
        };
    }
}

Decision Trees

When to use Command vs Query?

Operation changes state?
├─ YES → Use Command
│  ├─ Create → CreateXCommand + CreateXCommandHandler
│  ├─ Update → UpdateXCommand + UpdateXCommandHandler
│  ├─ Delete → DeleteXCommand + DeleteXCommandHandler
│  └─ Upsert → UpsertXCommand + UpsertXCommandHandler
│
└─ NO → Use Query
   ├─ Single record → FindXQuery + FindXQueryHandler
   ├─ Multiple records → GetXQuery + GetXQueryHandler
   ├─ Paginated → PaginateXQuery + PaginateXQueryHandler
   └─ Aggregate → CountXQuery / MaxXQuery / MinXQuery / SumXQuery

Where to add custom logic?

Need validation before save?
└─ Add in CommandHandler.execute() before service call

Need to react to events?
└─ Create EventHandler

Need to coordinate multiple operations?
└─ Create Saga

Need to transform data?
└─ Use Mapper

Need custom query logic?
└─ Add in QueryHandler.execute() before/after service call

Need reusable business logic?
└─ Create custom Service and inject in Handler

Best Practices

✅ DO

  • Mark all custom code with /* #region AI-generated code */ comments
  • Only edit execute() method body in handlers
  • Create custom services for reusable logic
  • Use dependency injection for custom services
  • Validate data in command handlers before calling service
  • Use QueryStatement for complex filters
  • Map aggregates to responses in query handlers
  • Apply events in services (created, updated, deleted)
  • Use sagas for cross-aggregate coordination
  • Inject EventPublisher in command services
  • Use Value Objects for type safety

❌ DON'T

  • Don't modify generated Command/Query classes
  • Don't modify Service main() methods (create custom services instead)
  • Don't modify Repository interfaces
  • Don't modify Aggregates or Value Objects
  • Don't modify Mappers
  • Don't put business logic in services (put in handlers or custom services)
  • Don't bypass repository (always use repository interface)
  • Don't create commands/queries manually (use Aurora CLI to regenerate)
  • Don't forget to commit events (call aggregate.commit())
  • Don't use direct database access (use repository)

Common Patterns

Pattern 1: Command with Validation

@CommandHandler(CreateTeslaModelCommand)
export class CreateTeslaModelCommandHandler {
    constructor(
        private readonly service: CreateTeslaModelService,
        private readonly validator: TeslaValidator,
    ) {}

    async execute(command: CreateTeslaModelCommand): Promise<void> {
        /* #region AI-generated code */
        // Pre-validation
        await this.validator.validate(command.payload);
        /* #endregion AI-generated code */

        // Execute service
        await this.service.main(payload, command.cQMetadata);
    }
}

Pattern 2: Query with Cache

@QueryHandler(GetTeslaModelsQuery)
export class GetTeslaModelsQueryHandler {
    constructor(
        private readonly service: GetTeslaModelsService,
        private readonly cache: CacheService,
    ) {}

    async execute(query: GetTeslaModelsQuery): Promise<TeslaModelResponse[]> {
        /* #region AI-generated code */
        const cached = await this.cache.get(key);
        if (cached) return cached;
        /* #endregion AI-generated code */

        const models = await this.service.main(...);
        const responses = this.mapper.mapAggregatesToResponses(models);

        /* #region AI-generated code */
        await this.cache.set(key, responses);
        /* #endregion AI-generated code */

        return responses;
    }
}

Pattern 3: Event Handler with Side Effects

@EventsHandler(CreatedTeslaModelEvent)
export class CreatedTeslaModelEventHandler {
    constructor(private readonly notifier: NotificationService) {}

    async handle(event: CreatedTeslaModelEvent): Promise<void> {
        /* #region AI-generated code */
        await this.notifier.sendEmail({
            to: 'admin@tesla.com',
            subject: 'New Model Created',
            body: `Model ${event.event.payload.name} created`,
        });
        /* #endregion AI-generated code */
    }
}

Pattern 4: Saga Coordination

@Injectable()
export class TeslaModelSagas {
    @Saga()
    createRelatedRecords = (events$: Observable<any>): Observable<ICommand> => {
        return events$.pipe(
            ofType(CreatedTeslaModelEvent),
            mergeMap((event) => [
                new CreateInventoryCommand({ modelId: event.event.payload.id }),
                new CreatePricingCommand({ modelId: event.event.payload.id }),
            ]),
        );
    };
}

Resources

  • NestJS CQRS: https://docs.nestjs.com/recipes/cqrs
  • Aurora Core: @aurorajs.dev/core exports CQMetadata, IRepository, etc.
  • Project Structure: .claude/skills/aurora-project-structure/SKILL.md
  • Aurora CLI: .claude/skills/aurora-cli/SKILL.md
  • aurora-development - USE THIS for implementing business logic in handlers
  • aurora-project-structure - Understand where CQRS components live
  • aurora-criteria - Build complex QueryStatements for queries
  • typescript - Type-safe implementation
  • aurora-cli - Regenerate CQRS structure after YAML changes

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です