一键导入
create-module
// Create a new NestJS module with repository, service, controller, schema, and Drizzle table definition. Use when adding new feature modules, API endpoints, or business domains.
// Create a new NestJS module with repository, service, controller, schema, and Drizzle table definition. Use when adding new feature modules, API endpoints, or business domains.
Use when releasing mx-core server (apps/core), @mx-space/api-client, or @mx-space/cli (mxs) — version bump, changelog, git tag, Docker build, GitHub Release, and Dokploy redeploy. Triggers on "发版", "release a new version", "cut a release", "bump version", "publish api-client", "publish cli", "release mxs".
Use when an agent must inspect, draft, validate, edit, publish, unpublish, delete, or configure mx-space content through packages/cli or the mxs binary.
Author and review Drizzle SQL migrations safely for rolling deploys. Triggers when editing apps/core/src/database/schema/*.ts or apps/core/src/database/migrations/*.sql, when the user runs drizzle-kit generate, when "lint-migrations" reports a violation, or on prompts like "迁移", "改 schema", "alter table", "add a column", "drop column", "migration safety". Enforces the expand-contract pattern because mx-core ships rolling deploys (Dokploy, 2 replicas) where new and old pods coexist for tens of seconds during cutover.
MX Space API design conventions. Apply when writing controllers, API endpoints, or handling HTTP requests.
Create E2E test file for a specified module. Use when adding end-to-end tests for controllers or unit tests for services and repositories.
Review code for MX Space project conventions. Checks NestJS patterns, Drizzle ORM repositories, Zod schemas, API design, etc.
| name | create-module |
| description | Create a new NestJS module with repository, service, controller, schema, and Drizzle table definition. Use when adding new feature modules, API endpoints, or business domains. |
| argument-hint | <module-name> |
| disable-model-invocation | true |
Create a new NestJS module for MX Space project. Module name: $ARGUMENTS
Create the following files under apps/core/src/modules/<module-name>/:
<module-name>/
├── <name>.module.ts # Module definition
├── <name>.controller.ts # HTTP controller
├── <name>.service.ts # Business logic
├── <name>.repository.ts # Drizzle repository (extends BaseRepository)
├── <name>.schema.ts # Zod validation schemas for API DTOs
├── <name>.types.ts # TypeScript row/input types
└── <name>.enum.ts # Enums (optional, only if needed)
Also add the Drizzle table definition in apps/core/src/database/schema/.
database/schema/<name>.ts)Create a new schema file (or append to an existing one) in apps/core/src/database/schema/.
import { index, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core'
import { createdAt, pkText, refText, tsCol, updatedAt } from './columns'
export const <name>s = pgTable(
'<name>s',
{
id: pkText(),
createdAt: createdAt(),
name: text('name').notNull(),
// Add other columns...
},
(table) => [
// Add indexes and unique constraints
// uniqueIndex('<name>s_name_uniq').on(table.name),
// index('<name>s_created_at_idx').on(table.createdAt),
],
)
Then re-export it from apps/core/src/database/schema/index.ts:
export * from './<name>'
Column helpers (from columns.ts):
pkText() — Snowflake primary key as text (auto-named id)refText(name) — Snowflake foreign-key reference as textcreatedAt() — created_at timestamp with default now()updatedAt() — updated_at nullable timestamptsCol(name) — generic timestamp columnCommon column types:
text('col') — stringinteger('col') — numberboolean('col') — boolean (use .default(false))jsonb('col').$type<T>() — JSON datatext('col').array() — text array (e.g., tags)<name>.types.ts)import type { EntityId } from '~/shared/id/entity-id'
export interface <Name>Row {
id: EntityId
name: string
createdAt: Date
}
export interface <Name>CreateInput {
name: string
}
export type <Name>PatchInput = Partial<<Name>CreateInput>
<name>.repository.ts)import { Inject, Injectable } from '@nestjs/common'
import { desc, eq, sql } from 'drizzle-orm'
import { PG_DB_TOKEN } from '~/constants/system.constant'
import { <name>s } from '~/database/schema'
import {
BaseRepository,
type PaginationResult,
toEntityId,
} from '~/processors/database/base.repository'
import type { AppDatabase } from '~/processors/database/postgres.provider'
import { type EntityId, parseEntityId } from '~/shared/id/entity-id'
import { SnowflakeService } from '~/shared/id/snowflake.service'
import type { <Name>CreateInput, <Name>PatchInput, <Name>Row } from './<name>.types'
const mapRow = (row: typeof <name>s.$inferSelect): <Name>Row => ({
id: toEntityId(row.id) as EntityId,
name: row.name,
createdAt: row.createdAt,
})
@Injectable()
export class <Name>Repository extends BaseRepository {
constructor(
@Inject(PG_DB_TOKEN) db: AppDatabase,
private readonly snowflake: SnowflakeService,
) {
super(db)
}
async list(
page = 1,
size = 10,
filter?: Record<string, unknown>,
): Promise<PaginationResult<<Name>Row>> {
page = Math.max(1, page)
size = Math.min(50, Math.max(1, size))
const offset = (page - 1) * size
const [rows, [{ count }]] = await Promise.all([
this.db
.select()
.from(<name>s)
.orderBy(desc(<name>s.createdAt))
.limit(size)
.offset(offset),
this.db.select({ count: sql<number>`count(*)::int` }).from(<name>s),
])
return {
data: rows.map(mapRow),
pagination: this.paginationOf(Number(count ?? 0), page, size),
}
}
async findAll(): Promise<<Name>Row[]> {
const rows = await this.db
.select()
.from(<name>s)
.orderBy(desc(<name>s.createdAt))
return rows.map(mapRow)
}
async findById(id: EntityId | string): Promise<<Name>Row | null> {
const idBig = parseEntityId(id)
const [row] = await this.db
.select()
.from(<name>s)
.where(eq(<name>s.id, idBig))
.limit(1)
return row ? mapRow(row) : null
}
async create(input: <Name>CreateInput): Promise<<Name>Row> {
const id = this.snowflake.nextId()
const [row] = await this.db
.insert(<name>s)
.values({
id,
name: input.name,
})
.returning()
return mapRow(row)
}
async update(
id: EntityId | string,
patch: <Name>PatchInput,
): Promise<<Name>Row | null> {
const idBig = parseEntityId(id)
const update: Partial<typeof <name>s.$inferInsert> = {}
if (patch.name !== undefined) update.name = patch.name
if (Object.keys(update).length === 0) {
const [existing] = await this.db
.select()
.from(<name>s)
.where(eq(<name>s.id, idBig))
.limit(1)
return existing ? mapRow(existing) : null
}
const [row] = await this.db
.update(<name>s)
.set(update)
.where(eq(<name>s.id, idBig))
.returning()
return row ? mapRow(row) : null
}
async deleteById(id: EntityId | string): Promise<<Name>Row | null> {
const idBig = parseEntityId(id)
const [row] = await this.db
.delete(<name>s)
.where(eq(<name>s.id, idBig))
.returning()
return row ? mapRow(row) : null
}
async count(): Promise<number> {
const [row] = await this.db
.select({ count: sql<number>`count(*)::int` })
.from(<name>s)
return Number(row?.count ?? 0)
}
}
Key patterns:
PG_DB_TOKEN (the Drizzle AppDatabase instance) and SnowflakeServiceparseEntityId(id) to validate incoming Snowflake IDs before queriestoEntityId(row.id) when mapping rows out of the repositorythis.snowflake.nextId() to generate new Snowflake IDs on insert.returning() on insert/update/delete to get the affected row back<name>.service.ts)import { Injectable } from '@nestjs/common'
import { <Name>Repository } from './<name>.repository'
@Injectable()
export class <Name>Service {
constructor(private readonly <name>Repository: <Name>Repository) {}
public get repository() {
return this.<name>Repository
}
// Add business logic methods here.
// Simple CRUD is delegated to the repository.
// Cross-module orchestration, validation, and events go in the service.
}
<name>.schema.ts)import { createZodDto } from 'nestjs-zod'
import { z } from 'zod'
import { zNonEmptyString } from '~/common/zod'
export const <Name>Schema = z.object({
name: zNonEmptyString,
// Add other fields...
})
export class <Name>Dto extends createZodDto(<Name>Schema) {}
export const Partial<Name>Schema = <Name>Schema.partial()
export class Partial<Name>Dto extends createZodDto(Partial<Name>Schema) {}
// Type exports
export type <Name>Input = z.infer<typeof <Name>Schema>
export type Partial<Name>Input = z.infer<typeof Partial<Name>Schema>
ID validation: Use EntityIdDto from ~/shared/dto/id.dto for route params:
import { EntityIdDto } from '~/shared/dto/id.dto'
// In controller: @Param() params: EntityIdDto
// Access: params.id (validated Snowflake string)
Common zod primitives (from ~/common/zod):
zNonEmptyString — z.string().min(1)zCoerceBoolean — coerces string "true"/"1" to booleanzCoerceInt / zCoercePositiveInt — coerced number validatorszPaginationPage / zPaginationSize — pagination defaultszEntityId — validates Snowflake string formatzHttpsUrl — HTTPS URL validatorzEmail(msg) — email validator with message<name>.controller.ts)Option A: Manual controller (for custom routes and logic):
import { Body, Delete, Get, HttpCode, Param, Post, Put, Query } from '@nestjs/common'
import { ApiController } from '~/common/decorators/api-controller.decorator'
import { Auth } from '~/common/decorators/auth.decorator'
import { HTTPDecorators } from '~/common/decorators/http.decorator'
import { EntityIdDto } from '~/shared/dto/id.dto'
import { PagerDto } from '~/shared/dto/pager.dto'
import { <Name>Service } from './<name>.service'
import { <Name>Dto, Partial<Name>Dto } from './<name>.schema'
@ApiController('<name>s')
export class <Name>Controller {
constructor(private readonly <name>Service: <Name>Service) {}
@Get('/')
async getPaginate(@Query() query: PagerDto) {
const { page, size } = query
return this.<name>Service.repository.list(page, size)
}
@Get('/all')
async getAll() {
return this.<name>Service.repository.findAll()
}
@Get('/:id')
async getById(@Param() params: EntityIdDto) {
return this.<name>Service.repository.findById(params.id)
}
@Post('/')
@Auth()
@HTTPDecorators.Idempotence()
async create(@Body() body: <Name>Dto) {
return this.<name>Service.repository.create(body)
}
@Put('/:id')
@Auth()
async update(@Param() params: EntityIdDto, @Body() body: <Name>Dto) {
return this.<name>Service.repository.update(params.id, body)
}
@Delete('/:id')
@Auth()
@HttpCode(204)
async delete(@Param() params: EntityIdDto) {
await this.<name>Service.repository.deleteById(params.id)
}
}
Option B: Auto-CRUD via BasePgCrudFactory (for simple CRUD modules):
import { Get, Query } from '@nestjs/common'
import { BasePgCrudFactory } from '~/transformers/crud-factor.pg.transformer'
import { PagerDto } from '~/shared/dto/pager.dto'
import { <Name>Repository } from './<name>.repository'
export class <Name>Controller extends BasePgCrudFactory({
repository: <Name>Repository,
}) {
// inherits GET /, GET /all, GET /:id, POST /, PUT /:id, PATCH /:id, DELETE /:id
// add custom routes here
}
<name>.module.ts)import { Module } from '@nestjs/common'
import { <Name>Controller } from './<name>.controller'
import { <Name>Repository } from './<name>.repository'
import { <Name>Service } from './<name>.service'
@Module({
controllers: [<Name>Controller],
providers: [<Name>Service, <Name>Repository],
exports: [<Name>Service, <Name>Repository],
})
export class <Name>Module {}
If the module needs to be globally available (used by many other modules), add @Global():
import { Global, Module } from '@nestjs/common'
// ...
@Global()
@Module({ /* ... */ })
export class <Name>Module {}
After creating files, register the module in apps/core/src/app.module.ts:
<Name>Module<Name>Module to the imports arrayIf other modules need to inject the repository by token, add an entry in apps/core/src/processors/database/repository.tokens.ts:
export const POSTGRES_REPOSITORY_TOKENS = {
// ... existing tokens
<name>: Symbol('<Name>Repository'),
} as const
Then provide it in the module:
{
provide: POSTGRES_REPOSITORY_TOKENS.<name>,
useExisting: <Name>Repository,
}
After adding the Drizzle table definition, generate a SQL migration:
pnpm drizzle-kit generate # Run from apps/core/ directory
This creates a new numbered SQL file in apps/core/src/database/migrations/.
@ApiController() instead of @Controller() — adds /api/v2 prefix in productionzEntityId / EntityIdDtodatabase/schema/ using Drizzle pgTable()BaseRepository and inject PG_DB_TOKEN + SnowflakeService@Auth() decorator for authenticated endpoints@HTTPDecorators.Paginator or PagerDto for paginated endpoints@HTTPDecorators.Idempotence() for POST endpoints to prevent duplicatesJSONTransformInterceptor