chore: scaffold monorepo and migrate uhd_web design system (W0-A)
- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
commit
b22c440139
138 files changed
+14455
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** 本地实体 ID 一律 UUID */
|
||||
export const Id = z.uuid();
|
||||
export type Id = z.infer<typeof Id>;
|
||||
|
||||
/** ISO 8601 时间串 */
|
||||
export const Timestamp = z.iso.datetime({ offset: true });
|
||||
export type Timestamp = z.infer<typeof Timestamp>;
|
||||
|
||||
/** 游标分页请求:cursor 为不透明字符串,由服务端编码 */
|
||||
export const CursorQuery = z.object({
|
||||
cursor: z.string().min(1).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||
});
|
||||
export type CursorQuery = z.infer<typeof CursorQuery>;
|
||||
|
||||
/** 游标分页结果:nextCursor 为 null 表示已到末尾 */
|
||||
export const cursorPage = <T extends z.ZodType>(item: T) =>
|
||||
z.object({
|
||||
items: z.array(item),
|
||||
nextCursor: z.string().nullable(),
|
||||
});
|
||||
export type CursorPage<T> = { items: T[]; nextCursor: string | null };
|
||||
|
||||
/** 错误码表:新增错误码只在这里加 */
|
||||
export const ApiErrorCode = z.enum([
|
||||
"bad_request",
|
||||
"validation_failed",
|
||||
"unauthorized",
|
||||
"forbidden",
|
||||
"not_found",
|
||||
"conflict",
|
||||
"precondition_failed",
|
||||
"rate_limited",
|
||||
"automation_disabled",
|
||||
"upstream_unavailable",
|
||||
"internal",
|
||||
]);
|
||||
export type ApiErrorCode = z.infer<typeof ApiErrorCode>;
|
||||
|
||||
/** 错误码对应的 HTTP 状态 */
|
||||
export const API_ERROR_STATUS = {
|
||||
bad_request: 400,
|
||||
validation_failed: 422,
|
||||
unauthorized: 401,
|
||||
forbidden: 403,
|
||||
not_found: 404,
|
||||
conflict: 409,
|
||||
precondition_failed: 412,
|
||||
rate_limited: 429,
|
||||
automation_disabled: 423,
|
||||
upstream_unavailable: 502,
|
||||
internal: 500,
|
||||
} as const satisfies Record<ApiErrorCode, number>;
|
||||
|
||||
export const ApiError = z.object({
|
||||
code: ApiErrorCode,
|
||||
message: z.string(),
|
||||
});
|
||||
export type ApiError = z.infer<typeof ApiError>;
|
||||
|
||||
/** 统一响应信封 */
|
||||
export const apiResponse = <T extends z.ZodType>(data: T) =>
|
||||
z.object({
|
||||
ok: z.boolean(),
|
||||
data: data.optional(),
|
||||
error: ApiError.optional(),
|
||||
});
|
||||
export type ApiResponse<T> =
|
||||
| { ok: true; data: T; error?: undefined }
|
||||
| { ok: false; data?: undefined; error: ApiError };
|
||||
|
||||
/** 没有 data 的失败响应 schema(OpenAPI 错误分支用) */
|
||||
export const ApiErrorResponse = z.object({
|
||||
ok: z.literal(false),
|
||||
error: ApiError,
|
||||
});
|
||||
|
||||
/** API Key 权限范围 */
|
||||
export const ApiScope = z.enum(["read", "organize.read", "organize.execute", "admin"]);
|
||||
export type ApiScope = z.infer<typeof ApiScope>;
|
||||
|
||||
/** 健康检查 */
|
||||
export const HealthStatus = z.object({
|
||||
status: z.enum(["ok", "degraded"]),
|
||||
version: z.string(),
|
||||
role: z.enum(["api", "worker", "all"]),
|
||||
uptimeSec: z.number(),
|
||||
db: z.enum(["ok", "down", "unknown"]),
|
||||
time: Timestamp,
|
||||
});
|
||||
export type HealthStatus = z.infer<typeof HealthStatus>;
|
||||
Reference in new issue
Block a user