返回 Skill 列表
extension
分类: 开发与工程无需 API Key

create-container-module

根据ModuleImplementationGuide.md第3.1节的标准结构创建新的容器模块。创建目录布局、必需的文件(Dockerfile、package.json、tsconfig.json、README.md、CHANGELOG.md)、带有模式验证的配置文件、server.ts入口点以及OpenAPI规范。在创建新的微服务容器、脚手架模块结构或按照标准架构设置新服务时使用。

person作者: jakexiaohubgithub

Create Container Module

Scaffolds new container modules following the standard structure from ModuleImplementationGuide.md Section 3.1.

Quick Start

When creating a new container module:

  1. Create standard directory structure
  2. Generate required files (Dockerfile, package.json, tsconfig.json, README.md, CHANGELOG.md)
  3. Set up configuration (config/default.yaml, config/schema.json)
  4. Create src/server.ts entry point
  5. Generate OpenAPI spec template

Standard Directory Layout

Reference: ModuleImplementationGuide.md Section 3.1

containers/[module-name]/
├── Dockerfile
├── package.json
├── tsconfig.json
├── README.md
├── CHANGELOG.md
├── openapi.yaml
├── config/
│   ├── default.yaml
│   └── schema.json
├── src/
│   ├── server.ts
│   ├── config/
│   │   ├── index.ts
│   │   └── types.ts
│   ├── routes/
│   │   ├── index.ts
│   │   └── [resource].ts
│   ├── services/
│   │   └── [Service].ts
│   ├── types/
│   │   └── index.ts
│   └── utils/
│       └── logger.ts
└── tests/
    ├── unit/
    ├── integration/
    └── fixtures/

Required Files

1. Dockerfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY containers/shared/package*.json ./containers/shared/
RUN npm install
COPY containers/[module-name] ./containers/[module-name]
COPY containers/shared ./containers/shared
WORKDIR /app/containers/shared
RUN npm run build
WORKDIR /app/containers/[module-name]
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/containers/[module-name]/dist ./dist
COPY --from=builder /app/containers/[module-name]/node_modules ./node_modules
COPY --from=builder /app/containers/[module-name]/package.json ./
EXPOSE 3XXX
CMD ["node", "dist/server.js"]

2. package.json

{
  "name": "@coder/[module-name]",
  "version": "1.0.0",
  "description": "[Module Description]",
  "main": "dist/server.js",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "lint": "eslint src --ext .ts"
  },
  "dependencies": {
    "@coder/shared": "file:../shared",
    "@fastify/swagger": "^9.1.0",
    "@fastify/swagger-ui": "^5.0.0",
    "fastify": "^5.1.0",
    "js-yaml": "^4.1.0",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^25.0.5",
    "@vitest/coverage-v8": "^2.1.0",
    "tsx": "^4.19.2",
    "typescript": "^5.9.3",
    "vitest": "^4.0.0"
  }
}

3. tsconfig.json

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

4. config/default.yaml

Reference: ModuleImplementationGuide.md Section 4.2

module:
  name: [module-name]
  version: 1.0.0

server:
  port: ${PORT:-3XXX}
  host: ${HOST:-0.0.0.0}

cosmos_db:
  endpoint: ${COSMOS_DB_ENDPOINT}
  key: ${COSMOS_DB_KEY}
  database_id: ${COSMOS_DB_DATABASE_ID:-castiel}
  containers:
    main: [module-name]_data

