Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ on:
inputs:
tag:
description: "要发布的 tag(如 @sfmc-bds/sdk@v0.1.0)"
required: true
# 勿 required:true:改本 workflow 的 branch push 会被 GHA 误评估,
# 缺 inputs 时出现空 jobs + "workflow file issue" failure(见 run 29922880511)。
required: false
type: string

jobs:
publish:
# dispatch 放行;push 仅真实 tag 且 @sfmc-bds/ 前缀才跑。
# ref_type==tag 防御误触的 branch push(skipped 绿,而非空 jobs 红)。
if: >-
github.event_name == 'workflow_dispatch' ||
startsWith(github.ref_name, '@sfmc-bds/')
(github.ref_type == 'tag' && startsWith(github.ref_name, '@sfmc-bds/'))
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
7 changes: 4 additions & 3 deletions db-server/src/routes/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,17 @@ export const body = sharedBody;

/**
* v2 失败信封权威形态(DRY + LSP):
* 一律 `{ ok:false, error, code? }`,勿再混用 `{ success:false }`。
* 一律 `{ ok:false, error, code? }`(+ 可选 extra),勿再混用 `{ success:false }`。
* service-routes / db / module-config / index 模块鉴权门应走此助手。
*/
export function jsonV2Fail(
res: ServerResponse,
error: string,
status: number,
code?: string
code?: string,
extra?: Record<string, unknown>
): void {
const payload: Record<string, unknown> = { ok: false, error };
const payload: Record<string, unknown> = { ...(extra ?? {}), ok: false, error };
if (code) payload.code = code;
sharedJson(res, payload, status);
}
Expand Down
16 changes: 8 additions & 8 deletions db-server/src/routes/db-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const result = deps.schemaRegistry!.define(moduleId, req2);
json(res, { success: true, table: result.table, created: result.created }, 200);
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 400);
jsonV2Fail(res, (e as Error).message, 400);
}
return true;
}
Expand All @@ -89,7 +89,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const status = result.ok ? 200 : 400;
json(res, result as unknown as Record<string, unknown>, status);
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
return true;
}
Expand All @@ -109,17 +109,17 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
const result = await deps.txRunner!.run({ moduleId, steps: [step] });
if (!result.ok) {
const code = result.code === "permission_denied" ? 403 : 400;
json(res, { success: false, error: result.error, step: result.step, code: result.code }, code);
jsonV2Fail(res, result.error, code, result.code, { step: result.step });
return true;
}
const first = result.results[0];
json(res, { success: true, ...unwrapResult(first, cand.op) }, 200);
} catch (e) {
if (e instanceof PermissionDeniedError) {
json(res, { success: false, error: e.message, code: "permission_denied" }, 403);
jsonV2Fail(res, e.message, 403, "permission_denied");
return true;
}
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
return true;
}
Expand All @@ -136,12 +136,12 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
};
const result = await deps.txRunner!.run({ moduleId, steps: [step] });
if (!result.ok) {
json(res, { success: false, error: result.error }, 400);
jsonV2Fail(res, result.error, 400, result.code);
return true;
}
json(res, { success: true });
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
return true;
}
Expand All @@ -166,7 +166,7 @@ export function createDbRoutes(depsIn: Partial<DbRoutesDeps>) {
json(res, { success: r.ok });
}
} catch (e) {
json(res, { success: false, error: (e as Error).message }, 500);
jsonV2Fail(res, (e as Error).message, 500);
}
return true;
}
Expand Down
20 changes: 20 additions & 0 deletions db-server/src/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,23 @@ test("ServiceRegistry: missing service → no_such_service (not not_in_requires)
(e: unknown) => e instanceof DispatchError && e.code === "no_such_service"
);
});

