// packages/sdk/src/dev/api-server.ts import { Elysia } from 'elysia'; import type { ModuleDeps, ModulePluginFactory } from '../platform/types.js'; import { MODULE_HANDLER_API_PREFIX, resolveModuleRouteBase } from '../module-api-path.js'; import { getDevDbPool } from './db.js'; import { createModuleDb } from '../module-db.js'; import { assertModuleSqlAllowed } from '../sql-guard.js'; import { createLoggedDbQuery, type SqlLogEvent } from '../db-query.js'; import { annotateModuleSql } from '../sql-comment.js'; import { getSqlCommentRequestUserId, runWithSqlCommentRequestContext, } from '../sql-comment-context.js'; import { getModuleRequestUser } from '../platform/module-user.js'; import type { PoolClient } from 'pg'; import { formatDevAppLog } from '../dev-log.js'; export interface DevModuleDepsOptions { moduleName: string; moduleVersion: string; mountPrefix?: string; logSql?: boolean; logEvent?: (event: SqlLogEvent) => void; log?: (message: string) => void; /** 本地模拟 deps.env;默认 {} */ env?: Record; } export function createDevModuleDeps(options: DevModuleDepsOptions): ModuleDeps { const mountPrefix = options.mountPrefix ?? ''; const routeBase = resolveModuleRouteBase(mountPrefix, MODULE_HANDLER_API_PREFIX); const pool = getDevDbPool(); const tag = `${options.moduleName}@${options.moduleVersion}`; const logOpts = { logSql: options.logSql, logEvent: options.logEvent, log: options.log, tag, }; const runAnnotated = async ( execute: (sql: string, params?: unknown[]) => Promise<{ rows: unknown[] }>, sql: string, params?: unknown[], ) => { assertModuleSqlAllowed(sql, options.moduleName); const annotated = annotateModuleSql(sql, { moduleName: options.moduleName, userId: getSqlCommentRequestUserId(), startTime: new Date(), }); return execute(annotated, params); }; // dev 与生产共用同一份模块 SQL 安全规则(assertModuleSqlAllowed), // 让 SET / 事务控制 / DDL 在 stago dev 阶段就报与线上一致的错误。 // 顺序:校验原始 SQL → 注入来源注释 → 交给驱动。 const db = createModuleDb({ query: createLoggedDbQuery(async (sql, params) => { return runAnnotated(async (annotated, p) => { const result = await pool.query(annotated, p); return { rows: result.rows }; }, sql, params); }, logOpts), withClient: async (fn) => { const client: PoolClient = await pool.connect(); try { // rawQuery 供平台内部下发 BEGIN/COMMIT/SET LOCAL 超时,不经过安全规则、不加模块注释 const rawQuery = createLoggedDbQuery(async (sql, params) => { const result = await client.query(sql, params); return { rows: result.rows }; }, logOpts); const scopedQuery = createLoggedDbQuery(async (sql, params) => { return runAnnotated(async (annotated, p) => { const result = await client.query(annotated, p); return { rows: result.rows }; }, sql, params); }, logOpts); return await fn({ query: scopedQuery, rawQuery }); } finally { client.release(); } }, }); const log = (level: 'info' | 'warn' | 'error', message: string, data?: unknown) => { const line = formatDevAppLog(level, message, data); if (level === 'error') console.error(line); else if (level === 'warn') console.warn(line); else console.log(line); }; return { moduleName: options.moduleName, version: options.moduleVersion, mountPrefix, routeBase, db, log: { info: (message, data) => log('info', message, data), warn: (message, data) => log('warn', message, data), error: (message, data) => log('error', message, data), }, auditSink: { async write(record) { log('info', '[dev-audit] 业务审计', record) return { id: `dev-${Date.now()}` } }, }, env: Object.freeze({ ...(options.env ?? {}) }), }; } /** * 从 ModulePluginFactory 创建开发用 Elysia app。 * 包装 fetch:把 x-stago-user 中的登录人 id 写入 SQL 注释请求上下文。 */ export function createDevModuleApp(factory: ModulePluginFactory, deps: ModuleDeps) { const moduleApp = factory(deps); const host = new Elysia({ name: 'stago-dev-module-host' }) .onError(({ error, set }) => { set.status = 500; const message = error instanceof Error ? error.message || error.stack || 'Unknown error' : String(error); return { success: false, error: { code: 'INTERNAL_ERROR', message } }; }) .use(moduleApp); const originalFetch = host.fetch.bind(host); const wrappedFetch = (input: RequestInfo | URL, init?: RequestInit) => { const request = input instanceof Request ? input : new Request(input, init); const user = getModuleRequestUser(request); return runWithSqlCommentRequestContext({ userId: user?.id }, () => originalFetch(request)); }; return Object.assign(host, { fetch: wrappedFetch }) as typeof host; } /** @deprecated 使用 createDevModuleApp */ export function createDevApiServer(factory: ModulePluginFactory, options: DevModuleDepsOptions) { const deps = createDevModuleDeps(options); return createDevModuleApp(factory, deps); } export type DevApiServerOptions = DevModuleDepsOptions;