services:
  auth:
    url: ${AUTH_URL:-http://localhost:3021}
  logging:
    url: ${LOGGING_URL:-http://localhost:3014}

rabbitmq:
  url: ${RABBITMQ_URL}
  exchange: coder_events
  queue: [module-name]_service
  bindings: []

features:
  feature_flag: ${FEATURE_FLAG:-true}

5. config/schema.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["server", "cosmos_db"],
  "properties": {
    "server": {
      "type": "object",
      "required": ["port"],
      "properties": {
        "port": { "type": "number" },
        "host": { "type": "string" }
      }
    },
    "cosmos_db": {
      "type": "object",
      "required": ["endpoint", "key", "database_id"],
      "properties": {
        "endpoint": { "type": "string" },
        "key": { "type": "string" },
        "database_id": { "type": "string" }
      }
    }
  }
}

6. src/server.ts

Reference: containers/auth/src/server.ts

import { randomUUID } from 'crypto';
import Fastify, { FastifyInstance } from 'fastify';
import { initializeDatabase, getDatabaseClient, setupJWT } from '@coder/shared';
import swagger from '@fastify/swagger';
import swaggerUI from '@fastify/swagger-ui';
import { loadConfig } from './config';
import { log } from './utils/logger';

let app: FastifyInstance | null = null;

export async function buildApp(): Promise<FastifyInstance> {
  const config = loadConfig();
  
  const fastify = Fastify({
    logger: false,
    requestIdHeader: 'x-request-id',
    genReqId: () => randomUUID(),
    bodyLimit: 1048576,
    requestTimeout: 30000,
  });

  await fastify.register(swagger, {
    openapi: {
      openapi: '3.0.3',
      info: {
        title: '[Module Name] API',
        version: '1.0.0',
      },
      servers: [{ url: '/api/v1' }],
    },
  });

  await fastify.register(swaggerUI, {
    routePrefix: '/docs',
  });
  
  await setupJWT(fastify, {
    secret: config.jwt?.secret || process.env.JWT_SECRET || 'change-me',
  });
  
  // Initialize database
  initializeDatabase({
    endpoint: config.cosmos_db.endpoint,
    key: config.cosmos_db.key,
    database: config.cosmos_db.database_id,
    containers: config.cosmos_db.containers,
  });
  
  const db = getDatabaseClient();
  
  // Note: Tenant enforcement is handled per-route via tenantEnforcementMiddleware()
  // No need to register globally - use in route preHandler arrays
  
  // Register global error handler
  fastify.setErrorHandler((error: Error & { validation?: unknown; statusCode?: number }, request, reply) => {
    log.error('Request error', error, {
      requestId: request.id,
      path: request.url,
      method: request.method,
      service: '[module-name]',
    });
    
    // Handle validation errors
    if (error.validation) {
      return reply.status(400).send({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Invalid request',
          details: error.validation,
        },
      });
    }
    
    // Generic error response
    return reply.status(error.statusCode || 500).send({
      error: {
        code: 'INTERNAL_ERROR',
        message: process.env.NODE_ENV === 'production'
          ? 'An unexpected error occurred'
          : error.message,
      },
    });
  });
  
  // Register request/response logging hooks
  fastify.addHook('onRequest', async (request) => {
    log.debug('Request received', {
      requestId: request.id,
      method: request.method,
      path: request.url,
      service: '[module-name]',
    });
  });
  
  fastify.addHook('onResponse', async (request, reply) => {
    log.debug('Request completed', {
      requestId: request.id,
      method: request.method,
      path: request.url,
      statusCode: reply.statusCode,
      responseTime: reply.elapsedTime,
      service: '[module-name]',
    });
  });
  
  // Register routes
  const { registerRoutes } = await import('./routes');
  await registerRoutes(fastify, { db, config });
  
  // Health check endpoints (no auth required)
  fastify.get('/health', async () => {
    return {
      status: 'healthy',
      timestamp: new Date().toISOString(),
      service: '[module-name]',
    };
  });
  
  fastify.get('/ready', async () => {
    const db = getDatabaseClient();
    let dbStatus = 'unknown';
    
    try {
      // Test database connection
      const container = db.getContainer(config.cosmos_db.containers.main);
      await container.read();
      dbStatus = 'ok';
    } catch (error) {
      dbStatus = 'error';
    }
    
    const allOk = dbStatus === 'ok';
    
    return {
      status: allOk ? 'ready' : 'not_ready',
      checks: {
        database: { status: dbStatus },
      },
      timestamp: new Date().toISOString(),
      service: '[module-name]',
    };
  });
  
  app = fastify;
  return fastify;
}