test("jsonV2Fail: ok 方言 + extra(step) 合并(LSP/DRY)", async () => {
const { jsonV2Fail } = await import("./routes/_shared.js");
const chunks: Buffer[] = [];
let statusCode = 0;
const res = {
writeHead(code: number) {
statusCode = code;
},
end(body?: string) {
if (body) chunks.push(Buffer.from(body));
},
setHeader() {},
} as unknown as import("node:http").ServerResponse;

jsonV2Fail(res, "boom", 400, "bad_step", { step: 2 });
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
equal(statusCode, 400);
deepEqual(payload, { ok: false, error: "boom", code: "bad_step", step: 2 });
});
22 changes: 22 additions & 0 deletions modules/sdk/@sfmc-sdk/src/module-loader/http-data-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* HttpDB → ConfigManager.DataAdapter 适配器(DIP)。
* ConfigManager 只依赖 DataAdapter 抽象;本文件是 module-loader 侧唯一 HttpDB 装配点。
*/

import { HttpDB } from "../sapi/runtime/httpdb.js";
import type { DataAdapter } from "./internal/config-manager.js";

/** 用 HttpDB 实现 DataAdapter;可选覆盖 db-server 基址。 */
export function createHttpDataAdapter(opts?: { baseUrl?: string }): DataAdapter {
if (opts?.baseUrl) HttpDB.configure({ baseUrl: opts.baseUrl });
return {
checkHealth: async () => {
await HttpDB.checkHealth();
},
getAllConfigs: async () => HttpDB.get("/api/sfmc/configs/all"),
getModules: async () => HttpDB.get("/api/sfmc/modules"),
setAuthToken: (token: string) => {
HttpDB.setAuthToken(token);
},
};
}
1 change: 1 addition & 0 deletions modules/sdk/@sfmc-sdk/src/module-loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export type { ModuleLifecycle, ModuleDescriptor } from "./runtime.js";
export { guardEvent, announceLoaded } from "./runtime.js";
export { installHostBootstrap } from "./install.js";
export type { InstallOptions, HostBackend, ModuleSurface } from "./install.js";
export { createHttpDataAdapter } from "./http-data-adapter.js";
export const SFMC_MODULE_LOADER_VERSION = "0.1.0" as const;
45 changes: 21 additions & 24 deletions modules/sdk/@sfmc-sdk/src/module-loader/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,19 @@
* 1) system.beforeEvents.startup.subscribe:ConfigManager.init() + bootAll + snapshot
* 2) world.afterEvents.worldLoad.subscribe:bootAfterWorldLoad
* 3) system.beforeEvents.shutdown.subscribe:teardown
* 4) bindDataAdapter():注入 db-server HTTP 适配器
* 4) bindDataAdapter():注入 db-server HTTP 适配器(DIP:经 DataAdapter)
* 5) 注册 setModuleGuard 给 Command.trigger 使用
*
* 本批 (Stage A+B):占位实现,host adapters (Command/Permission/HttpDB 等) 在 Stage F
* (core-* 模块迁移)完成后实装。
*/

import { system, world } from "@minecraft/server";
import { createHttpDataAdapter } from "./http-data-adapter.js";
import type { DataAdapter } from "./internal/config-manager.js";
import { ConfigManager } from "./internal/config-manager.js";
import { ModuleRegistry } from "./runtime.js";

export interface HostBackend {
/** 注入 db-server 数据适配器 */
bindDataAdapter(adapter: unknown): void;
bindDataAdapter(adapter: DataAdapter): void;
/** 关闭 db-server HTTP 客户端 */
dispose(): void;
}
Expand All @@ -38,23 +37,28 @@ export interface InstallOptions {
dbServerUrl?: string;
/** 可选注入自定义 HostBackend(测试用) */
hostBackend?: HostBackend;
/**
* 测试/离线可注入自定义 DataAdapter;默认走 HttpDB 实现。
* 高层只依赖 DataAdapter 抽象(DIP),不直接依赖 HttpDB。
*/
dataAdapter?: DataAdapter;
/** 模块 id 列表(默认 undefined = 全部装载,从 catalog 读取) */
enabledModuleIds?: readonly string[];
}

let _installed = false;

