@oj-module/cli 0.1.17 → 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.
- package/devkit/README.md +11 -10
- package/devkit/SKILL.md +32 -18
- package/devkit/manual.md +318 -131
- package/package.json +2 -2
- package/shell-dist/assets/contract.js +0 -55
- package/shell-dist/assets/runtime.js +206 -212
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +5 -5
- package/src/build.ts +1 -0
- package/src/contract/__tests__/check-api.test.ts +74 -0
- package/src/contract/__tests__/emit-client.test.ts +132 -0
- package/src/contract/__tests__/emit-jsonschema-zod.test.ts +93 -0
- package/src/contract/__tests__/mock.test.ts +56 -0
- package/src/contract/__tests__/openapi-ir.test.ts +137 -0
- package/src/contract/__tests__/run-api.test.ts +67 -0
- package/src/contract/check.ts +67 -294
- package/src/contract/emit-client.ts +23 -15
- package/src/contract/emit-jsonschema-zod.ts +209 -0
- package/src/contract/emit-oj-schema.ts +108 -161
- package/src/contract/emit-stub.ts +17 -57
- package/src/contract/handler-scan.ts +263 -0
- package/src/contract/ir.ts +11 -179
- package/src/contract/json-schema-sample.ts +72 -0
- package/src/contract/mock.ts +28 -100
- package/src/contract/openapi-ir.ts +145 -0
- package/src/contract/run.ts +71 -120
- package/src/dev.ts +20 -18
- package/src/init.ts +18 -3
- package/src/oj.ts +6 -2
- package/src/vendor.ts +8 -4
- package/templates/api/config.yaml +1 -0
- package/templates/api/src/auth/api.ts +66 -0
- package/templates/api/src/notification/_shared/notify.ts +199 -0
- package/templates/api/src/notification/api.ts +231 -0
- package/templates/api/src/notification/del/api.ts +47 -0
- package/templates/api/src/notification/manifest.yaml +3 -1
- package/templates/api/src/notification/migrations/0001__create_notifications.sql +1 -1
- package/templates/api/src/notification/migrations/0002__notifications_user.sql +9 -0
- package/templates/api/src/notification/migrations/0003__notifications_broadcast.sql +22 -0
- package/templates/api/src/notification/schema.yaml +23 -1
- package/templates/api/src/notification/seed.sql +6 -4
- package/templates/api/src/personal-center/upload/api.ts +12 -1
- package/templates/env.d.ts +1 -1
- package/templates/openapi.json +471 -0
- package/templates/web/src/auth/__client/api.schemas.ts +8 -0
- package/templates/web/src/auth/__client/api.ts +110 -0
- package/templates/web/src/notification/__client/api.schemas.ts +50 -0
- package/templates/web/src/notification/{client → __client}/api.ts +29 -29
- package/templates/web/src/notification/entry.ts +11 -9
- package/templates/web/src/notifications/__client/api.schemas.ts +8 -0
- package/templates/web/src/notifications/__client/api.ts +80 -0
- package/templates/web/src/personal-center/__client/api.schemas.ts +10 -0
- package/templates/web/src/personal-center/__client/api.ts +88 -0
- package/templates/web/src/web/__client/api.schemas.ts +8 -0
- package/templates/web/src/web/__client/api.ts +110 -0
- package/vendor/host-versions.json +5 -5
- package/src/contract/emit-meta.ts +0 -175
- package/src/contract/emit-schema.ts +0 -133
- package/src/contract/evaluate.ts +0 -88
- package/templates/api/src/auth/login/api.ts +0 -27
- package/templates/api/src/auth/logout/api.ts +0 -10
- package/templates/api/src/auth/refresh/api.ts +0 -26
- package/templates/api/src/notification/contract.ts +0 -98
- package/templates/api/src/notification/notifications/api.ts +0 -24
- package/templates/api/src/notification/notifications/clear/api.ts +0 -13
- package/templates/api/src/notification/notifications/read/api.ts +0 -19
- package/templates/api/src/notification/notifications/read-all/api.ts +0 -13
- package/templates/api/src/personal-center/contract.ts +0 -16
- package/templates/web/src/notification/client/api.schemas.ts +0 -50
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
|
|
3
|
+
// AC-D15:仅供 DEV 校验动态 import,生产构建不进产物
|
|
4
|
+
import { z } from "@oj-module/runtime";
|
|
5
|
+
|
|
6
|
+
export const schemas = {
|
|
7
|
+
"notificationNotificationsGet": {
|
|
8
|
+
data: z.array(z.object({
|
|
9
|
+
"id": z.string(),
|
|
10
|
+
"scope": z.enum(["user", "broadcast"]),
|
|
11
|
+
"avatar": z.string(),
|
|
12
|
+
"date": z.string(),
|
|
13
|
+
"isRead": z.boolean().optional(),
|
|
14
|
+
"message": z.string(),
|
|
15
|
+
"title": z.string(),
|
|
16
|
+
"link": z.string().nullable().optional(),
|
|
17
|
+
"category": z.enum(["notice", "pm", "mention"]).optional()
|
|
18
|
+
})),
|
|
19
|
+
},
|
|
20
|
+
"notificationNotificationsReadPost": {
|
|
21
|
+
body: z.object({
|
|
22
|
+
"id": z.string()
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
"notificationPushPost": {
|
|
26
|
+
body: z.object({
|
|
27
|
+
"userId": z.union([z.string(), z.number()]).optional(),
|
|
28
|
+
"title": z.string(),
|
|
29
|
+
"message": z.string(),
|
|
30
|
+
"link": z.string().optional(),
|
|
31
|
+
"avatar": z.string().optional(),
|
|
32
|
+
"category": z.enum(["notice", "pm", "mention"]).optional()
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
"notificationPushBroadcastPost": {
|
|
36
|
+
body: z.object({
|
|
37
|
+
"title": z.string(),
|
|
38
|
+
"message": z.string(),
|
|
39
|
+
"link": z.string().optional(),
|
|
40
|
+
"avatar": z.string().optional(),
|
|
41
|
+
"category": z.enum(["notice", "pm", "mention"]).optional()
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
"notificationDelPost": {
|
|
45
|
+
body: z.object({
|
|
46
|
+
"id": z.string(),
|
|
47
|
+
"userId": z.union([z.string(), z.number()]).optional()
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -33,7 +33,7 @@ export function createNotificationClient(ctx: ModuleContext) {
|
|
|
33
33
|
}
|
|
34
34
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
35
|
bindRequest(ctx.utils.request);
|
|
36
|
-
return {
|
|
36
|
+
return { notificationNotificationsGet, notificationNotificationsReadPost, notificationNotificationsReadAllPost, notificationNotificationsClearPost, notificationPushPost, notificationPushBroadcastPost, notificationDelPost };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -64,14 +64,22 @@ async function toApiError(e: unknown): Promise<unknown> {
|
|
|
64
64
|
return e;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export
|
|
67
|
+
export type NotificationNotificationsGetData = z.infer<(typeof schemas)["notificationNotificationsGet"]["data"]>;
|
|
68
|
+
|
|
69
|
+
export async function notificationNotificationsGet(): Promise<NotificationNotificationsGetData> {
|
|
68
70
|
const client = ensureReq();
|
|
69
71
|
try {
|
|
70
|
-
const env = await client.
|
|
72
|
+
const env = await client.get(`notification/notifications`).json<OjEnvelope<NotificationNotificationsGetData>>();
|
|
71
73
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
72
74
|
if (typeof env.code === "number" && env.code !== 0)
|
|
73
75
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
74
|
-
const data = env.data as
|
|
76
|
+
const data = env.data as NotificationNotificationsGetData;
|
|
77
|
+
if (import.meta.env.DEV) {
|
|
78
|
+
const { schemas } = await import("./api.schemas");
|
|
79
|
+
const r = schemas["notificationNotificationsGet"].data.safeParse(data);
|
|
80
|
+
if (!r.success)
|
|
81
|
+
throw new ContractApiError(-1, `[契约违例] notificationNotificationsGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
82
|
+
}
|
|
75
83
|
return data;
|
|
76
84
|
}
|
|
77
85
|
catch (e) {
|
|
@@ -79,12 +87,12 @@ export async function clearAll(): Promise<unknown> {
|
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
export type
|
|
90
|
+
export type NotificationNotificationsReadPostBody = z.input<(typeof schemas)["notificationNotificationsReadPost"]["body"]>;
|
|
83
91
|
|
|
84
|
-
export async function
|
|
92
|
+
export async function notificationNotificationsReadPost(body: NotificationNotificationsReadPostBody): Promise<unknown> {
|
|
85
93
|
const client = ensureReq();
|
|
86
94
|
try {
|
|
87
|
-
const env = await client.post(`notification/
|
|
95
|
+
const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<unknown>>();
|
|
88
96
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
89
97
|
if (typeof env.code === "number" && env.code !== 0)
|
|
90
98
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
@@ -96,22 +104,14 @@ export async function del(body: DelBody): Promise<unknown> {
|
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
export
|
|
100
|
-
|
|
101
|
-
export async function fetchNotifications(): Promise<FetchNotificationsData> {
|
|
107
|
+
export async function notificationNotificationsReadAllPost(): Promise<unknown> {
|
|
102
108
|
const client = ensureReq();
|
|
103
109
|
try {
|
|
104
|
-
const env = await client.
|
|
110
|
+
const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<unknown>>();
|
|
105
111
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
106
112
|
if (typeof env.code === "number" && env.code !== 0)
|
|
107
113
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
108
|
-
const data = env.data as
|
|
109
|
-
if (import.meta.env.DEV) {
|
|
110
|
-
const { schemas } = await import("./api.schemas");
|
|
111
|
-
const r = schemas.fetchNotifications.data.safeParse(data);
|
|
112
|
-
if (!r.success)
|
|
113
|
-
throw new ContractApiError(-1, `[契约违例] fetchNotifications 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
114
|
-
}
|
|
114
|
+
const data = env.data as unknown;
|
|
115
115
|
return data;
|
|
116
116
|
}
|
|
117
117
|
catch (e) {
|
|
@@ -119,10 +119,10 @@ export async function fetchNotifications(): Promise<FetchNotificationsData> {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
export async function
|
|
122
|
+
export async function notificationNotificationsClearPost(): Promise<unknown> {
|
|
123
123
|
const client = ensureReq();
|
|
124
124
|
try {
|
|
125
|
-
const env = await client.post(`notification/notifications/
|
|
125
|
+
const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<unknown>>();
|
|
126
126
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
127
127
|
if (typeof env.code === "number" && env.code !== 0)
|
|
128
128
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
@@ -134,12 +134,12 @@ export async function markAllRead(): Promise<unknown> {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
export type
|
|
137
|
+
export type NotificationPushPostBody = z.input<(typeof schemas)["notificationPushPost"]["body"]>;
|
|
138
138
|
|
|
139
|
-
export async function
|
|
139
|
+
export async function notificationPushPost(body: NotificationPushPostBody): Promise<unknown> {
|
|
140
140
|
const client = ensureReq();
|
|
141
141
|
try {
|
|
142
|
-
const env = await client.post(`notification/
|
|
142
|
+
const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<unknown>>();
|
|
143
143
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
144
144
|
if (typeof env.code === "number" && env.code !== 0)
|
|
145
145
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
@@ -151,12 +151,12 @@ export async function markRead(body: MarkReadBody): Promise<unknown> {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export type
|
|
154
|
+
export type NotificationPushBroadcastPostBody = z.input<(typeof schemas)["notificationPushBroadcastPost"]["body"]>;
|
|
155
155
|
|
|
156
|
-
export async function
|
|
156
|
+
export async function notificationPushBroadcastPost(body: NotificationPushBroadcastPostBody): Promise<unknown> {
|
|
157
157
|
const client = ensureReq();
|
|
158
158
|
try {
|
|
159
|
-
const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<unknown>>();
|
|
159
|
+
const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<unknown>>();
|
|
160
160
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
161
161
|
if (typeof env.code === "number" && env.code !== 0)
|
|
162
162
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
@@ -168,12 +168,12 @@ export async function push(body: PushBody): Promise<unknown> {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
export type
|
|
171
|
+
export type NotificationDelPostBody = z.input<(typeof schemas)["notificationDelPost"]["body"]>;
|
|
172
172
|
|
|
173
|
-
export async function
|
|
173
|
+
export async function notificationDelPost(body: NotificationDelPostBody): Promise<unknown> {
|
|
174
174
|
const client = ensureReq();
|
|
175
175
|
try {
|
|
176
|
-
const env = await client.post(`notification/
|
|
176
|
+
const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<unknown>>();
|
|
177
177
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
178
178
|
if (typeof env.code === "number" && env.code !== 0)
|
|
179
179
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
@@ -2,7 +2,7 @@ import type { NotificationsApiProvider } from "@oj-module/runtime";
|
|
|
2
2
|
|
|
3
3
|
import { defineModule } from "@oj-module/runtime";
|
|
4
4
|
|
|
5
|
-
import { createNotificationClient } from "./
|
|
5
|
+
import { createNotificationClient } from "./__client/api";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* notification 模块(G5 模板形态,对齐 apps/playground-oj/web/src/notification)。
|
|
@@ -21,23 +21,25 @@ export default defineModule({
|
|
|
21
21
|
lifecycle: {
|
|
22
22
|
async onInit(ctx) {
|
|
23
23
|
// 构造即接线:createNotificationClient 登记契约前缀 /notification
|
|
24
|
-
// (唯一真源在
|
|
24
|
+
// (唯一真源在 __client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
|
|
25
25
|
// request(越界/穿越发请求前即拒绝,前缀收敛 D11),返回的 client
|
|
26
26
|
// 方法都自动带 /api/notification/ 前缀。
|
|
27
27
|
const notificationClient = createNotificationClient(ctx);
|
|
28
28
|
const provider: NotificationsApiProvider = {
|
|
29
|
-
|
|
29
|
+
// 端点函数名由 oj operationId 派生(camelCase 子路径 + 方法),
|
|
30
|
+
// 见 __client/api.ts;provider 方法名(fetchNotifications/markRead…)不变。
|
|
31
|
+
fetchNotifications: () => notificationClient.notificationNotificationsGet(),
|
|
30
32
|
// id 为命名空间串("u:<id>" / "b:<id>"),原样透传
|
|
31
|
-
markRead: id => notificationClient.
|
|
32
|
-
markAllRead: () => notificationClient.
|
|
33
|
-
clearAll: () => notificationClient.
|
|
33
|
+
markRead: id => notificationClient.notificationNotificationsReadPost({ id: String(id) }).then(() => {}),
|
|
34
|
+
markAllRead: () => notificationClient.notificationNotificationsReadAllPost().then(() => {}),
|
|
35
|
+
clearAll: () => notificationClient.notificationNotificationsClearPost().then(() => {}),
|
|
34
36
|
push: input => notificationClient
|
|
35
|
-
.
|
|
37
|
+
.notificationPushPost({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
|
|
36
38
|
.then(r => String((r as { id: string }).id)),
|
|
37
39
|
pushBroadcast: input => notificationClient
|
|
38
|
-
.
|
|
40
|
+
.notificationPushBroadcastPost({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
|
|
39
41
|
.then(r => String((r as { id: string }).id)),
|
|
40
|
-
del: (id, userId) => notificationClient.
|
|
42
|
+
del: (id, userId) => notificationClient.notificationDelPost({ id: String(id), userId }).then(() => {}),
|
|
41
43
|
};
|
|
42
44
|
ctx.register.notificationsApi(provider);
|
|
43
45
|
},
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
|
|
3
|
+
import { ContractApiError } from "@oj-module/runtime/contract/errors";
|
|
4
|
+
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
5
|
+
import type { ModuleContext } from "@oj-module/runtime";
|
|
6
|
+
import type { z } from "@oj-module/runtime";
|
|
7
|
+
import type { schemas } from "./api.schemas";
|
|
8
|
+
|
|
9
|
+
/** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
|
|
10
|
+
interface OjEnvelope<T> { code: number, msg?: string, data?: T }
|
|
11
|
+
|
|
12
|
+
let req: ScopedRequestLike | undefined;
|
|
13
|
+
|
|
14
|
+
/** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
|
|
15
|
+
export const API_PREFIX = "/notifications";
|
|
16
|
+
|
|
17
|
+
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
|
+
export const MODULE_PREFIX = "/notifications";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
|
+
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
23
|
+
* 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
|
|
24
|
+
*/
|
|
25
|
+
export function createNotificationsClient(ctx: ModuleContext) {
|
|
26
|
+
// 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
|
|
27
|
+
// ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
|
|
28
|
+
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
|
+
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
|
+
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
|
|
32
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
|
+
}
|
|
34
|
+
ctx.register.apiPrefix(API_PREFIX);
|
|
35
|
+
bindRequest(ctx.utils.request);
|
|
36
|
+
return { notificationsGet };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated 请改用 createNotificationsClient(ctx)——工厂构造即完成
|
|
41
|
+
* 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
|
|
42
|
+
*/
|
|
43
|
+
export function bindRequest(r: ScopedRequestLike): void {
|
|
44
|
+
req = r;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureReq(): ScopedRequestLike {
|
|
48
|
+
if (!req)
|
|
49
|
+
throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createNotificationsClient(ctx)(或 bindRequest(ctx.utils.request))。");
|
|
50
|
+
return req;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
|
|
54
|
+
async function toApiError(e: unknown): Promise<unknown> {
|
|
55
|
+
if (e instanceof ContractApiError)
|
|
56
|
+
return e;
|
|
57
|
+
// ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
|
|
58
|
+
// 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
|
|
59
|
+
const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
|
|
60
|
+
const res = err?.response;
|
|
61
|
+
const d = err?.data;
|
|
62
|
+
if (res instanceof Response && d && typeof d.code === "number")
|
|
63
|
+
return new ContractApiError(d.code, d.msg ?? res.statusText);
|
|
64
|
+
return e;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function notificationsGet(): Promise<unknown> {
|
|
68
|
+
const client = ensureReq();
|
|
69
|
+
try {
|
|
70
|
+
const env = await client.get(`notifications`).json<OjEnvelope<unknown>>();
|
|
71
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
72
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
73
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
74
|
+
const data = env.data as unknown;
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
throw await toApiError(e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
|
|
3
|
+
import { ContractApiError } from "@oj-module/runtime/contract/errors";
|
|
4
|
+
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
5
|
+
import type { ModuleContext } from "@oj-module/runtime";
|
|
6
|
+
import type { z } from "@oj-module/runtime";
|
|
7
|
+
import type { schemas } from "./api.schemas";
|
|
8
|
+
|
|
9
|
+
/** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
|
|
10
|
+
interface OjEnvelope<T> { code: number, msg?: string, data?: T }
|
|
11
|
+
|
|
12
|
+
let req: ScopedRequestLike | undefined;
|
|
13
|
+
|
|
14
|
+
/** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
|
|
15
|
+
export const API_PREFIX = "/personal-center";
|
|
16
|
+
|
|
17
|
+
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
|
+
export const MODULE_PREFIX = "/personal-center";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
|
+
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
23
|
+
* 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
|
|
24
|
+
*/
|
|
25
|
+
export function createPersonalCenterClient(ctx: ModuleContext) {
|
|
26
|
+
// 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
|
|
27
|
+
// ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
|
|
28
|
+
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
|
+
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
|
+
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
|
|
32
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
|
+
}
|
|
34
|
+
ctx.register.apiPrefix(API_PREFIX);
|
|
35
|
+
bindRequest(ctx.utils.request);
|
|
36
|
+
return { personalCenterUploadPost };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated 请改用 createPersonalCenterClient(ctx)——工厂构造即完成
|
|
41
|
+
* 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
|
|
42
|
+
*/
|
|
43
|
+
export function bindRequest(r: ScopedRequestLike): void {
|
|
44
|
+
req = r;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureReq(): ScopedRequestLike {
|
|
48
|
+
if (!req)
|
|
49
|
+
throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createPersonalCenterClient(ctx)(或 bindRequest(ctx.utils.request))。");
|
|
50
|
+
return req;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
|
|
54
|
+
async function toApiError(e: unknown): Promise<unknown> {
|
|
55
|
+
if (e instanceof ContractApiError)
|
|
56
|
+
return e;
|
|
57
|
+
// ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
|
|
58
|
+
// 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
|
|
59
|
+
const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
|
|
60
|
+
const res = err?.response;
|
|
61
|
+
const d = err?.data;
|
|
62
|
+
if (res instanceof Response && d && typeof d.code === "number")
|
|
63
|
+
return new ContractApiError(d.code, d.msg ?? res.statusText);
|
|
64
|
+
return e;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type PersonalCenterUploadPostData = z.infer<(typeof schemas)["personalCenterUploadPost"]["data"]>;
|
|
68
|
+
|
|
69
|
+
export async function personalCenterUploadPost(): Promise<PersonalCenterUploadPostData> {
|
|
70
|
+
const client = ensureReq();
|
|
71
|
+
try {
|
|
72
|
+
const env = await client.post(`personal-center/upload`).json<OjEnvelope<PersonalCenterUploadPostData>>();
|
|
73
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
74
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
75
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
76
|
+
const data = env.data as PersonalCenterUploadPostData;
|
|
77
|
+
if (import.meta.env.DEV) {
|
|
78
|
+
const { schemas } = await import("./api.schemas");
|
|
79
|
+
const r = schemas["personalCenterUploadPost"].data.safeParse(data);
|
|
80
|
+
if (!r.success)
|
|
81
|
+
throw new ContractApiError(-1, `[契约违例] personalCenterUploadPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
82
|
+
}
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
throw await toApiError(e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
|
|
3
|
+
import { ContractApiError } from "@oj-module/runtime/contract/errors";
|
|
4
|
+
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
5
|
+
import type { ModuleContext } from "@oj-module/runtime";
|
|
6
|
+
import type { z } from "@oj-module/runtime";
|
|
7
|
+
import type { schemas } from "./api.schemas";
|
|
8
|
+
|
|
9
|
+
/** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
|
|
10
|
+
interface OjEnvelope<T> { code: number, msg?: string, data?: T }
|
|
11
|
+
|
|
12
|
+
let req: ScopedRequestLike | undefined;
|
|
13
|
+
|
|
14
|
+
/** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
|
|
15
|
+
export const API_PREFIX = "/web";
|
|
16
|
+
|
|
17
|
+
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
|
+
export const MODULE_PREFIX = "/web";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
|
+
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
23
|
+
* 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
|
|
24
|
+
*/
|
|
25
|
+
export function createWebClient(ctx: ModuleContext) {
|
|
26
|
+
// 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
|
|
27
|
+
// ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
|
|
28
|
+
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
|
+
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
|
+
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
|
|
32
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
|
+
}
|
|
34
|
+
ctx.register.apiPrefix(API_PREFIX);
|
|
35
|
+
bindRequest(ctx.utils.request);
|
|
36
|
+
return { webGetAsyncRoutesGet, webHelloGet, webUserInfoGet };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated 请改用 createWebClient(ctx)——工厂构造即完成
|
|
41
|
+
* 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
|
|
42
|
+
*/
|
|
43
|
+
export function bindRequest(r: ScopedRequestLike): void {
|
|
44
|
+
req = r;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureReq(): ScopedRequestLike {
|
|
48
|
+
if (!req)
|
|
49
|
+
throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createWebClient(ctx)(或 bindRequest(ctx.utils.request))。");
|
|
50
|
+
return req;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
|
|
54
|
+
async function toApiError(e: unknown): Promise<unknown> {
|
|
55
|
+
if (e instanceof ContractApiError)
|
|
56
|
+
return e;
|
|
57
|
+
// ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
|
|
58
|
+
// 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
|
|
59
|
+
const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
|
|
60
|
+
const res = err?.response;
|
|
61
|
+
const d = err?.data;
|
|
62
|
+
if (res instanceof Response && d && typeof d.code === "number")
|
|
63
|
+
return new ContractApiError(d.code, d.msg ?? res.statusText);
|
|
64
|
+
return e;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function webGetAsyncRoutesGet(): Promise<unknown> {
|
|
68
|
+
const client = ensureReq();
|
|
69
|
+
try {
|
|
70
|
+
const env = await client.get(`web/get-async-routes`).json<OjEnvelope<unknown>>();
|
|
71
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
72
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
73
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
74
|
+
const data = env.data as unknown;
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
throw await toApiError(e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function webHelloGet(): Promise<unknown> {
|
|
83
|
+
const client = ensureReq();
|
|
84
|
+
try {
|
|
85
|
+
const env = await client.get(`web/hello`).json<OjEnvelope<unknown>>();
|
|
86
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
87
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
88
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
89
|
+
const data = env.data as unknown;
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
throw await toApiError(e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function webUserInfoGet(): Promise<unknown> {
|
|
98
|
+
const client = ensureReq();
|
|
99
|
+
try {
|
|
100
|
+
const env = await client.get(`web/user-info`).json<OjEnvelope<unknown>>();
|
|
101
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
102
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
103
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
104
|
+
const data = env.data as unknown;
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
throw await toApiError(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"react-router": "7.18.2",
|
|
9
9
|
"react-router/dom": "7.18.2",
|
|
10
10
|
"@tanstack/react-query": "5.102.8",
|
|
11
|
-
"@oj-module/runtime": "0.1.
|
|
12
|
-
"@oj-module/runtime/contract": "0.1.
|
|
13
|
-
"@oj-module/runtime/contract/errors": "0.1.
|
|
14
|
-
"@plane/kit": "0.1.
|
|
15
|
-
"@plane/editor": "0.1.
|
|
11
|
+
"@oj-module/runtime": "0.1.20",
|
|
12
|
+
"@oj-module/runtime/contract": "0.1.20",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.1.20",
|
|
14
|
+
"@plane/kit": "0.1.20",
|
|
15
|
+
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|
|
17
17
|
"antd/locale/en_US": "6.6.1",
|
|
18
18
|
"antd/locale/zh_CN": "6.6.1",
|