This is an automated email from the ASF dual-hosted git repository.
guoqqqi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new fa2fd0f60 fix: accept integer reference ids from the Admin API (#3472)
fa2fd0f60 is described below
commit fa2fd0f60f8afffb096476333b9ba63b4c518fa3
Author: Mohammad Izzraff Janius
<[email protected]>
AuthorDate: Tue Sep 1 11:33:40 2026 +0900
fix: accept integer reference ids from the Admin API (#3472)
---
.../regression/form.numeric-reference-ids.spec.ts | 190 +++++++++++++++++++++
src/components/form/ResourceRef.tsx | 5 +-
.../form/ref-id.test.ts} | 40 ++---
.../consumers.ts => components/form/ref-id.ts} | 28 +--
src/routes/protos/detail.$id.tsx | 2 +-
src/types/schema/apisix/common.test.ts | 42 +++++
src/types/schema/apisix/common.ts | 7 +-
src/types/schema/apisix/consumers.ts | 2 +-
src/types/schema/apisix/gateway-contract.test.ts | 54 +++++-
src/types/schema/apisix/routes.ts | 8 +-
src/types/schema/apisix/services.ts | 2 +-
src/types/schema/apisix/stream_routes.ts | 6 +-
src/types/schema/apisix/upstreams.ts | 2 +-
13 files changed, 326 insertions(+), 62 deletions(-)
diff --git a/e2e/tests/regression/form.numeric-reference-ids.spec.ts
b/e2e/tests/regression/form.numeric-reference-ids.spec.ts
new file mode 100644
index 000000000..3804f1095
--- /dev/null
+++ b/e2e/tests/regression/form.numeric-reference-ids.spec.ts
@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { consumersPom } from '@e2e/pom/consumers';
+import { routesPom } from '@e2e/pom/routes';
+import { servicesPom } from '@e2e/pom/services';
+import { streamRoutesPom } from '@e2e/pom/stream_routes';
+import { safeClean } from '@e2e/utils/clean';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiGoto } from '@e2e/utils/ui';
+import { expect, type Page } from '@playwright/test';
+
+import {
+ API_CONSUMER_GROUPS,
+ API_CONSUMERS,
+ API_PLUGIN_CONFIGS,
+ API_ROUTES,
+ API_SERVICES,
+ API_STREAM_ROUTES,
+ API_UPSTREAMS,
+} from '@/config/constant';
+
+const UPSTREAM_ID = 10001;
+const UPSTREAM_NAME = 'numeric-id upstream';
+const PLUGIN_CONFIG_ID = 10002;
+const PLUGIN_CONFIG_NAME = 'numeric-id plugin config';
+const GROUP_ID = 10003;
+const ROUTE_ID = 10004;
+const STREAM_ROUTE_ID = 'numeric-ref-stream-route';
+const SERVICE_ID = 'numeric-ref-service';
+const CONSUMER_NAME = 'numeric_ref_consumer';
+
+const clean = () =>
+ safeClean(
+ () => e2eReq.delete(`${API_ROUTES}/${ROUTE_ID}`),
+ () => e2eReq.delete(`${API_STREAM_ROUTES}/${STREAM_ROUTE_ID}`),
+ () => e2eReq.delete(`${API_SERVICES}/${SERVICE_ID}`),
+ () => e2eReq.delete(`${API_CONSUMERS}/${CONSUMER_NAME}`),
+ () => e2eReq.delete(`${API_UPSTREAMS}/${UPSTREAM_ID}`),
+ () => e2eReq.delete(`${API_PLUGIN_CONFIGS}/${PLUGIN_CONFIG_ID}`),
+ () => e2eReq.delete(`${API_CONSUMER_GROUPS}/${GROUP_ID}`)
+ );
+
+test.describe.configure({ mode: 'serial' });
+
+test.beforeAll(async () => {
+ await clean();
+ await e2eReq.put(`${API_UPSTREAMS}/${UPSTREAM_ID}`, {
+ name: UPSTREAM_NAME,
+ type: 'roundrobin',
+ nodes: { 'numeric.local:80': 1 },
+ });
+ await e2eReq.put(`${API_PLUGIN_CONFIGS}/${PLUGIN_CONFIG_ID}`, {
+ name: PLUGIN_CONFIG_NAME,
+ plugins: {},
+ });
+ await e2eReq.put(`${API_CONSUMER_GROUPS}/${GROUP_ID}`, { plugins: {} });
+ await e2eReq.put(API_ROUTES, {
+ id: ROUTE_ID,
+ name: 'numeric-ref-route',
+ uri: '/numeric-ref',
+ upstream_id: UPSTREAM_ID,
+ plugin_config_id: PLUGIN_CONFIG_ID,
+ });
+ await e2eReq.put(`${API_STREAM_ROUTES}/${STREAM_ROUTE_ID}`, {
+ server_port: 9100,
+ upstream_id: UPSTREAM_ID,
+ });
+ await e2eReq.put(`${API_SERVICES}/${SERVICE_ID}`, {
+ name: SERVICE_ID,
+ upstream_id: UPSTREAM_ID,
+ });
+ await e2eReq.put(API_CONSUMERS, {
+ username: CONSUMER_NAME,
+ group_id: GROUP_ID,
+ });
+});
+
+test.afterAll(clean);
+
+const refInput = (page: Page, name: string) =>
+ page.locator(`input[name="${name}"]`);
+
+const saveUntouched = async (page: Page, resource: string) => {
+ await page.getByRole('button', { name: 'Edit', exact: true }).click();
+ await page.getByRole('button', { name: 'Save', exact: true }).click();
+ await expect(page.getByText(`Edit ${resource} Successfully`)).toBeVisible();
+ await expect(page.getByText('Expected string, received number')).toHaveCount(
+ 0
+ );
+};
+
+test('a route whose id and references are numbers renders, resolves and
saves', async ({
+ page,
+}) => {
+ await uiGoto(page, '/routes/detail/$id', { id: String(ROUTE_ID) });
+ await routesPom.isDetailPage(page);
+
+ await expect(refInput(page, 'upstream_id')).toHaveValue(
+ String(UPSTREAM_ID)
+ );
+ await expect(
+ page.getByRole('link', { name: `View Upstream: ${UPSTREAM_NAME}` })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('link', {
+ name: `View Plugin Config: ${PLUGIN_CONFIG_NAME}`,
+ })
+ ).toBeVisible();
+
+ await saveUntouched(page, 'Route');
+
+ const { data } = await e2eReq.get(`${API_ROUTES}/${ROUTE_ID}`);
+ expect(data.value.id).toBe(String(ROUTE_ID));
+ expect(data.value.upstream_id).toBe(String(UPSTREAM_ID));
+ expect(data.value.plugin_config_id).toBe(String(PLUGIN_CONFIG_ID));
+});
+
+test('a stream route whose upstream_id is a number renders, resolves and
saves', async ({
+ page,
+}) => {
+ await uiGoto(page, '/stream_routes/detail/$id', { id: STREAM_ROUTE_ID });
+ await streamRoutesPom.isDetailPage(page);
+
+ await expect(refInput(page, 'upstream_id')).toHaveValue(
+ String(UPSTREAM_ID)
+ );
+ await expect(
+ page.getByRole('link', { name: `View Upstream: ${UPSTREAM_NAME}` })
+ ).toBeVisible();
+
+ await saveUntouched(page, 'Stream Route');
+
+ const { data } = await e2eReq.get(`${API_STREAM_ROUTES}/${STREAM_ROUTE_ID}`);
+ expect(data.value.upstream_id).toBe(String(UPSTREAM_ID));
+});
+
+test('a service whose upstream_id is a number renders, resolves and saves',
async ({
+ page,
+}) => {
+ await uiGoto(page, '/services/detail/$id', { id: SERVICE_ID });
+ await servicesPom.isDetailPage(page);
+
+ await expect(refInput(page, 'upstream_id')).toHaveValue(
+ String(UPSTREAM_ID)
+ );
+ await expect(
+ page.getByRole('link', { name: `View Upstream: ${UPSTREAM_NAME}` })
+ ).toBeVisible();
+
+ await saveUntouched(page, 'Service');
+
+ const { data } = await e2eReq.get(`${API_SERVICES}/${SERVICE_ID}`);
+ expect(data.value.upstream_id).toBe(String(UPSTREAM_ID));
+});
+
+test('a consumer whose group_id is a number renders, resolves and saves',
async ({
+ page,
+}) => {
+ await uiGoto(page, '/consumers/detail/$username', {
+ username: CONSUMER_NAME,
+ });
+ await consumersPom.isDetailPage(page);
+
+ await expect(refInput(page, 'group_id')).toHaveValue(
+ String(GROUP_ID)
+ );
+ await expect(
+ page.getByRole('link', { name: 'View Consumer Group' })
+ ).toBeVisible();
+
+ await saveUntouched(page, 'Consumer');
+
+ const { data } = await e2eReq.get(`${API_CONSUMERS}/${CONSUMER_NAME}`);
+ expect(data.value.group_id).toBe(String(GROUP_ID));
+});
diff --git a/src/components/form/ResourceRef.tsx
b/src/components/form/ResourceRef.tsx
index f630fc1bd..6ea1b4706 100644
--- a/src/components/form/ResourceRef.tsx
+++ b/src/components/form/ResourceRef.tsx
@@ -29,6 +29,7 @@ import {
getServiceQueryOptions,
getUpstreamQueryOptions,
} from '@/apis/hooks';
+import { toRefId } from '@/components/form/ref-id';
import {
FormItemTextInput,
type FormItemTextInputProps,
@@ -122,10 +123,10 @@ export const FormItemResourceRef = <T extends
FieldValues>(
const value = useWatch({
control: props.control,
name: props.name,
- }) as string | undefined;
+ });
// Without this the field fires one query per keystroke while it is being
// typed into, and all but the last are guaranteed to 404.
- const [id] = useDebouncedValue((value ?? '').trim(), 300);
+ const [id] = useDebouncedValue(toRefId(value), 300);
const { to, getQueryOptions } = REFS[resource];
const options = getQueryOptions(id);
diff --git a/src/types/schema/apisix/consumers.ts
b/src/components/form/ref-id.test.ts
similarity index 55%
copy from src/types/schema/apisix/consumers.ts
copy to src/components/form/ref-id.test.ts
index c6f8e261d..68f6aee26 100644
--- a/src/types/schema/apisix/consumers.ts
+++ b/src/components/form/ref-id.test.ts
@@ -14,28 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { z } from 'zod';
+import { describe, expect, it } from 'vitest';
-import { APISIXCommon } from './common';
-import { APISIXPlugins } from './plugins';
+import { toRefId } from './ref-id';
-const Consumer = z
- .object({
- username: z
- .string()
- .min(1)
- // ref:
https://github.com/apache/apisix/blob/a2482df74d712228a1a6644662d74d2f51a3f5e6/apisix/schema_def.lua#L713
- .regex(/^[a-zA-Z0-9_-]+$/),
- plugins: APISIXPlugins.Plugins.optional(),
- group_id: z.string().optional(),
- })
- .merge(APISIXCommon.Basic.omit({ name: true }))
- .merge(APISIXCommon.Info.omit({ id: true }));
+describe('toRefId', () => {
+ it.each([
+ ['a string, trimmed', ' up-1 ', 'up-1'],
+ ['a number, as its decimal string', 10001, '10001'],
+ ['undefined (field not set)', undefined, ''],
+ ['null', null, ''],
+ ['whitespace only', ' ', ''],
+ ])('%s', (_, input, expected) => {
+ expect(toRefId(input)).toBe(expected);
+ });
-export const APISIXConsumers = {
- Consumer,
- ConsumerPut: Consumer.omit({
- create_time: true,
- update_time: true,
- }),
-};
+ it('resolves nothing for a value that is not an id at all', () => {
+ for (const value of [true, {}, [], () => undefined]) {
+ expect(toRefId(value)).toBe('');
+ }
+ });
+});
diff --git a/src/types/schema/apisix/consumers.ts
b/src/components/form/ref-id.ts
similarity index 55%
copy from src/types/schema/apisix/consumers.ts
copy to src/components/form/ref-id.ts
index c6f8e261d..c21d6f819 100644
--- a/src/types/schema/apisix/consumers.ts
+++ b/src/components/form/ref-id.ts
@@ -14,28 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { z } from 'zod';
-
-import { APISIXCommon } from './common';
-import { APISIXPlugins } from './plugins';
-
-const Consumer = z
- .object({
- username: z
- .string()
- .min(1)
- // ref:
https://github.com/apache/apisix/blob/a2482df74d712228a1a6644662d74d2f51a3f5e6/apisix/schema_def.lua#L713
- .regex(/^[a-zA-Z0-9_-]+$/),
- plugins: APISIXPlugins.Plugins.optional(),
- group_id: z.string().optional(),
- })
- .merge(APISIXCommon.Basic.omit({ name: true }))
- .merge(APISIXCommon.Info.omit({ id: true }));
-
-export const APISIXConsumers = {
- Consumer,
- ConsumerPut: Consumer.omit({
- create_time: true,
- update_time: true,
- }),
+export const toRefId = (value: unknown): string => {
+ if (typeof value === 'number') return String(value);
+ if (typeof value === 'string') return value.trim();
+ return '';
};
diff --git a/src/routes/protos/detail.$id.tsx b/src/routes/protos/detail.$id.tsx
index 0f98d9c08..7f61376bb 100644
--- a/src/routes/protos/detail.$id.tsx
+++ b/src/routes/protos/detail.$id.tsx
@@ -58,7 +58,7 @@ const ProtoDetailForm = ({ id, readOnly, setReadOnly }:
ProtoFormProps) => {
refetch,
} = useSuspenseQuery(getProtoQueryOptions(id));
- const form = useForm<APISIXType['Proto']>({
+ const form = useForm({
resolver: zodResolver(APISIX.Proto),
shouldUnregister: true,
mode: 'all',
diff --git a/src/types/schema/apisix/common.test.ts
b/src/types/schema/apisix/common.test.ts
new file mode 100644
index 000000000..416954eae
--- /dev/null
+++ b/src/types/schema/apisix/common.test.ts
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { describe, expect, it } from 'vitest';
+
+import { APISIXCommon } from './common';
+
+describe('RefId', () => {
+ it.each([
+ ['a string', 'up-1', 'up-1'],
+ ['an empty string (the cleared form field)', '', ''],
+ ['a positive integer, as its decimal string', 10001, '10001'],
+ ])('accepts %s', (_, input, expected) => {
+ expect(APISIXCommon.RefId.parse(input)).toBe(expected);
+ });
+
+ // The gateway's `id_schema` integer branch is `minimum: 1`.
+ it.each([0, -1, 1.5, true, null, {}])('rejects %j', (input) => {
+ expect(APISIXCommon.RefId.safeParse(input).success).toBe(false);
+ });
+});
+
+describe('ID', () => {
+ // `PUT /apisix/admin/routes` with `"id": 10001` in the body (no id in the
+ // URL) stores and returns a numeric primary id.
+ it('accepts the integer primary id and normalizes it to a string', () => {
+ expect(APISIXCommon.ID.parse({ id: 10001 })).toEqual({ id: '10001' });
+ });
+});
diff --git a/src/types/schema/apisix/common.ts
b/src/types/schema/apisix/common.ts
index fd26d7f05..ec94a12de 100644
--- a/src/types/schema/apisix/common.ts
+++ b/src/types/schema/apisix/common.ts
@@ -41,8 +41,12 @@ const Basic = z
})
.partial();
+const RefId = z
+ .union([z.string(), z.number().int().min(1)])
+ .transform((id) => String(id));
+
const ID = z.object({
- id: z.string(),
+ id: RefId,
});
const Timestamp = z.object({
@@ -70,6 +74,7 @@ export const APISIXCommon = {
Labels,
Expr,
ID,
+ RefId,
Timestamp,
Info,
HttpMethod,
diff --git a/src/types/schema/apisix/consumers.ts
b/src/types/schema/apisix/consumers.ts
index c6f8e261d..92bb7bc4e 100644
--- a/src/types/schema/apisix/consumers.ts
+++ b/src/types/schema/apisix/consumers.ts
@@ -27,7 +27,7 @@ const Consumer = z
// ref:
https://github.com/apache/apisix/blob/a2482df74d712228a1a6644662d74d2f51a3f5e6/apisix/schema_def.lua#L713
.regex(/^[a-zA-Z0-9_-]+$/),
plugins: APISIXPlugins.Plugins.optional(),
- group_id: z.string().optional(),
+ group_id: APISIXCommon.RefId.optional(),
})
.merge(APISIXCommon.Basic.omit({ name: true }))
.merge(APISIXCommon.Info.omit({ id: true }));
diff --git a/src/types/schema/apisix/gateway-contract.test.ts
b/src/types/schema/apisix/gateway-contract.test.ts
index 876e67669..075fb18d2 100644
--- a/src/types/schema/apisix/gateway-contract.test.ts
+++ b/src/types/schema/apisix/gateway-contract.test.ts
@@ -18,7 +18,7 @@ import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
-import type { ZodObject, ZodRawShape, ZodTypeAny } from 'zod';
+import { ZodObject, type ZodRawShape, type ZodTypeAny } from 'zod';
import { APISIX } from '.';
@@ -49,8 +49,15 @@ import { APISIX } from '.';
const FIXTURE_DIR = fileURLToPath(new URL('./__fixtures__/gateway/',
import.meta.url));
+type GatewayProperty = {
+ enum?: unknown[];
+ type?: string;
+ anyOf?: { type?: string }[];
+ properties?: Record<string, GatewayProperty>;
+};
+
type GatewaySchema = {
- properties?: Record<string, { enum?: unknown[]; type?: string }>;
+ properties?: Record<string, GatewayProperty>;
required?: string[];
};
@@ -167,3 +174,46 @@ describe('gateway contract: zod is looser-or-equal to the
APISIX schema', () =>
).toEqual({ notCovered: [], enumGaps: [], staleAllow: [] });
});
});
+
+const isIdSchema = (spec: GatewayProperty) =>
+ Array.isArray(spec.anyOf) &&
+ spec.anyOf.some((t) => t.type === 'integer') &&
+ spec.anyOf.some((t) => t.type === 'string');
+
+const collectIdGaps = (
+ properties: Record<string, GatewayProperty>,
+ shape: ZodRawShape,
+ path = ''
+): { rejectsInteger: string[]; keepsInteger: string[] } => {
+ const rejectsInteger: string[] = [];
+ const keepsInteger: string[] = [];
+ for (const [field, spec] of Object.entries(properties)) {
+ if (!(field in shape)) continue;
+ const fieldSchema = shape[field] as ZodTypeAny;
+ const at = path ? `${path}.${field}` : field;
+ if (isIdSchema(spec)) {
+ const parsed = fieldSchema.safeParse(10001);
+ if (!parsed.success) rejectsInteger.push(at);
+ else if (parsed.data !== '10001') keepsInteger.push(at);
+ } else if (spec.properties) {
+ const nested = toObject(fieldSchema);
+ if (!(nested instanceof ZodObject)) continue;
+ const gaps = collectIdGaps(spec.properties, nested.shape, at);
+ rejectsInteger.push(...gaps.rejectsInteger);
+ keepsInteger.push(...gaps.keepsInteger);
+ }
+ }
+ return { rejectsInteger, keepsInteger };
+};
+
+describe('gateway contract: id-typed fields accept the integer form', () => {
+ it.each(Object.keys(SCHEMAS))('%s', (resource) => {
+ const properties = loadFixture(resource).properties ?? {};
+ const shape = toObject(SCHEMAS[resource]).shape;
+
+ expect(
+ collectIdGaps(properties, shape),
+ `${resource}: an id-typed field rejects the integer form the gateway
accepts, or does not normalize it to a string`
+ ).toEqual({ rejectsInteger: [], keepsInteger: [] });
+ });
+});
diff --git a/src/types/schema/apisix/routes.ts
b/src/types/schema/apisix/routes.ts
index f87c9f7e0..0889fac27 100644
--- a/src/types/schema/apisix/routes.ts
+++ b/src/types/schema/apisix/routes.ts
@@ -32,12 +32,12 @@ const Route = z
vars: APISIXCommon.Expr,
filter_func: z.string(),
script: z.string(),
- script_id: z.string(),
+ script_id: APISIXCommon.RefId,
plugins: APISIXPlugins.Plugins,
- plugin_config_id: z.string(),
+ plugin_config_id: APISIXCommon.RefId,
upstream: APISIXUpstreams.Upstream.omit({ id: true }),
- upstream_id: z.string(),
- service_id: z.string(),
+ upstream_id: APISIXCommon.RefId,
+ service_id: APISIXCommon.RefId,
timeout: APISIXUpstreams.UpstreamTimeout.partial(),
enable_websocket: z.boolean(),
priority: z.number().default(0),
diff --git a/src/types/schema/apisix/services.ts
b/src/types/schema/apisix/services.ts
index 28658962b..0a8293f34 100644
--- a/src/types/schema/apisix/services.ts
+++ b/src/types/schema/apisix/services.ts
@@ -24,7 +24,7 @@ const Service = z
.object({
plugins: APISIXPlugins.Plugins.optional(),
upstream: APISIXUpstreams.Upstream.omit({ id: true }).optional(),
- upstream_id: z.string().optional(),
+ upstream_id: APISIXCommon.RefId.optional(),
script: z.string().optional(),
enable_websocket: z.boolean().optional(),
hosts: z.array(z.string()).optional(),
diff --git a/src/types/schema/apisix/stream_routes.ts
b/src/types/schema/apisix/stream_routes.ts
index 576342f7e..db7167bfb 100644
--- a/src/types/schema/apisix/stream_routes.ts
+++ b/src/types/schema/apisix/stream_routes.ts
@@ -27,7 +27,7 @@ const StreamRouteProtocolLoggerItem = z.object({
});
const StreamRouteProtocol = z.object({
name: z.string(),
- superior_id: z.string(),
+ superior_id: APISIXCommon.RefId,
conf: z.object({}).optional(),
logger: z.array(StreamRouteProtocolLoggerItem).optional(),
});
@@ -40,8 +40,8 @@ const StreamRoute = z
sni: z.string().optional(),
plugins: APISIXPlugins.Plugins.optional(),
upstream: APISIXUpstreams.Upstream.omit({ id: true }).optional(),
- upstream_id: z.string().optional(),
- service_id: z.string().optional(),
+ upstream_id: APISIXCommon.RefId.optional(),
+ service_id: APISIXCommon.RefId.optional(),
protocol: StreamRouteProtocol.partial().optional(),
})
.partial()
diff --git a/src/types/schema/apisix/upstreams.ts
b/src/types/schema/apisix/upstreams.ts
index a04839929..e04f8cacd 100644
--- a/src/types/schema/apisix/upstreams.ts
+++ b/src/types/schema/apisix/upstreams.ts
@@ -155,7 +155,7 @@ const UpstreamHealthCheck = z.object({
});
const UpstreamTls = z.object({
- client_cert_id: z.string().optional(),
+ client_cert_id: APISIXCommon.RefId.optional(),
client_cert: z.string().optional(),
client_key: z.string().optional(),
verify: z.boolean().optional(),