feat(contracts): openapi base, error codes, type generation
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
// 占位冒烟测试:仅验证 jest + ts-jest 工具链可用(Task 6 落地真正的类型测试)。
|
||||
import { ErrorCodes, type ErrorCode } from '../errors.js';
|
||||
|
||||
describe('ErrorCodes', () => {
|
||||
it('每个 key 映射到同名的稳定 code 字符串', () => {
|
||||
for (const [key, code] of Object.entries(ErrorCodes)) {
|
||||
expect(key).toBe(code);
|
||||
}
|
||||
});
|
||||
|
||||
it('ErrorCode 联合类型可收窄具体 code', () => {
|
||||
const code: ErrorCode = ErrorCodes.ALBUM_NOT_FOUND;
|
||||
expect(code).toBe('ALBUM_NOT_FOUND');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// packages/contracts/src/errors.ts
|
||||
export const ErrorCodes = {
|
||||
AUTH_INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
|
||||
AUTH_TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
|
||||
AUTH_FORBIDDEN: 'AUTH_FORBIDDEN',
|
||||
ALBUM_NOT_FOUND: 'ALBUM_NOT_FOUND',
|
||||
ALBUM_NOT_PUBLIC: 'ALBUM_NOT_PUBLIC',
|
||||
ALBUM_UPLOAD_DISABLED: 'ALBUM_UPLOAD_DISABLED',
|
||||
MEMBER_CONFLICT: 'MEMBER_CONFLICT',
|
||||
UPLOAD_SESSION_CONFLICT: 'UPLOAD_SESSION_CONFLICT',
|
||||
UPLOAD_VERIFY_FAILED: 'UPLOAD_VERIFY_FAILED',
|
||||
PHOTO_NOT_FOUND: 'PHOTO_NOT_FOUND',
|
||||
PHOTO_NOT_PUBLIC: 'PHOTO_NOT_PUBLIC',
|
||||
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||
} as const;
|
||||
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* This file was auto-generated by openapi-typescript.
|
||||
* Do not make direct changes to the file.
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/auth/password": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* 密码登录
|
||||
* @description 校验账号密码,签发会话令牌。
|
||||
*/
|
||||
post: operations["loginWithPassword"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/auth/refresh": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* 会话滑动续期
|
||||
* @description 携带有效 Bearer 令牌滑动续期,返回新的过期时间。
|
||||
*/
|
||||
post: operations["refreshSession"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/auth/logout": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* 退出登录
|
||||
* @description 使当前 Bearer 令牌失效。
|
||||
*/
|
||||
post: operations["logout"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
Error: {
|
||||
/**
|
||||
* @description 稳定错误码,与 src/errors.ts 的 ErrorCodes 一一对应。
|
||||
* @enum {string}
|
||||
*/
|
||||
readonly code: "AUTH_INVALID_CREDENTIALS" | "AUTH_TOKEN_EXPIRED" | "AUTH_FORBIDDEN" | "ALBUM_NOT_FOUND" | "ALBUM_NOT_PUBLIC" | "ALBUM_UPLOAD_DISABLED" | "MEMBER_CONFLICT" | "UPLOAD_SESSION_CONFLICT" | "UPLOAD_VERIFY_FAILED" | "PHOTO_NOT_FOUND" | "PHOTO_NOT_PUBLIC" | "VALIDATION_FAILED" | "INTERNAL_ERROR";
|
||||
/** @description 服务器生成的可读消息(诊断用,不作为客户端展示文案依据)。 */
|
||||
readonly message: string;
|
||||
/** @description 请求追踪 ID。 */
|
||||
readonly requestId: string;
|
||||
};
|
||||
/** @description 游标分页元信息;各列表响应以 allOf 内嵌本结构(条目 schema 在业务端点中定义)。 */
|
||||
CursorPageMeta: {
|
||||
/** @description 下一页游标;null 表示没有更多数据。 */
|
||||
readonly nextCursor: string | null;
|
||||
/** @description 是否还有下一页。 */
|
||||
readonly hasMore: boolean;
|
||||
};
|
||||
LoginRequest: {
|
||||
/** @description 账号(用户名或邮箱)。 */
|
||||
account: string;
|
||||
/** @description 密码。 */
|
||||
password: string;
|
||||
};
|
||||
LoginResponse: {
|
||||
/** @description 会话令牌(Bearer)。 */
|
||||
readonly sessionToken: string;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description 会话过期时间。
|
||||
*/
|
||||
readonly expiresAt: string;
|
||||
};
|
||||
RefreshResponse: {
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description 续期后的过期时间。
|
||||
*/
|
||||
readonly expiresAt: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description 错误响应(稳定 code;客户端按 code 映射文案,message 仅供诊断)。 */
|
||||
ErrorResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Error"];
|
||||
};
|
||||
};
|
||||
};
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
loginWithPassword: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["LoginRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description 登录成功,返回会话令牌与过期时间。 */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["LoginResponse"];
|
||||
};
|
||||
};
|
||||
401: components["responses"]["ErrorResponse"];
|
||||
500: components["responses"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
refreshSession: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description 续期成功。 */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RefreshResponse"];
|
||||
};
|
||||
};
|
||||
401: components["responses"]["ErrorResponse"];
|
||||
500: components["responses"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
logout: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description 已退出,无响应体。 */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
401: components["responses"]["ErrorResponse"];
|
||||
500: components["responses"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// packages/contracts/src/index.ts
|
||||
// 契约统一出口:错误码 + OpenAPI 生成类型(src/generated/openapi.d.ts 提交入库,
|
||||
// 消费方不跑 gen;本包改动后执行 pnpm gen 并提交生成文件 diff)。
|
||||
// NodeNext + ESM:相对导入必须带 .js 扩展名;生成文件仅含类型,用 export type * 在运行时擦除。
|
||||
export * from './errors.js';
|
||||
export type * from './generated/openapi.js';
|
||||
Reference in New Issue
Block a user