@oj-module/cli 0.1.16 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/devkit/README.md +11 -10
  2. package/devkit/SKILL.md +35 -18
  3. package/devkit/manual.md +359 -131
  4. package/package.json +2 -2
  5. package/shell-dist/assets/contract.js +0 -55
  6. package/shell-dist/assets/runtime.js +238 -213
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +5 -5
  9. package/src/build.ts +1 -0
  10. package/src/contract/__tests__/check-api.test.ts +74 -0
  11. package/src/contract/__tests__/emit-client.test.ts +132 -0
  12. package/src/contract/__tests__/emit-jsonschema-zod.test.ts +93 -0
  13. package/src/contract/__tests__/mock.test.ts +56 -0
  14. package/src/contract/__tests__/openapi-ir.test.ts +137 -0
  15. package/src/contract/__tests__/run-api.test.ts +67 -0
  16. package/src/contract/check.ts +67 -294
  17. package/src/contract/emit-client.ts +23 -15
  18. package/src/contract/emit-jsonschema-zod.ts +209 -0
  19. package/src/contract/emit-oj-schema.ts +108 -161
  20. package/src/contract/emit-stub.ts +17 -57
  21. package/src/contract/handler-scan.ts +263 -0
  22. package/src/contract/ir.ts +11 -179
  23. package/src/contract/json-schema-sample.ts +72 -0
  24. package/src/contract/mock.ts +28 -100
  25. package/src/contract/openapi-ir.ts +145 -0
  26. package/src/contract/run.ts +71 -120
  27. package/src/dev.ts +20 -18
  28. package/src/init.ts +18 -3
  29. package/src/oj.ts +6 -2
  30. package/src/vendor.ts +8 -4
  31. package/templates/api/config.yaml +1 -0
  32. package/templates/api/src/auth/api.ts +66 -0
  33. package/templates/api/src/notification/_shared/notify.ts +199 -0
  34. package/templates/api/src/notification/api.ts +231 -0
  35. package/templates/api/src/notification/del/api.ts +47 -0
  36. package/templates/api/src/notification/manifest.yaml +3 -1
  37. package/templates/api/src/notification/migrations/0001__create_notifications.sql +1 -1
  38. package/templates/api/src/notification/migrations/0002__notifications_user.sql +9 -0
  39. package/templates/api/src/notification/migrations/0003__notifications_broadcast.sql +22 -0
  40. package/templates/api/src/notification/schema.yaml +23 -1
  41. package/templates/api/src/notification/seed.sql +6 -4
  42. package/templates/api/src/personal-center/upload/api.ts +12 -1
  43. package/templates/env.d.ts +1 -1
  44. package/templates/openapi.json +471 -0
  45. package/templates/web/src/auth/__client/api.schemas.ts +8 -0
  46. package/templates/web/src/auth/__client/api.ts +110 -0
  47. package/templates/web/src/notification/__client/api.schemas.ts +50 -0
  48. package/templates/web/src/notification/__client/api.ts +186 -0
  49. package/templates/web/src/notification/entry.ts +20 -9
  50. package/templates/web/src/notifications/__client/api.schemas.ts +8 -0
  51. package/templates/web/src/notifications/__client/api.ts +80 -0
  52. package/templates/web/src/{notification/client → personal-center/__client}/api.schemas.ts +2 -14
  53. package/templates/web/src/personal-center/__client/api.ts +88 -0
  54. package/templates/web/src/web/__client/api.schemas.ts +8 -0
  55. package/templates/web/src/{notification/client → web/__client}/api.ts +12 -37
  56. package/vendor/host-versions.json +5 -5
  57. package/src/contract/emit-meta.ts +0 -175
  58. package/src/contract/emit-schema.ts +0 -133
  59. package/src/contract/evaluate.ts +0 -88
  60. package/templates/api/src/auth/login/api.ts +0 -27
  61. package/templates/api/src/auth/logout/api.ts +0 -10
  62. package/templates/api/src/auth/refresh/api.ts +0 -26
  63. package/templates/api/src/notification/contract.ts +0 -52
  64. package/templates/api/src/notification/notifications/api.ts +0 -24
  65. package/templates/api/src/notification/notifications/clear/api.ts +0 -13
  66. package/templates/api/src/notification/notifications/read/api.ts +0 -19
  67. package/templates/api/src/notification/notifications/read-all/api.ts +0 -13
  68. package/templates/api/src/personal-center/contract.ts +0 -16