export async function start(): Promise<void> {
  try {
    const config = loadConfig();
    const app = await buildApp();
    
    await app.listen({
      port: config.server.port,
      host: config.server.host,
    });
    
    log.info(`[Module Name] service listening on http://${config.server.host}:${config.server.port}`, {
      port: config.server.port,
      host: config.server.host,
      service: '[module-name]',
    });
  } catch (error) {
    log.error('Failed to start [module-name] service', error, { service: '[module-name]' });
    process.exit(1);
  }
}

// Graceful shutdown handler
async function gracefulShutdown(signal: string): Promise<void> {
  log.info(`${signal} received, shutting down gracefully`, { service: '[module-name]' });
  if (app) {
    await app.close();
  }
  process.exit(0);
}

// Graceful shutdown
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

// Handle uncaught exceptions
process.on('uncaughtException', (error: Error) => {
  log.error('Uncaught exception', error, { service: '[module-name]' });
  gracefulShutdown('uncaughtException').catch(() => {
    process.exit(1);
  });
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
  log.error('Unhandled promise rejection', reason as Error, {
    service: '[module-name]',
    promise: promise.toString(),
  });
});

// Start server if this file is run directly
if (require.main === module) {
  start().catch((error) => {
    console.error('Fatal error starting server:', error);
    process.exit(1);
  });
}

7. src/routes/index.ts

Reference: containers/auth/src/routes/index.ts, containers/context-service/src/routes/index.ts

/**
 * Route Registration
 * Per ModuleImplementationGuide Section 7
 */

import { FastifyInstance } from 'fastify';
import { ModuleConfig } from '../config/types';

export async function registerRoutes(
  fastify: FastifyInstance,
  deps: { db: any; config: ModuleConfig }
): Promise<void> {
  // Register resource routes
  const { setupResourceRoutes } = await import('./resource');
  await setupResourceRoutes(fastify, deps);
}

8. src/routes/[resource].ts (Example Route)

Reference: containers/context-service/src/routes/index.ts

import { FastifyInstance } from 'fastify';
import { authenticateRequest, tenantEnforcementMiddleware } from '@coder/shared';
import { AppError } from '@coder/shared';
import { ResourceService } from '../services/ResourceService';

export async function setupResourceRoutes(
  fastify: FastifyInstance,
  deps: { db: any; config: any }
): Promise<void> {
  const service = new ResourceService(deps.db);
  
  // GET /api/v1/resources/:id
  fastify.get<{ Params: { id: string } }>(
    '/api/v1/resources/:id',
    {
      preHandler: [authenticateRequest(), tenantEnforcementMiddleware()],
      schema: {
        description: 'Get resource by ID',
        tags: ['Resources'],
        params: {
          type: 'object',
          properties: {
            id: { type: 'string', format: 'uuid' },
          },
        },
        response: {
          200: {
            type: 'object',
            description: 'Resource details',
          },
        },
      },
    },
    async (request, reply) => {
      // ✅ tenantId is available from tenantEnforcementMiddleware via request.user
      // The middleware extracts X-Tenant-ID header and validates it
      const tenantId = request.user!.tenantId;
      const userId = request.user!.id; // Also available from authenticateRequest
      
      const resource = await service.getResource(tenantId, request.params.id);
      if (!resource) {
        throw new AppError('Resource not found', 404, 'NOT_FOUND');
      }
      
      return reply.send({ data: resource });
    }
  );
  
  // POST /api/v1/resources
  fastify.post<{ Body: CreateResourceInput }>(
    '/api/v1/resources',
    {
      preHandler: [authenticateRequest(), tenantEnforcementMiddleware()],
      schema: {
        description: 'Create a new resource',
        tags: ['Resources'],
        body: {
          type: 'object',
          required: ['name'],
          properties: {
            name: { type: 'string' },
          },
        },
        response: {
          201: {
            type: 'object',
            description: 'Resource created successfully',
          },
        },
      },
    },
    async (request, reply) => {
      // ✅ tenantId and userId available from middleware
      const tenantId = request.user!.tenantId;
      const userId = request.user!.id;
      
      const resource = await service.createResource(tenantId, request.body);
      return reply.status(201).send({ data: resource });
    }
  );
}

