diff --git a/docs/public/openapi.json b/docs/public/openapi.json index 5dadf43fd..c15e60add 100644 --- a/docs/public/openapi.json +++ b/docs/public/openapi.json @@ -4340,7 +4340,13 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + } } }, "example": { @@ -6399,6 +6405,7 @@ }, "obj": { "type": "array", + "nullable": true, "items": { "type": "string" } @@ -6480,6 +6487,7 @@ }, "obj": { "type": "array", + "nullable": true, "items": { "$ref": "#/components/schemas/LogEntry" } diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 5dadf43fd..c15e60add 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -4340,7 +4340,13 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + } } }, "example": { @@ -6399,6 +6405,7 @@ }, "obj": { "type": "array", + "nullable": true, "items": { "type": "string" } @@ -6480,6 +6487,7 @@ }, "obj": { "type": "array", + "nullable": true, "items": { "$ref": "#/components/schemas/LogEntry" } diff --git a/frontend/scripts/build-openapi.mjs b/frontend/scripts/build-openapi.mjs index fbeabadfd..09651d150 100644 --- a/frontend/scripts/build-openapi.mjs +++ b/frontend/scripts/build-openapi.mjs @@ -245,7 +245,13 @@ function buildOperation(ep, tag) { ); } const ref = { $ref: `#/components/schemas/${ep.responseSchema}` }; - objSchema = ep.responseSchemaArray ? { type: 'array', items: ref } : ref; + objSchema = ep.responseSchemaArray + ? { + type: 'array', + ...(ep.responseSchemaArrayNullable ? { nullable: true } : {}), + items: ref, + } + : ref; if (successExample === undefined) { successExample = { success: true, obj: ep.responseSchemaArray ? [obj] : obj }; } diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index 9962610f5..9f631ed1a 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -46,6 +46,7 @@ export interface Endpoint { bodyRequiredOneOf?: string[]; responseSchema?: string; responseSchemaArray?: boolean; + responseSchemaArrayNullable?: boolean; responseObjectSchema?: Record; responses?: Record>; security?: readonly Record[]; @@ -265,6 +266,7 @@ export const sections: readonly Section[] = [ { method: 'GET', path: '/panel/api/inbounds/allLinks', + responseObjectSchema: { type: 'array', nullable: true, items: { type: 'string' } }, summary: 'Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, mtproto) across all inbounds and all of their clients. Links are rendered through the subscription engine, so the configured remark template (name-only display part) is applied per client — the same output the client info/QR pages use. Protocols without a URL form (socks, http, mixed, wireguard, dokodemo, tunnel) contribute nothing. Used by the panel’s "Export all inbound links" action.', response: @@ -709,7 +711,7 @@ export const sections: readonly Section[] = [ }, ], body: 'level=info&syslog=false', - responseObjectSchema: { type: 'array', items: { type: 'string' } }, + responseObjectSchema: { type: 'array', nullable: true, items: { type: 'string' } }, response: '{\n "success": true,\n "obj": [\n "2025/01/01 12:00:00 [INFO] Server started",\n "2025/01/01 12:00:01 [INFO] Xray is running"\n ]\n}', }, @@ -751,6 +753,7 @@ export const sections: readonly Section[] = [ body: 'filter=error&showDirect=false&showBlocked=true&showProxy=true', responseSchema: 'LogEntry', responseSchemaArray: true, + responseSchemaArrayNullable: true, }, { method: 'POST', diff --git a/frontend/src/test/openapi-runtime-contracts.test.ts b/frontend/src/test/openapi-runtime-contracts.test.ts index ecfa3cd2b..155f88000 100644 --- a/frontend/src/test/openapi-runtime-contracts.test.ts +++ b/frontend/src/test/openapi-runtime-contracts.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { buildSpec } from '../../scripts/build-openapi.mjs'; +import { EXAMPLES } from '../generated/examples'; interface OpenApiSchema { $ref?: string; @@ -31,7 +32,7 @@ interface OpenApiOperation { responses: Record< string, { - content?: Record; + content?: Record; } >; security?: Record[]; @@ -127,15 +128,30 @@ describe('generated OpenAPI runtime contracts', () => { }); }); + it('documents all inbound links as a nullable string array', () => { + expect(responseObjectSchema('/panel/api/inbounds/allLinks')).toEqual({ + type: 'array', + nullable: true, + items: { type: 'string' }, + }); + }); + it('uses the runtime REST response schemas', () => { expect(responseObjectSchema('/panel/api/server/logs/{count}', 'post')).toEqual({ type: 'array', + nullable: true, items: { type: 'string' }, }); expect(responseObjectSchema('/panel/api/server/xraylogs/{count}', 'post')).toEqual({ type: 'array', + nullable: true, items: { $ref: '#/components/schemas/LogEntry' }, }); + expect( + operation('/panel/api/server/xraylogs/{count}', 'post').responses['200'].content?.[ + 'application/json' + ].example, + ).toEqual({ success: true, obj: [EXAMPLES.LogEntry] }); expect(responseObjectSchema('/panel/api/server/getNewUUID')).toEqual({ $ref: '#/components/schemas/NewUUIDResponse', });