@@ -1,175 +0,0 @@
1
- import type { IrEndpoint } from "./ir";
2
- import { stringify } from "yaml";
3
- import { z } from "zod";
4
-
5
- /**
6
- * AC-D1/D9:routes.json 与 openapi.yaml 发射器。
7
- *
8
- * - routes.json:ojm dev mock 消费;格式 = oj routes.js 规范化
9
- * (去 apiPrefix 首斜杠、不含 base、含模块段),method+pattern 排序保证字节稳定。
10
- * - openapi.yaml:OpenAPI 3.1,文档站(ojm api docs)与外部评审的单一来源。
11
- * z.date() 无法进 JSON Schema——先降级为 datetime string(线上传输本就是 ISO 串)。
12
- * 两者都是提交产物:同输入必须字节稳定,diff 即可评审。
13
- */
14
-
15
- /** routes.json:`[{ method, pattern }]`,pattern 去首斜杠、含模块段(AC-D9) */
16
- export function emitRoutesJson(ir: IrEndpoint[]): string {
17
- const rows = ir
18
- .map(ep => ({ method: ep.method, pattern: ep.fullPath.replace(/^\//, "") }))
19
- .sort((a, b) => (a.method < b.method ? -1 : a.method > b.method ? 1 : a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0));
20
- return `${JSON.stringify(rows, null, "\t")}\n`;
21
- }
22
-
23
- /** z.date() → datetime string(JSON Schema 无 date 类型;线上本就是 ISO 串) */
24
- function replaceDates(schema: unknown): unknown {
25
- const def = (schema as { _zod?: { def?: Record<string, unknown> } })?._zod?.def;
26
- if (!def)
27
- return schema;
28
- switch (def.type) {
29
- case "date":
30
- return z.string().datetime();
31
- case "object": {
32
- const shape: Record<string, unknown> = {};
33
- for (const [k, v] of Object.entries(def.shape as Record<string, unknown>))
34
- shape[k] = replaceDates(v);
35
- return z.object(shape as Record<string, z.ZodType>);
36
- }
37
- case "array":
38
- return z.array(replaceDates(def.element) as z.ZodType);
39
- case "union":
40
- return z.union((def.options as unknown[]).map(s => replaceDates(s) as z.ZodType) as [z.ZodType, ...z.ZodType[]]);
41
- case "optional":
42
- return (replaceDates(def.innerType) as z.ZodType).optional();
43
- case "nullable":
44
- return (replaceDates(def.innerType) as z.ZodType).nullable();
45
- case "default":
46
- return (replaceDates(def.innerType) as z.ZodType).default(def.defaultValue);
47
- default:
48
- return schema;
49
- }
50
- }
51
-
52
- /** zod schema → JSON Schema(剥 $schema——嵌入文档不需要逐份声明 draft) */
53
- function jsonSchema(schema: unknown): Record<string, unknown> {
54
- const out = z.toJSONSchema(replaceDates(schema) as z.ZodType) as Record<string, unknown>;
55
- delete out.$schema;
56
- return out;
57
- }
58
-
59
- /** `{*path}` → `{path}`(OpenAPI 模板无 catch-all 记号) */
60
- function openapiPath(fullPath: string): string {
61
- return fullPath.replace(/\{\*(\w+)\}/g, "{$1}");
62
- }
63
-
64
- function pathParameters(ep: IrEndpoint): Record<string, unknown>[] {
65
- const props = (ep.paramsSchema ? jsonSchema(ep.paramsSchema).properties : {}) as Record<string, unknown> | undefined;
66
- return ep.paramNames.map(name => ({
67
- name,
68
- in: "path",
69
- required: true,
70
- schema: props?.[name] ?? { type: "string" },
71
- }));
72
- }
73
-
74
- function queryParameters(ep: IrEndpoint): Record<string, unknown>[] {
75
- if (!ep.querySchema)
76
- return [];
77
- const s = jsonSchema(ep.querySchema);
78
- const required = new Set((s.required as string[]) ?? []);
79
- return Object.entries((s.properties as Record<string, unknown>) ?? {}).map(([name, schema]) => ({
80
- name,
81
- in: "query",
82
- required: required.has(name),
83
- schema,
84
- }));
85
- }
86
-
87
- function successResponse(ep: IrEndpoint): Record<string, unknown> {
88
- if (ep.raw) {
89
- return {
90
- description: "成功(原始流,非信封)",
91
- content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } },
92
- };
93
- }
94
- const properties: Record<string, unknown> = { code: { type: "integer" }, msg: { type: "string" } };
95
- if (ep.dataSchema)
96
- properties.data = jsonSchema(ep.dataSchema);
97
- return {
98
- description: "成功(oj 信封:code=0)",
99
- content: { "application/json": { schema: { type: "object", properties } } },
100
- };
101
- }
102
-
103
- /** 业务错误响应(§6.2 通道 b:HTTP status=code,信封 {code,msg}) */
104
- function errorResponse(): Record<string, unknown> {
105
- return {
106
- description: "业务错误(oj 信封:code 非 0,HTTP status=code)",
107
- content: { "application/json": { schema: { type: "object", properties: { code: { type: "integer" }, msg: { type: "string" } }, required: ["code"] } } },
108
- };
109
- }
110
-
111
- /** 文件部件 schema(OpenAPI 3.1 binary;multiple → array of binary) */
112
- function filePartSchema(multiple: boolean): Record<string, unknown> {
113
- const binary = { type: "string", format: "binary" };
114
- return multiple ? { type: "array", items: binary } : binary;
115
- }
116
-
117
- /**
118
- * multipart/form-data 请求体:文本部件取 form.fields 的 JSON Schema properties(键序 = 契约声明序),
119
- * 文件部件按声明补 `{ type: "string", format: "binary" }`;required = fields 的 required ∪ 必传文件。
120
- */
121
- function formRequestBody(ep: IrEndpoint): Record<string, unknown> {
122
- const form = ep.form!;
123
- const fields = form.fieldsSchema ? jsonSchema(form.fieldsSchema) : {};
124
- const fieldProps = (fields.properties as Record<string, unknown>) ?? {};
125
- const requiredFields = (fields.required as string[]) ?? [];
126
- const properties: Record<string, unknown> = {};
127
- // 键序固定:先文本部件(契约 shape 序),再文件部件(DSL 数组序)——字节稳定
128
- for (const name of form.fieldNames)
129
- properties[name] = fieldProps[name];
130
- const required = [...requiredFields];
131
- for (const file of form.files) {
132
- properties[file.name] = filePartSchema(file.multiple);
133
- if (file.required)
134
- required.push(file.name);
135
- }
136
- return {
137
- required: true,
138
- content: {
139
- "multipart/form-data": {
140
- schema: { type: "object", properties, ...(required.length ? { required } : {}) },
141
- },
142
- },
143
- };
144
- }
145
-
146
- /** openapi.yaml:OpenAPI 3.1;paths/methods 排序 + 键序固定,保证字节稳定 */
147
- export function emitOpenapiYaml(ir: IrEndpoint[], opts: { title: string, version: string }): string {
148
- const paths: Record<string, Record<string, unknown>> = {};
149
- const sorted = [...ir].sort((a, b) => (a.fullPath < b.fullPath ? -1 : a.fullPath > b.fullPath ? 1 : a.method < b.method ? -1 : 1));
150
- for (const ep of sorted) {
151
- const op: Record<string, unknown> = {};
152
- op.operationId = ep.name;
153
- if (ep.description)
154
- op.description = ep.description;
155
- const parameters = [...pathParameters(ep), ...queryParameters(ep)];
156
- if (parameters.length)
157
- op.parameters = parameters;
158
- if (ep.bodySchema)
159
- op.requestBody = { required: true, content: { "application/json": { schema: jsonSchema(ep.bodySchema) } } };
160
- // form 端点(与 body 互斥):multipart/form-data 请求体(文本部件 + binary 文件部件)
161
- if (ep.form)
162
- op.requestBody = formRequestBody(ep);
163
- op.responses = ep.raw
164
- ? { 200: successResponse(ep) }
165
- // §6.2 错误通道入交付物(评审 F14/m9):业务错误为 default 响应,信封 code 非 0
166
- : { 200: successResponse(ep), default: errorResponse() };
167
- const p = openapiPath(ep.fullPath);
168
- (paths[p] ??= {})[ep.method.toLowerCase()] = op;
169
- }
170
- return stringify({
171
- openapi: "3.1.0",
172
- info: { title: opts.title, version: opts.version },
173
- paths,
174
- });
175
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * 白名单 schema → TS 源码发射器(AC-D12/D6)。
3
- *
4
- * 生成物(api.schemas.ts)里的 schema 是**重新发射的源码**而非跨树
5
- * import——前端模块因此保持自包含,不直接依赖契约源文件。
6
- * 只处理 AC-D12 白名单内的 def.type(IR 构建期已拦截白名单外 schema),
7
- * 遇到意料外结构时报人话错误而非静默错发。
8
- */
9
-
10
- interface ZodDef { [key: string]: unknown }
11
-
12
- function defOf(schema: unknown): ZodDef {
13
- const def = (schema as { _zod?: { def?: ZodDef } })?._zod?.def;
14
- if (!def)
15
- throw new Error("[ojm-api] schema 发射失败:不是 zod schema(白名单校验应已在 IR 阶段拦截,此为内部不一致)。");
16
- return def;
17
- }
18
-
19
- const IDENT = /^[a-z_$][\w$]*$/i;
20
-
21
- /** 对象键发射:合法标识符原样,其余 JSON 引号化(emit-stub 的示例值同款复用) */
22
- export function keyOf(name: string): string {
23
- return IDENT.test(name) ? name : JSON.stringify(name);
24
- }
25
-
26
- /** string_format → zod 方法名映射(白名单内常见格式;regex 单列) */
27
- const STRING_FORMAT_METHOD: Record<string, string> = {
28
- email: "email",
29
- url: "url",
30
- uuid: "uuid",
31
- datetime: "datetime",
32
- date: "date",
33
- time: "time",
34
- };
35
-
36
- function emitChecks(type: string, def: ZodDef): string {
37
- let out = "";
38
- for (const check of (def.checks as { _zod?: { def?: ZodDef } }[]) ?? []) {
39
- const cd = check._zod?.def;
40
- if (!cd)
41
- continue;
42
- switch (cd.check) {
43
- case "min_length":
44
- out += `.min(${cd.minimum})`;
45
- break;
46
- case "max_length":
47
- out += `.max(${cd.maximum})`;
48
- break;
49
- case "length_equals":
50
- out += `.length(${cd.length})`;
51
- break;
52
- case "greater_than":
53
- out += cd.inclusive === false ? `.gt(${cd.value})` : `.min(${cd.value})`;
54
- break;
55
- case "less_than":
56
- out += cd.inclusive === false ? `.lt(${cd.value})` : `.max(${cd.value})`;
57
- break;
58
- case "multiple_of":
59
- out += `.multipleOf(${cd.value})`;
60
- break;
61
- case "number_format":
62
- if (cd.format === "safeint")
63
- out += ".int()";
64
- break;
65
- case "string_format": {
66
- if (cd.format === "regex") {
67
- const pattern = cd.pattern as RegExp;
68
- // 评审 F10:flags 丢失即语义削弱——不可保真,拒绝而非静默降级
69
- if (pattern.flags && pattern.flags !== "u")
70
- throw new Error(`[ojm-api] schema 发射失败:regex /${pattern.source}/${pattern.flags} 带 flags 无法保真发射(白名单只支持无 flags 形态)。`);
71
- out += `.regex(/${pattern.source}/)`;
72
- break;
73
- }
74
- const method = STRING_FORMAT_METHOD[cd.format as string];
75
- if (!method) {
76
- throw new Error(`[ojm-api] schema 发射失败:string format "${String(cd.format)}" 不在可保真映射(${Object.keys(STRING_FORMAT_METHOD).join("/")})——静默放宽校验违背白名单「检出即报错」,请改用已支持格式或纯 string。`);
77
- }
78
- out += `.${method}()`;
79
- break;
80
- }
81
- default:
82
- break;
83
- }
84
- }
85
- return out;
86
- }
87
-
88
- /** 递归发射 schema 为 z 调用链源码 */
89
- export function emitSchemaSource(schema: unknown): string {
90
- const def = defOf(schema);
91
- const type = def.type as string;
92
- switch (type) {
93
- case "string":
94
- return `z.string()${emitChecks(type, def)}`;
95
- case "number":
96
- return `z.number()${emitChecks(type, def)}`;
97
- case "boolean":
98
- return "z.boolean()";
99
- case "date":
100
- // 评审 F1:线上传输本就是 ISO 串(emit-meta/mock/stub 三链一致),
101
- // z.date() 期望 Date 实例会让 dev 校验对线上数据必误报——发 z.iso.datetime()
102
- return "z.iso.datetime()";
103
- case "literal": {
104
- const values = def.values as unknown[];
105
- return `z.literal(${values.length === 1 ? JSON.stringify(values[0]) : JSON.stringify(values)})`;
106
- }
107
- case "enum":
108
- return `z.enum(${JSON.stringify(Object.keys(def.entries as object))})`;
109
- case "union":
110
- return `z.union([${(def.options as unknown[]).map(emitSchemaSource).join(", ")}])`;
111
- case "array":
112
- // min/max 等长度约束同样保真发射(评审 F10:此前静默丢弃)
113
- return `z.array(${emitSchemaSource(def.element)})${emitChecks(type, def)}`;
114
- case "object": {
115
- const entries = Object.entries(def.shape as Record<string, unknown>)
116
- .map(([k, v]) => `\n\t${keyOf(k)}: ${emitSchemaSource(v).replaceAll("\n", "\n\t")},`);
117
- return `z.object({${entries.join("")}\n})`;
118
- }
119
- case "optional":
120
- return `${emitSchemaSource(def.innerType)}.optional()`;
121
- case "nullable":
122
- return `${emitSchemaSource(def.innerType)}.nullable()`;
123
- case "default": {
124
- const value = def.defaultValue;
125
- if (typeof value === "function" || JSON.stringify(value) === undefined) {
126
- throw new Error("[ojm-api] schema 发射失败:default 值必须是 JSON 可序列化字面量(函数/undefined 不支持)。");
127
- }
128
- return `${emitSchemaSource(def.innerType)}.default(${JSON.stringify(value)})`;
129
- }
130
- default:
131
- throw new Error(`[ojm-api] schema 发射失败:类型 "${type}" 不在白名单(应在 IR 阶段被拦截,此为内部不一致)。`);
132
- }
133
- }
@@ -1,88 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { pathToFileURL } from "node:url";
4
-
5
- import { build as esbuild } from "esbuild";
6
-
7
- /**
8
- * AC-D13:契约求值——复用 build.ts 的 `readModuleDefinition` 同款链路
9
- * (esbuild bundle → 工程内临时目录 → 真 import()),但 stub 策略不同:
10
- *
11
- * - `@oj-module/runtime/contract` 保持 external,Node 侧命中**真实现**
12
- * (contract 子路径零浏览器依赖,其 zod 与浏览器侧 runtime re-export 同源钉版,
13
- * 版本一致性由 pnpm catalog + 版本矩阵门禁保证);
14
- * - `@oj-module/runtime` 若被契约误 import,替换为空壳 stub 并告警
15
- * ——runtime 入口是浏览器代码(React/import.meta.env/localStorage),
16
- * Node 下求值即崩;
17
- * - 其余共享依赖全部 external,由 Node 在 import() 时真实解析。
18
- */
19
-
20
- /** runtime 误 import 的空壳 stub(任何具名导出都给占位函数,防 esbuild 报 missing export) */
21
- const RUNTIME_GUARD_STUB = `
22
- const warn = (name) => {
23
- console.warn("[ojm-api] 契约文件不应 import @oj-module/runtime(浏览器代码),已用空壳替代:" + name);
24
- };
25
- const _fn = (...a) => { warn("value"); return a[a.length - 1]; };
26
- export const defineModule = _fn;
27
- export const z = new Proxy({}, { get: () => { warn("z"); return _fn; } });
28
- export default new Proxy({}, { get: () => _fn });
29
- `;
30
-
31
- export async function evaluateContract(
32
- contractFile: string,
33
- projectRoot: string,
34
- ): Promise<Record<string, unknown>> {
35
- if (!fs.existsSync(contractFile)) {
36
- throw new Error(`[ojm-api] 契约文件不存在:${contractFile}——请检查路径(默认发现:api/src/*/contract.ts 或 web/src/*/client/contract.ts)。`);
37
- }
38
-
39
- // 必须落在工程目录内(而非 os.tmpdir):bundle 外部化的依赖在 import() 时
40
- // 需要从工程 node_modules 解析(同 build.ts readModuleDefinition 的理由)
41
- const outDir = fs.mkdtempSync(path.join(projectRoot, ".ojm-tmp-"));
42
- try {
43
- await esbuild({
44
- entryPoints: [contractFile],
45
- entryNames: "entry",
46
- bundle: true,
47
- format: "esm",
48
- platform: "node",
49
- packages: "external",
50
- plugins: [{
51
- name: "ojm-contract-runtime-guard",
52
- setup(build) {
53
- build.onResolve({ filter: /^@oj-module\/runtime$/ }, () => ({
54
- path: "ojm-runtime-guard-stub",
55
- namespace: "ojm-stub",
56
- }));
57
- build.onLoad({ filter: /.*/, namespace: "ojm-stub" }, () => ({
58
- contents: RUNTIME_GUARD_STUB,
59
- loader: "js",
60
- }));
61
- },
62
- }],
63
- outdir: outDir,
64
- loader: { ".ts": "ts", ".tsx": "tsx", ".json": "json" },
65
- logLevel: "silent",
66
- });
67
- const bundled = path.join(outDir, "entry.js");
68
- try {
69
- const mod = await import(pathToFileURL(bundled).href) as Record<string, unknown>;
70
- return mod;
71
- }
72
- catch (e) {
73
- const msg = e instanceof Error ? e.message : String(e);
74
- // 最常见的外部工程缺依赖:给出可操作修复,而不是裸 ERR_MODULE_NOT_FOUND
75
- if (msg.includes("@oj-module/runtime/contract")) {
76
- throw new Error(
77
- "[ojm-api] 契约求值需要工程 node_modules 里能解析 @oj-module/runtime/contract,但当前工程未安装它。\n"
78
- + "修复:把 \"@oj-module/runtime\" 加进工程 devDependencies(版本与宿主对齐)后重跑 pnpm install;"
79
- + "新工程由 ojm init 自动声明。",
80
- );
81
- }
82
- throw e;
83
- }
84
- }
85
- finally {
86
- fs.rmSync(outDir, { recursive: true, force: true });
87
- }
88
- }
@@ -1,27 +0,0 @@
1
- import { issueTokens } from "../_shared/session";
2
-
3
- // POST /api/auth/login —— 登录(devkit 手册 §8:auth 端点是业务路由)。
4
- // 用户不存在与密码错同报 invalid credentials(不泄露用户存在性)。
5
- export default {
6
- async post() {
7
- const body = http.body || {};
8
- const rows = await db.query(
9
- "select id, password_hash, roles from users where username = ?",
10
- [String(body.username ?? "")],
11
- );
12
- const row = rows[0];
13
- if (!row || !(await bcrypt.verify(String(body.password ?? ""), row.password_hash || ""))) {
14
- json.fail(401, "invalid credentials");
15
- return;
16
- }
17
- // roles 列按 JSON 数组串解析,失败回落空。
18
- let roles: string[] = [];
19
- try {
20
- roles = JSON.parse(row.roles || "[]");
21
- }
22
- catch {
23
- roles = [];
24
- }
25
- json.ok(await issueTokens(String(row.id), roles));
26
- },
27
- };
@@ -1,10 +0,0 @@
1
- import { sessionKey } from "../_shared/session";
2
-
3
- // POST /api/auth/logout —— 注销(删 refresh session;access 到期前仍有效)。
4
- export default {
5
- async post() {
6
- const token = String((http.body || {}).refresh_token ?? "");
7
- await kv.del(sessionKey(token));
8
- json.ok(null);
9
- },
10
- };
@@ -1,26 +0,0 @@
1
- import { issueTokens, nowSecs, sessionKey } from "../_shared/session";
2
-
3
- // POST /api/auth/refresh —— 刷新(轮换:旧 refresh 立即失效,一次一用)。
4
- export default {
5
- async post() {
6
- const token = String((http.body || {}).refresh_token ?? "");
7
- const key = sessionKey(token);
8
- const raw = await kv.get(key);
9
- const sess = raw ? JSON.parse(raw) : null;
10
- if (!sess || !(sess.exp > nowSecs())) {
11
- json.fail(401, "invalid or expired refresh token");
12
- return;
13
- }
14
- // session 只存 uid——roles 重查库取最新。
15
- const rows = await db.query("select roles from users where id = ?", [sess.uid]);
16
- let roles: string[] = [];
17
- try {
18
- roles = JSON.parse((rows[0] || {}).roles || "[]");
19
- }
20
- catch {
21
- roles = [];
22
- }
23
- await kv.del(key);
24
- json.ok(await issueTokens(String(sess.uid), roles));
25
- },
26
- };
@@ -1,52 +0,0 @@
1
- import { defineApi, z } from "@oj-module/runtime/contract";
2
-
3
- /**
4
- * notification 模块契约(uni-dev 形态,apiPrefix 字面等于 "/notification",AC-D9)。
5
- * 通知读写端点——前端经 ctx.register.notificationsApi 全量接管内置通知 API(D9 + G4):
6
- * 读 + 单条已读 + 全部已读 + 清空,四端点全必填。
7
- */
8
-
9
- /** 通知项 */
10
- const notificationItem = z.object({
11
- id: z.number(),
12
- avatar: z.string(),
13
- date: z.string(),
14
- isRead: z.boolean().optional(),
15
- message: z.string(),
16
- title: z.string(),
17
- });
18
-
19
- /* 通知列表 */
20
- export const fetchNotifications = defineApi({
21
- apiPrefix: "/notification",
22
- route: "/notifications",
23
- data: z.array(notificationItem),
24
- description: "通知列表",
25
- });
26
-
27
- /* 单条已读 */
28
- export const markRead = defineApi({
29
- apiPrefix: "/notification",
30
- route: "/notifications/read",
31
- method: "POST",
32
- body: z.object({
33
- id: z.number(),
34
- }),
35
- description: "单条通知已读",
36
- });
37
-
38
- /* 全部已读 */
39
- export const markAllRead = defineApi({
40
- apiPrefix: "/notification",
41
- route: "/notifications/read-all",
42
- method: "POST",
43
- description: "全部通知已读",
44
- });
45
-
46
- /* 清空通知 */
47
- export const clearAll = defineApi({
48
- apiPrefix: "/notification",
49
- route: "/notifications/clear",
50
- method: "POST",
51
- description: "清空通知",
52
- });
@@ -1,24 +0,0 @@
1
- // GET /api/notification/notifications —— 通知列表(真实化 P4-2,AC-D9 apiPrefix=/notification)。
2
- // 真实化:查 notifications 表,is_read 映射布尔;Bearer 守卫保护。
3
- // G4:补 id(单条已读的前提,评审 A4——查了 id 必须返回)。
4
- export default {
5
- async get() {
6
- try {
7
- const rows = await db.query(
8
- "SELECT id, avatar, date, is_read, message, title FROM notifications ORDER BY id DESC",
9
- [],
10
- );
11
- json.ok(rows.map(r => ({
12
- id: Number(r.id),
13
- avatar: String(r.avatar),
14
- date: String(r.date),
15
- isRead: Number(r.is_read) === 1,
16
- message: String(r.message),
17
- title: String(r.title),
18
- })));
19
- }
20
- catch (e) {
21
- json.fail(500, String(e));
22
- }
23
- },
24
- };
@@ -1,13 +0,0 @@
1
- // POST /api/notification/notifications/clear —— 清空通知(G4)。
2
- // 清空 notifications 全表。Bearer 守卫保护。
3
- export default {
4
- async post() {
5
- try {
6
- await db.query("DELETE FROM notifications", []);
7
- json.ok(null);
8
- }
9
- catch (e) {
10
- json.fail(500, String(e));
11
- }
12
- },
13
- };
@@ -1,19 +0,0 @@
1
- // POST /api/notification/notifications/read —— 单条通知已读(G4)。
2
- // body: { id };把对应行 is_read 置 1。Bearer 守卫保护。
3
- export default {
4
- async post() {
5
- try {
6
- const body = (http.body ?? {}) as { id?: unknown };
7
- const id = Number(body.id);
8
- if (!Number.isFinite(id)) {
9
- json.fail(400, "id is required");
10
- return;
11
- }
12
- await db.query("UPDATE notifications SET is_read = 1 WHERE id = ?", [id]);
13
- json.ok(null);
14
- }
15
- catch (e) {
16
- json.fail(500, String(e));
17
- }
18
- },
19
- };
@@ -1,13 +0,0 @@
1
- // POST /api/notification/notifications/read-all —— 全部通知已读(G4)。
2
- // 把 notifications 表 is_read 全置 1。Bearer 守卫保护。
3
- export default {
4
- async post() {
5
- try {
6
- await db.query("UPDATE notifications SET is_read = 1", []);
7
- json.ok(null);
8
- }
9
- catch (e) {
10
- json.fail(500, String(e));
11
- }
12
- },
13
- };
@@ -1,16 +0,0 @@
1
- import { defineApi, z } from "@oj-module/runtime/contract";
2
-
3
- /**
4
- * personal-center 模块契约(uni-dev 形态,apiPrefix 字面等于 "/personal-center",AC-D9)。
5
- * 头像/附件上传端点——前端经 ctx.register.uploadApi 接管 form-avatar-item 的 action/headers(见 P4)。
6
- */
7
-
8
- /* 头像上传:实际为 multipart(由 antd Upload 直连 uploadProvider.action 发送,
9
- * 不经 JSON client),故此处不声明 body schema;data 为上传后返回的资源 URL。 */
10
- export const upload = defineApi({
11
- apiPrefix: "/personal-center",
12
- route: "/upload",
13
- method: "POST",
14
- data: z.string(),
15
- description: "头像/附件上传,返回资源 URL",
16
- });