export function installHostBootstrap(options: InstallOptions = {}): HostBackend {
if (_installed) return _bootstrapStubBackend();
if (_installed) return _bootstrapBackend();
_installed = true;

// 1) 占位 data adapter:Stage F 之前 ConsoleData 把 readAll 抛 NOT_IMPLEMENTED;
// installHostBootstrap 仍然注册 hooks,只有 ConfigManager.init() 会失败 —
// 这是允许的中间形态。modules/src/index.ts 还可以装运行。
ConfigManager.bindDataAdapter(stubDataAdapter());
if (options.hostBackend) options.hostBackend.bindDataAdapter(undefined);
// DIP:ConfigManager ← DataAdapter;默认 HttpDB,可被 options 替换(测试/自定义 host)
const adapter =
options.dataAdapter ?? createHttpDataAdapter(options.dbServerUrl ? { baseUrl: options.dbServerUrl } : undefined);
ConfigManager.bindDataAdapter(adapter);
if (options.hostBackend) options.hostBackend.bindDataAdapter(adapter);

// 2) 装配 system.events
// 装配 system.events
system.beforeEvents.startup.subscribe(async () => {
try {
await ConfigManager.init();
Expand All @@ -77,25 +81,18 @@ export function installHostBootstrap(options: InstallOptions = {}): HostBackend
} catch {}
});

return _bootstrapStubBackend();
return _bootstrapBackend();
}

function _bootstrapStubBackend(): HostBackend {
function _bootstrapBackend(): HostBackend {
return {
bindDataAdapter: () => undefined,
bindDataAdapter: (adapter: DataAdapter) => {
ConfigManager.bindDataAdapter(adapter);
},
dispose: () => undefined,
};
}

function stubDataAdapter() {
return {
checkHealth: async () => undefined,
getAllConfigs: async () => null,
getModules: async () => null,
setAuthToken: () => undefined,
};
}

function announceLoaded() {
// runtime.ts 内的同名 export;此处仅为了不让 TS 报 unused 警告
const _ref = announceLoadedExported;
Expand Down
24 changes: 17 additions & 7 deletions modules/sdk/@sfmc-sdk/src/sapi/runtime/httpdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,26 @@ import { system } from "@minecraft/server";
import { http, HttpRequest, HttpRequestMethod } from "@minecraft/server-net";
import { isSuccessfulHttpEnvelope } from "./http-envelope.js";

const HOST = "127.0.0.1";
const PORT = 3001;
const BASE_URL = `http://${HOST}:${PORT}`;
/** 默认连本机 db-server;可通过 configure / InstallOptions.dbServerUrl 覆盖(DIP)。 */
let baseUrl = "http://127.0.0.1:3001";
const TIMEOUT = 3;

export class HttpDB {
private static available = true;
private static _lastErrorLog = 0;
private static authToken = "";

/** 注入 db-server 基址(如 http://127.0.0.1:4000),去掉末尾 /。 */
static configure(opts: { baseUrl?: string }): void {
if (opts.baseUrl) {
baseUrl = opts.baseUrl.replace(/\/+$/, "");
}
}

static getBaseUrl(): string {
return baseUrl;
}

static setAuthToken(token: string): void {
this.authToken = token.trim();
}
Expand Down Expand Up @@ -51,10 +61,10 @@ export class HttpDB {
static async checkHealth(): Promise<boolean> {
for (let i = 0; i < 5; i++) {
try {
const res = await http.get(`${BASE_URL}/api/health`);
const res = await http.get(`${baseUrl}/api/health`);
this.available = res.status === 200;
if (this.available) {
console.info(`[HttpDB] 数据库服务连接成功 (${BASE_URL}/api/health)`);
console.info(`[HttpDB] 数据库服务连接成功 (${baseUrl}/api/health)`);
return true;
}
console.error(`[HttpDB] 数据库服务返回异常状态 ${res.status}`);
Expand All @@ -64,7 +74,7 @@ export class HttpDB {
console.info(`[HttpDB] 连接失败,2s 后重试 (${i + 1}/5)...`);
await system.waitTicks(40);
} else {
console.error(`[HttpDB] 连接失败 (${BASE_URL}): ${err}`);
console.error(`[HttpDB] 连接失败 (${baseUrl}): ${err}`);
}
}
}
Expand All @@ -89,7 +99,7 @@ export class HttpDB {
bodyData?: Record<string, unknown>
): Promise<{ status: number; body: string }> {
try {
const req = new HttpRequest(`${BASE_URL}${path}`);
const req = new HttpRequest(`${baseUrl}${path}`);
req.timeout = TIMEOUT;
req.method = method;

Expand Down
Loading