Note:

  • authenticateRequest() validates JWT token and attaches user to request.user
  • tenantEnforcementMiddleware() validates X-Tenant-ID header and attaches tenantId to request.user.tenantId
  • Both are required in preHandler array for protected routes
  • The gateway injects X-Tenant-ID header (immutable, from JWT)

9. openapi.yaml

Reference: ModuleImplementationGuide.md Section 7.4

openapi: 3.0.3
info:
  title: [Module Name] API
  version: 1.0.0
servers:
  - url: /api/v1
paths:
  /health:
    get:
      summary: Health check
      responses:
        '200':
          description: Service is healthy

10. README.md

# [Module Name] Module

[Description]

## Features

- Feature 1
- Feature 2

## Quick Start

### Prerequisites
- Node.js 20+
- Azure Cosmos DB NoSQL account

### Installation
\`\`\`bash
npm install
\`\`\`

### Configuration
\`\`\`bash
cp config/default.yaml config/local.yaml
\`\`\`

### Running
\`\`\`bash
npm run dev
\`\`\`

## API Reference

See [OpenAPI Spec](./openapi.yaml)

## Events

### Published Events
- \`[module].event.name\`

### Consumed Events
- \`other.event.name\`

11. src/utils/logger.ts

Reference: containers/auth/src/utils/logger.ts

/**
 * Structured Logger
 * Per ModuleImplementationGuide Section 15.2
 */

const LOG_LEVELS = {
  error: 0,
  warn: 1,
  info: 2,
  debug: 3,
} as const;

type LogLevel = keyof typeof LOG_LEVELS;

const currentLevel: LogLevel = (process.env.LOG_LEVEL as LogLevel) || 'info';

function shouldLog(level: LogLevel): boolean {
  return LOG_LEVELS[level] <= LOG_LEVELS[currentLevel];
}

function formatLog(level: LogLevel, message: string, meta?: Record<string, any>): string {
  const timestamp = new Date().toISOString();
  const logEntry = {
    timestamp,
    level: level.toUpperCase(),
    service: '[module-name]',
    message,
    ...meta,
  };
  return JSON.stringify(logEntry);
}

export const log = {
  error(message: string, error?: Error | unknown, meta?: Record<string, any>): void {
    if (shouldLog('error')) {
      const errorMeta = error instanceof Error 
        ? { error: error.message, stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined }
        : error 
          ? { error: String(error) }
          : {};
      console.error(formatLog('error', message, { ...errorMeta, ...meta }));
    }
  },
  
  warn(message: string, meta?: Record<string, any>): void {
    if (shouldLog('warn')) {
      console.warn(formatLog('warn', message, meta));
    }
  },
  
  info(message: string, meta?: Record<string, any>): void {
    if (shouldLog('info')) {
      console.info(formatLog('info', message, meta));
    }
  },
  
  debug(message: string, meta?: Record<string, any>): void {
    if (shouldLog('debug')) {
      console.debug(formatLog('debug', message, meta));
    }
  },
};

12. CHANGELOG.md

# Changelog

## [1.0.0] - YYYY-MM-DD

### Added
- Initial module creation
- Core functionality

Validation Checklist

  • [ ] All required files created
  • [ ] Directory structure matches Section 3.1
  • [ ] Config uses environment variables (no hardcoded values)
  • [ ] Server.ts follows auth container pattern
  • [ ] OpenAPI spec created
  • [ ] TypeScript config extends root tsconfig.json
  • [ ] Logger utility created in src/utils/logger.ts
  • [ ] All imports use @coder/shared (no direct module imports)
  • [ ] Health check endpoints implemented (/health, /ready)