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(),

Reply via email to