Просмотр исходного кода

docs(api): mark collection responses nullable (#6430)

* docs(api): mark collection responses nullable

Describe allLinks and panel log response objects as nullable string arrays so generated clients accept the existing nil-slice wire format. Pin both schemas with buildSpec regression assertions and regenerate the OpenAPI copies.

* docs(api): include nullable Xray log responses

Allow generated response arrays to opt into nullability while retaining their schema references and Go-derived examples. Apply this to Xray logs, whose nil slices already serialize as null, and pin the schema and example through buildSpec.
Gleb Gudkov 3 часов назад
Родитель
Сommit
b8597314f8

+ 9 - 1
docs/public/openapi.json

@@ -4340,7 +4340,13 @@
                     "msg": {
                     "msg": {
                       "type": "string"
                       "type": "string"
                     },
                     },
-                    "obj": {}
+                    "obj": {
+                      "type": "array",
+                      "nullable": true,
+                      "items": {
+                        "type": "string"
+                      }
+                    }
                   }
                   }
                 },
                 },
                 "example": {
                 "example": {
@@ -6399,6 +6405,7 @@
                     },
                     },
                     "obj": {
                     "obj": {
                       "type": "array",
                       "type": "array",
+                      "nullable": true,
                       "items": {
                       "items": {
                         "type": "string"
                         "type": "string"
                       }
                       }
@@ -6480,6 +6487,7 @@
                     },
                     },
                     "obj": {
                     "obj": {
                       "type": "array",
                       "type": "array",
+                      "nullable": true,
                       "items": {
                       "items": {
                         "$ref": "#/components/schemas/LogEntry"
                         "$ref": "#/components/schemas/LogEntry"
                       }
                       }

+ 9 - 1
frontend/public/openapi.json

@@ -4340,7 +4340,13 @@
                     "msg": {
                     "msg": {
                       "type": "string"
                       "type": "string"
                     },
                     },
-                    "obj": {}
+                    "obj": {
+                      "type": "array",
+                      "nullable": true,
+                      "items": {
+                        "type": "string"
+                      }
+                    }
                   }
                   }
                 },
                 },
                 "example": {
                 "example": {
@@ -6399,6 +6405,7 @@
                     },
                     },
                     "obj": {
                     "obj": {
                       "type": "array",
                       "type": "array",
+                      "nullable": true,
                       "items": {
                       "items": {
                         "type": "string"
                         "type": "string"
                       }
                       }
@@ -6480,6 +6487,7 @@
                     },
                     },
                     "obj": {
                     "obj": {
                       "type": "array",
                       "type": "array",
+                      "nullable": true,
                       "items": {
                       "items": {
                         "$ref": "#/components/schemas/LogEntry"
                         "$ref": "#/components/schemas/LogEntry"
                       }
                       }

+ 7 - 1
frontend/scripts/build-openapi.mjs

@@ -245,7 +245,13 @@ function buildOperation(ep, tag) {
       );
       );
     }
     }
     const ref = { $ref: `#/components/schemas/${ep.responseSchema}` };
     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) {
     if (successExample === undefined) {
       successExample = { success: true, obj: ep.responseSchemaArray ? [obj] : obj };
       successExample = { success: true, obj: ep.responseSchemaArray ? [obj] : obj };
     }
     }

+ 4 - 1
frontend/src/pages/api-docs/endpoints.ts

@@ -46,6 +46,7 @@ export interface Endpoint {
   bodyRequiredOneOf?: string[];
   bodyRequiredOneOf?: string[];
   responseSchema?: string;
   responseSchema?: string;
   responseSchemaArray?: boolean;
   responseSchemaArray?: boolean;
+  responseSchemaArrayNullable?: boolean;
   responseObjectSchema?: Record<string, unknown>;
   responseObjectSchema?: Record<string, unknown>;
   responses?: Record<string, Record<string, unknown>>;
   responses?: Record<string, Record<string, unknown>>;
   security?: readonly Record<string, readonly string[]>[];
   security?: readonly Record<string, readonly string[]>[];
@@ -265,6 +266,7 @@ export const sections: readonly Section[] = [
       {
       {
         method: 'GET',
         method: 'GET',
         path: '/panel/api/inbounds/allLinks',
         path: '/panel/api/inbounds/allLinks',
+        responseObjectSchema: { type: 'array', nullable: true, items: { type: 'string' } },
         summary:
         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.',
           '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:
         response:
@@ -709,7 +711,7 @@ export const sections: readonly Section[] = [
           },
           },
         ],
         ],
         body: 'level=info&syslog=false',
         body: 'level=info&syslog=false',
-        responseObjectSchema: { type: 'array', items: { type: 'string' } },
+        responseObjectSchema: { type: 'array', nullable: true, items: { type: 'string' } },
         response:
         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}',
           '{\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',
         body: 'filter=error&showDirect=false&showBlocked=true&showProxy=true',
         responseSchema: 'LogEntry',
         responseSchema: 'LogEntry',
         responseSchemaArray: true,
         responseSchemaArray: true,
+        responseSchemaArrayNullable: true,
       },
       },
       {
       {
         method: 'POST',
         method: 'POST',

+ 17 - 1
frontend/src/test/openapi-runtime-contracts.test.ts

@@ -1,6 +1,7 @@
 import { describe, expect, it } from 'vitest';
 import { describe, expect, it } from 'vitest';
 
 
 import { buildSpec } from '../../scripts/build-openapi.mjs';
 import { buildSpec } from '../../scripts/build-openapi.mjs';
+import { EXAMPLES } from '../generated/examples';
 
 
 interface OpenApiSchema {
 interface OpenApiSchema {
   $ref?: string;
   $ref?: string;
@@ -31,7 +32,7 @@ interface OpenApiOperation {
   responses: Record<
   responses: Record<
     string,
     string,
     {
     {
-      content?: Record<string, { schema: OpenApiSchema }>;
+      content?: Record<string, { schema: OpenApiSchema; example?: unknown }>;
     }
     }
   >;
   >;
   security?: Record<string, never[]>[];
   security?: Record<string, never[]>[];
@@ -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', () => {
   it('uses the runtime REST response schemas', () => {
     expect(responseObjectSchema('/panel/api/server/logs/{count}', 'post')).toEqual({
     expect(responseObjectSchema('/panel/api/server/logs/{count}', 'post')).toEqual({
       type: 'array',
       type: 'array',
+      nullable: true,
       items: { type: 'string' },
       items: { type: 'string' },
     });
     });
     expect(responseObjectSchema('/panel/api/server/xraylogs/{count}', 'post')).toEqual({
     expect(responseObjectSchema('/panel/api/server/xraylogs/{count}', 'post')).toEqual({
       type: 'array',
       type: 'array',
+      nullable: true,
       items: { $ref: '#/components/schemas/LogEntry' },
       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({
     expect(responseObjectSchema('/panel/api/server/getNewUUID')).toEqual({
       $ref: '#/components/schemas/NewUUIDResponse',
       $ref: '#/components/schemas/NewUUIDResponse',
     });
     });