From ce8393e0c308e5ef3969e7af01d0bbd010026d93 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 7 Aug 2026 13:18:47 +0700 Subject: [PATCH] refactor(services): update service creation logic and enhance service group schema - Changed service creation to default `enabled` to true in the database schema. - Updated service group schema to set default values for `icon`, `domain`, and health check parameters. - Refactored service routes to use `await` for fetching service views, ensuring proper asynchronous handling. - Improved error handling in service groups query to provide clearer feedback on response validation. This commit enhances the overall service management experience by ensuring services are enabled by default and improving the robustness of the service group schema. --- apps/api/src/routes/services.ts | 5 +- .../src/services/service-config-service.ts | 65 ++----- apps/api/test/services-create-list.test.ts | 168 ++++++++++++++++++ apps/web/src/lib/schemas.ts | 42 ++--- apps/web/src/queries/services.ts | 14 +- apps/web/src/routes/_auth/services.tsx | 60 ++++--- packages/db/dist/index.js | 4 +- packages/db/src/repos.ts | 2 +- packages/db/src/schema.ts | 2 +- packages/shared/dist/index.d.ts | 102 +++++------ packages/shared/dist/index.js | 34 ++-- packages/shared/src/schemas.ts | 34 ++-- 12 files changed, 346 insertions(+), 186 deletions(-) create mode 100644 apps/api/test/services-create-list.test.ts diff --git a/apps/api/src/routes/services.ts b/apps/api/src/routes/services.ts index e8e5698..2f0bda7 100644 --- a/apps/api/src/routes/services.ts +++ b/apps/api/src/routes/services.ts @@ -40,7 +40,7 @@ export async function serviceRoutes(app: FastifyInstance) { body.service_group_id, ); } - const view = serviceConfig.getView(request.server.db, service.id); + const view = await serviceConfig.getView(request.server.db, service.id); recordAudit(request.server, request, { action: "service.create", targetType: "app_resource", @@ -56,6 +56,7 @@ export async function serviceRoutes(app: FastifyInstance) { return serviceConfig.getView(request.server.db, Number(id)); }); + app.patch("/services/:id", async (request) => { const { id } = request.params as { id: string }; const body = updateServiceConfigSchema.parse(request.body); @@ -77,7 +78,7 @@ export async function serviceRoutes(app: FastifyInstance) { app.delete("/services/:id", async (request) => { const { id } = request.params as { id: string }; - const view = serviceConfig.getView(request.server.db, Number(id)); + const view = await serviceConfig.getView(request.server.db, Number(id)); repos.deleteService(request.server.db, Number(id)); recordAudit(request.server, request, { action: "service.delete", diff --git a/apps/api/src/services/service-config-service.ts b/apps/api/src/services/service-config-service.ts index bb65f1e..1522428 100644 --- a/apps/api/src/services/service-config-service.ts +++ b/apps/api/src/services/service-config-service.ts @@ -823,49 +823,6 @@ async function findOrImportDnsARecord( ); } -async function serviceBindingsExistInDns( - db: Db, - cf: CloudflareClient, - serviceId: number, -): Promise { - const bindings = repos.listBindingsByService(db, serviceId); - if (bindings.length === 0) return false; - - for (const binding of bindings) { - const cnameTarget = binding.cname_target?.trim() || null; - if (cnameTarget) { - const record = await findOrImportDnsRecord( - db, - cf, - binding.domain_id, - binding.zone_name, - binding.hostname, - "CNAME", - cnameTarget, - ); - if (!record) return false; - continue; - } - - const targetIps = repos.listBindingIps(db, binding.id); - if (targetIps.length === 0) return false; - - for (const ip of targetIps) { - const record = await findOrImportDnsARecord( - db, - cf, - binding.domain_id, - binding.zone_name, - binding.hostname, - ip, - ); - if (!record) return false; - } - } - - return true; -} - async function syncServiceBindingsToDns( db: Db, cf: CloudflareClient, @@ -1281,20 +1238,24 @@ export async function updateConfig( service = repos.getService(db, id); const remainingBindings = repos.listBindingsByService(db, id); + + // Creating/editing with FQDN bindings should publish DNS. Previously a + // disabled service stayed off unless records already existed in Cloudflare + // (chicken-and-egg for brand-new services). + if ( + req.domains && + req.domains.length > 0 && + !service.enabled + ) { + repos.setServiceEnabled(db, id, true); + service = repos.getService(db, id); + } + if (shouldPushDns(db, service)) { if (remainingBindings.length > 0) { await syncServiceBindingsToDns(db, cf, id); } await syncGroupDomainForService(db, cf, id); - } else if ( - req.domains && - req.domains.length > 0 && - !service.enabled && - (await serviceBindingsExistInDns(db, cf, id)) - ) { - repos.setServiceEnabled(db, id, true); - await syncServiceBindingsToDns(db, cf, id); - await syncGroupDomainForService(db, cf, id); } void syncServiceToVpsTracker(db, id, removedBindingIds); diff --git a/apps/api/test/services-create-list.test.ts b/apps/api/test/services-create-list.test.ts new file mode 100644 index 0000000..b94eec7 --- /dev/null +++ b/apps/api/test/services-create-list.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it } from "vitest"; +import { serviceGroupsResponseSchema } from "@cfdm/shared"; +import { repos } from "@cfdm/db"; +import type { CloudflareClient } from "../src/lib/cf-client.js"; +import { buildApp } from "../src/app.js"; +import { loadConfig } from "../src/config.js"; +import { + listGroupViews, + updateConfig, +} from "../src/services/service-config-service.js"; + +function mockCf(): CloudflareClient { + return { + listDnsRecords: async () => [], + createDnsRecord: async (_zoneId: string, payload: { type: string; name: string; content: string }) => ({ + id: `cf-${payload.name}`, + type: payload.type, + name: payload.name, + content: payload.content, + ttl: 1, + proxied: false, + }), + updateDnsRecord: async ( + _zoneId: string, + id: string, + payload: { type: string; name: string; content: string }, + ) => ({ + id, + type: payload.type, + name: payload.name, + content: payload.content, + ttl: 1, + proxied: false, + }), + deleteDnsRecord: async () => undefined, + verifyToken: async () => true, + listZones: async () => [{ id: "zone-1", name: "example.com", status: "active" }], + } as unknown as CloudflareClient; +} + +async function authHeaders(app: Awaited>) { + const config = loadConfig(); + const res = await app.inject({ + method: "POST", + url: "/api/v1/auth/login", + payload: { username: config.adminUsername, password: "admin" }, + }); + expect(res.statusCode).toBe(200); + const { token } = res.json() as { token: string }; + return { authorization: `Bearer ${token}` }; +} + +describe("create service then list groups", () => { + it("create + updateConfig then listGroupViews parses with shared Zod schema", async () => { + const app = await buildApp({ + config: { ...loadConfig(), staticDir: null }, + memory: true, + }); + const headers = await authHeaders(app); + const cf = mockCf(); + + repos.createDomain(app.db, null, "example.com", "zone-1"); + const group = repos.createServiceGroup( + app.db, + "VPN", + "vpn", + null, + "vpn.example.com", + ); + + const createRes = await app.inject({ + method: "POST", + url: "/api/v1/services", + headers, + payload: { + name: "Panel", + slug: "panel", + service_group_id: group.id, + }, + }); + expect(createRes.statusCode).toBe(200); + const created = createRes.json() as { + id: number; + name: string; + enabled: boolean; + }; + expect(created.id).toBeTypeOf("number"); + expect(created.enabled).toBe(true); + + await updateConfig(app.db, cf, created.id, { + ips: ["1.2.3.4"], + service_group_id: group.id, + domains: [ + { + fqdn: "panel.example.com", + target_ips: ["1.2.3.4"], + target_ip_weights: { "1.2.3.4": 1 }, + target_ip_priorities: { "1.2.3.4": 1 }, + lb_mode: "round_robin", + health_check_enabled: true, + health_check_type: "tcp", + health_check_port: 443, + health_check_path: null, + health_check_expected_status: null, + health_check_interval_sec: 30, + health_check_timeout_ms: 3000, + health_check_verify_tls: false, + }, + ], + }); + + const body = await listGroupViews(app.db); + const parsed = serviceGroupsResponseSchema.safeParse(body); + expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); + + const listRes = await app.inject({ + method: "GET", + url: "/api/v1/service-groups", + headers, + }); + expect(listRes.statusCode).toBe(200); + const httpParsed = serviceGroupsResponseSchema.safeParse(listRes.json()); + expect(httpParsed.success, JSON.stringify(httpParsed.error?.issues)).toBe( + true, + ); + expect( + httpParsed.data!.groups + .find((g) => g.id === group.id) + ?.services.some((s) => s.id === created.id), + ).toBe(true); + + await app.close(); + }); + + it("POST /services returns resolved ServiceView with numeric id", async () => { + const app = await buildApp({ + config: { ...loadConfig(), staticDir: null }, + memory: true, + }); + const headers = await authHeaders(app); + + const createRes = await app.inject({ + method: "POST", + url: "/api/v1/services", + headers, + payload: { name: "Bare", slug: "bare" }, + }); + expect(createRes.statusCode).toBe(200); + const created = createRes.json() as Record; + expect(typeof created.id).toBe("number"); + expect(created.name).toBe("Bare"); + expect(created.enabled).toBe(true); + expect(Array.isArray(created.ips)).toBe(true); + expect(Array.isArray(created.domains)).toBe(true); + + const listRes = await app.inject({ + method: "GET", + url: "/api/v1/service-groups", + headers, + }); + expect(listRes.statusCode).toBe(200); + const parsed = serviceGroupsResponseSchema.safeParse(listRes.json()); + expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); + expect(parsed.data!.ungrouped.some((s) => s.slug === "bare")).toBe(true); + + await app.close(); + }); +}); diff --git a/apps/web/src/lib/schemas.ts b/apps/web/src/lib/schemas.ts index 130662d..53a7150 100644 --- a/apps/web/src/lib/schemas.ts +++ b/apps/web/src/lib/schemas.ts @@ -24,15 +24,15 @@ export const serviceGroupSchema = z.object({ id: z.number(), name: z.string(), type: serviceGroupTypeSchema.catch('custom'), - icon: z.string().nullable(), - domain: z.string().nullable(), + icon: z.string().nullable().default(null), + domain: z.string().nullable().default(null), enabled: z.coerce.boolean(), lb_mode: z.enum(['round_robin', 'failover', 'weighted']).catch('round_robin'), health_check_enabled: z.coerce.boolean().default(false), - health_check_type: z.enum(['tcp', 'http']).catch('tcp'), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_type: z.enum(['tcp', 'http', 'ping', 'dns']).catch('tcp'), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3000), health_check_verify_tls: z.coerce.boolean().default(false), @@ -64,19 +64,19 @@ export const serviceDomainBindingSchema = z record_type: z.enum(['A', 'CNAME']).default('A'), target_ips: z.array(z.string()).optional(), target_ip: z.string().nullable().optional(), - target_ip_weights: z.record(z.string(), z.number()).optional(), - target_ip_priorities: z.record(z.string(), z.number()).optional(), + target_ip_weights: z.record(z.string(), z.coerce.number()).optional(), + target_ip_priorities: z.record(z.string(), z.coerce.number()).optional(), target_cname: z.string().nullable().optional(), lb_mode: z.enum(['round_robin', 'failover', 'weighted']).catch('round_robin'), health_check_enabled: z.coerce.boolean().default(false), - health_check_type: z.enum(['tcp', 'http']).catch('tcp'), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_type: z.enum(['tcp', 'http', 'ping', 'dns']).catch('tcp'), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3000), health_check_verify_tls: z.coerce.boolean().default(false), - sync_status: z.string().nullable(), + sync_status: z.string().nullable().default(null), }) .transform((binding) => ({ ...binding, @@ -149,18 +149,18 @@ export const serviceBindingSchema = z service_slug: z.string(), target_ip: z.string().nullable(), target_ips: z.array(z.string()).optional(), - target_ip_weights: z.record(z.string(), z.number()).optional(), - target_ip_priorities: z.record(z.string(), z.number()).optional(), + target_ip_weights: z.record(z.string(), z.coerce.number()).optional(), + target_ip_priorities: z.record(z.string(), z.coerce.number()).optional(), lb_mode: z.enum(['round_robin', 'failover', 'weighted']).catch('round_robin'), health_check_enabled: z.coerce.boolean().default(false), - health_check_type: z.enum(['tcp', 'http']).catch('tcp'), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_type: z.enum(['tcp', 'http', 'ping', 'dns']).catch('tcp'), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3000), health_check_verify_tls: z.coerce.boolean().default(false), - sync_status: z.string().nullable(), + sync_status: z.string().nullable().default(null), created_at: z.string(), updated_at: z.string(), }) @@ -233,7 +233,7 @@ const ipv4Schema = z ) const lbModeSchema = z.enum(['round_robin', 'failover', 'weighted']) -const healthCheckTypeSchema = z.enum(['tcp', 'http']) +const healthCheckTypeSchema = z.enum(['tcp', 'http', 'ping', 'dns']) const healthCheckConfigFields = { health_check_enabled: z.boolean().optional(), diff --git a/apps/web/src/queries/services.ts b/apps/web/src/queries/services.ts index 953b3be..26f42f6 100644 --- a/apps/web/src/queries/services.ts +++ b/apps/web/src/queries/services.ts @@ -20,7 +20,19 @@ export const serviceGroupsQueryOptions = () => queryKey: serviceGroupKeys.all, queryFn: async () => { const data = await api.get('/api/v1/service-groups') - return serviceGroupsResponseSchema.parse(data) + const parsed = serviceGroupsResponseSchema.safeParse(data) + if (!parsed.success) { + const detail = parsed.error.issues + .slice(0, 3) + .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`) + .join('; ') + throw new Error( + detail + ? `Некорректный ответ /service-groups: ${detail}` + : 'Некорректный ответ /service-groups', + ) + } + return parsed.data }, }) diff --git a/apps/web/src/routes/_auth/services.tsx b/apps/web/src/routes/_auth/services.tsx index 66fb0f3..3cbb4fc 100644 --- a/apps/web/src/routes/_auth/services.tsx +++ b/apps/web/src/routes/_auth/services.tsx @@ -159,11 +159,22 @@ function ServicesPage() { data, isLoading, isError, + isRefetchError, error, refetch, } = useQuery(serviceGroupsQueryOptions()) const { data: domains } = useQuery(domainsListQueryOptions()) + // Refetch after create/update must not wipe the list when cached data remains. + useEffect(() => { + if (!isRefetchError || !data) return + toast.error( + error instanceof Error && error.message + ? error.message + : 'Не удалось обновить список сервисов', + ) + }, [isRefetchError, data, error]) + const dragDisabled = domainId != null const { @@ -219,18 +230,20 @@ function ServicesPage() { }) } - function invalidateAll() { - queryClient.invalidateQueries({ queryKey: serviceGroupKeys.all }) - queryClient.invalidateQueries({ queryKey: serviceKeys.all }) - queryClient.invalidateQueries({ queryKey: serviceBindingKeys.all }) - queryClient.invalidateQueries({ queryKey: domainKeys.all }) + async function invalidateAll() { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: serviceGroupKeys.all }), + queryClient.invalidateQueries({ queryKey: serviceKeys.all }), + queryClient.invalidateQueries({ queryKey: serviceBindingKeys.all }), + queryClient.invalidateQueries({ queryKey: domainKeys.all }), + ]) } const createGroupMutation = useMutation({ mutationFn: (body: CreateServiceGroupInput) => api.post('/api/v1/service-groups', body), - onSuccess: () => { - invalidateAll() + onSuccess: async () => { + await invalidateAll() setCreateGroupSheetOpen(false) toast.success('Группа создана') }, @@ -242,8 +255,8 @@ function ServicesPage() { const updateGroupMutation = useMutation({ mutationFn: ({ id, body }: { id: number; body: CreateServiceGroupInput }) => api.patch(`/api/v1/service-groups/${id}`, body), - onSuccess: () => { - invalidateAll() + onSuccess: async () => { + await invalidateAll() setEditingGroup(null) toast.success('Группа сохранена, DNS синхронизируется') }, @@ -259,16 +272,21 @@ function ServicesPage() { slug: body.slug, service_group_id: body.service_group_id ?? null, }) + if (created?.id == null) { + throw new Error('Сервер не вернул id созданного сервиса') + } const hasConfig = body.ips.length > 0 || body.domains.length > 0 if (!hasConfig) return created return api.patch(`/api/v1/services/${created.id}`, { ips: body.ips, domains: body.domains, service_group_id: body.service_group_id ?? null, + ...(body.lb_weight != null ? { lb_weight: body.lb_weight } : {}), + ...(body.lb_priority != null ? { lb_priority: body.lb_priority } : {}), }) }, - onSuccess: () => { - invalidateAll() + onSuccess: async () => { + await invalidateAll() setCreateSheetOpen(false) toast.success('Сервис создан') }, @@ -280,8 +298,8 @@ function ServicesPage() { const updateServiceMutation = useMutation({ mutationFn: ({ id, body }: { id: number; body: UpdateServiceConfigInput }) => api.patch(`/api/v1/services/${id}`, body), - onSuccess: () => { - invalidateAll() + onSuccess: async () => { + await invalidateAll() setEditingService(null) clearServiceSearch() toast.success('Сервис сохранён') @@ -296,8 +314,8 @@ function ServicesPage() { const deleteServiceMutation = useMutation({ mutationFn: (id: number) => api.delete(`/api/v1/services/${id}`), - onSuccess: () => { - invalidateAll() + onSuccess: async () => { + await invalidateAll() setEditingService(null) setDeletingService(null) clearServiceSearch() @@ -340,16 +358,16 @@ function ServicesPage() { : 'Сервис выключен, DNS-записи удалены из Cloudflare', ) }, - onSettled: () => { + onSettled: async () => { setTogglingServiceId(null) - invalidateAll() + await invalidateAll() }, }) const deleteGroupMutation = useMutation({ mutationFn: (id: number) => api.delete(`/api/v1/service-groups/${id}`), - onSuccess: () => { - invalidateAll() + onSuccess: async () => { + await invalidateAll() setDeletingGroup(null) toast.success('Группа удалена') }, @@ -430,7 +448,7 @@ function ServicesPage() { } setBulkToggling(false) - invalidateAll() + await invalidateAll() } const columnConfigs = useMemo( @@ -598,7 +616,7 @@ function ServicesPage() { ) } - if (isError) { + if (isError && !data) { return ( diff --git a/packages/db/dist/index.js b/packages/db/dist/index.js index f2a1a43..62df0e5 100644 --- a/packages/db/dist/index.js +++ b/packages/db/dist/index.js @@ -29,7 +29,7 @@ var services = sqliteTable("services", { { onDelete: "set null" } ), subdomain: text("subdomain"), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(false), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), sort_order: integer("sort_order").notNull().default(0), lb_weight: integer("lb_weight").notNull().default(1), lb_priority: integer("lb_priority").notNull().default(1), @@ -932,7 +932,7 @@ function getService(db, id) { } function createService(db, name, slug) { const sortOrder = maxSortOrderInGroup(db, null) + 1; - const id = db.insert(services).values({ name, slug, subdomain: slug, sort_order: sortOrder }).returning({ id: services.id }).get().id; + const id = db.insert(services).values({ name, slug, subdomain: slug, sort_order: sortOrder, enabled: true }).returning({ id: services.id }).get().id; return getService(db, id); } function updateService(db, id, name, slug) { diff --git a/packages/db/src/repos.ts b/packages/db/src/repos.ts index d2233e5..9f8212b 100644 --- a/packages/db/src/repos.ts +++ b/packages/db/src/repos.ts @@ -649,7 +649,7 @@ export function createService(db: Db, name: string, slug: string): Service { const sortOrder = maxSortOrderInGroup(db, null) + 1; const id = db .insert(services) - .values({ name, slug, subdomain: slug, sort_order: sortOrder }) + .values({ name, slug, subdomain: slug, sort_order: sortOrder, enabled: true }) .returning({ id: services.id }) .get()!.id; return getService(db, id); diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 5419fdb..db89901 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -28,7 +28,7 @@ export const services = sqliteTable("services", { { onDelete: "set null" }, ), subdomain: text("subdomain"), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(false), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), sort_order: integer("sort_order").notNull().default(0), lb_weight: integer("lb_weight").notNull().default(1), lb_priority: integer("lb_priority").notNull().default(1), diff --git a/packages/shared/dist/index.d.ts b/packages/shared/dist/index.d.ts index 7049f79..2ef968c 100644 --- a/packages/shared/dist/index.d.ts +++ b/packages/shared/dist/index.d.ts @@ -304,8 +304,8 @@ declare const serviceGroupSchema: z.ZodObject<{ bgp: "bgp"; custom: "custom"; }>>; - icon: z.ZodNullable; - domain: z.ZodNullable; + icon: z.ZodDefault>; + domain: z.ZodDefault>; enabled: z.ZodCoercedBoolean; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; @@ -353,8 +353,8 @@ declare const serviceDomainBindingSchema: z.ZodPipe>; target_ips: z.ZodOptional>; target_ip: z.ZodOptional>; - target_ip_weights: z.ZodOptional>; - target_ip_priorities: z.ZodOptional>; + target_ip_weights: z.ZodOptional>>; + target_ip_priorities: z.ZodOptional>>; target_cname: z.ZodOptional>; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; - sync_status: z.ZodNullable; + sync_status: z.ZodDefault>; }, z.core.$strip>, z.ZodTransform<{ target_ips: string[]; target_ip_weights: Record; @@ -445,8 +445,8 @@ declare const serviceViewSchema: z.ZodObject<{ }>>; target_ips: z.ZodOptional>; target_ip: z.ZodOptional>; - target_ip_weights: z.ZodOptional>; - target_ip_priorities: z.ZodOptional>; + target_ip_weights: z.ZodOptional>>; + target_ip_priorities: z.ZodOptional>>; target_cname: z.ZodOptional>; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; - sync_status: z.ZodNullable; + sync_status: z.ZodDefault>; }, z.core.$strip>, z.ZodTransform<{ target_ips: string[]; target_ip_weights: Record; @@ -530,8 +530,8 @@ declare const serviceGroupViewSchema: z.ZodObject<{ bgp: "bgp"; custom: "custom"; }>>; - icon: z.ZodNullable; - domain: z.ZodNullable; + icon: z.ZodDefault>; + domain: z.ZodDefault>; enabled: z.ZodCoercedBoolean; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; @@ -578,8 +578,8 @@ declare const serviceGroupViewSchema: z.ZodObject<{ }>>; target_ips: z.ZodOptional>; target_ip: z.ZodOptional>; - target_ip_weights: z.ZodOptional>; - target_ip_priorities: z.ZodOptional>; + target_ip_weights: z.ZodOptional>>; + target_ip_priorities: z.ZodOptional>>; target_cname: z.ZodOptional>; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; - sync_status: z.ZodNullable; + sync_status: z.ZodDefault>; }, z.core.$strip>, z.ZodTransform<{ target_ips: string[]; target_ip_weights: Record; @@ -672,8 +672,8 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{ bgp: "bgp"; custom: "custom"; }>>; - icon: z.ZodNullable; - domain: z.ZodNullable; + icon: z.ZodDefault>; + domain: z.ZodDefault>; enabled: z.ZodCoercedBoolean; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; @@ -720,8 +720,8 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{ }>>; target_ips: z.ZodOptional>; target_ip: z.ZodOptional>; - target_ip_weights: z.ZodOptional>; - target_ip_priorities: z.ZodOptional>; + target_ip_weights: z.ZodOptional>>; + target_ip_priorities: z.ZodOptional>>; target_cname: z.ZodOptional>; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; - sync_status: z.ZodNullable; + sync_status: z.ZodDefault>; }, z.core.$strip>, z.ZodTransform<{ target_ips: string[]; target_ip_weights: Record; @@ -828,8 +828,8 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{ }>>; target_ips: z.ZodOptional>; target_ip: z.ZodOptional>; - target_ip_weights: z.ZodOptional>; - target_ip_priorities: z.ZodOptional>; + target_ip_weights: z.ZodOptional>>; + target_ip_priorities: z.ZodOptional>>; target_cname: z.ZodOptional>; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; - sync_status: z.ZodNullable; + sync_status: z.ZodDefault>; }, z.core.$strip>, z.ZodTransform<{ target_ips: string[]; target_ip_weights: Record; @@ -967,8 +967,8 @@ declare const serviceBindingSchema: z.ZodPipe; target_ips: z.ZodOptional>; - target_ip_weights: z.ZodOptional>; - target_ip_priorities: z.ZodOptional>; + target_ip_weights: z.ZodOptional>>; + target_ip_priorities: z.ZodOptional>>; lb_mode: z.ZodCatch>; - health_check_port: z.ZodNullable; - health_check_path: z.ZodNullable; - health_check_expected_status: z.ZodNullable; + health_check_port: z.ZodDefault>; + health_check_path: z.ZodDefault>; + health_check_expected_status: z.ZodDefault>; health_check_interval_sec: z.ZodDefault; health_check_timeout_ms: z.ZodDefault; health_check_verify_tls: z.ZodDefault>; - sync_status: z.ZodNullable; + sync_status: z.ZodDefault>; created_at: z.ZodString; updated_at: z.ZodString; }, z.core.$strip>, z.ZodTransform<{ diff --git a/packages/shared/dist/index.js b/packages/shared/dist/index.js index 6608452..1c8275c 100644 --- a/packages/shared/dist/index.js +++ b/packages/shared/dist/index.js @@ -203,15 +203,15 @@ var serviceGroupSchema = z.object({ id: z.number(), name: z.string(), type: serviceGroupTypeSchema.catch("custom"), - icon: z.string().nullable(), - domain: z.string().nullable(), + icon: z.string().nullable().default(null), + domain: z.string().nullable().default(null), enabled: z.coerce.boolean(), lb_mode: lbModeSchema.catch("round_robin"), health_check_enabled: z.coerce.boolean().default(false), health_check_type: healthCheckTypeSchema.catch("tcp"), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3e3), health_check_verify_tls: z.coerce.boolean().default(false), @@ -240,19 +240,19 @@ var serviceDomainBindingSchema = z.object({ record_type: z.enum(["A", "CNAME"]).default("A"), target_ips: z.array(z.string()).optional(), target_ip: z.string().nullable().optional(), - target_ip_weights: z.record(z.string(), z.number()).optional(), - target_ip_priorities: z.record(z.string(), z.number()).optional(), + target_ip_weights: z.record(z.string(), z.coerce.number()).optional(), + target_ip_priorities: z.record(z.string(), z.coerce.number()).optional(), target_cname: z.string().nullable().optional(), lb_mode: lbModeSchema.catch("round_robin"), health_check_enabled: z.coerce.boolean().default(false), health_check_type: healthCheckTypeSchema.catch("tcp"), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3e3), health_check_verify_tls: z.coerce.boolean().default(false), - sync_status: z.string().nullable() + sync_status: z.string().nullable().default(null) }).transform((binding) => ({ ...binding, target_ips: binding.target_ips && binding.target_ips.length > 0 ? binding.target_ips : binding.target_ip ? [binding.target_ip] : [], @@ -310,18 +310,18 @@ var serviceBindingSchema = z.object({ service_slug: z.string(), target_ip: z.string().nullable(), target_ips: z.array(z.string()).optional(), - target_ip_weights: z.record(z.string(), z.number()).optional(), - target_ip_priorities: z.record(z.string(), z.number()).optional(), + target_ip_weights: z.record(z.string(), z.coerce.number()).optional(), + target_ip_priorities: z.record(z.string(), z.coerce.number()).optional(), lb_mode: lbModeSchema.catch("round_robin"), health_check_enabled: z.coerce.boolean().default(false), health_check_type: healthCheckTypeSchema.catch("tcp"), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3e3), health_check_verify_tls: z.coerce.boolean().default(false), - sync_status: z.string().nullable(), + sync_status: z.string().nullable().default(null), created_at: z.string(), updated_at: z.string() }).transform((binding) => ({ diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index 9e5a664..d1dfed1 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -59,15 +59,15 @@ export const serviceGroupSchema = z.object({ id: z.number(), name: z.string(), type: serviceGroupTypeSchema.catch('custom'), - icon: z.string().nullable(), - domain: z.string().nullable(), + icon: z.string().nullable().default(null), + domain: z.string().nullable().default(null), enabled: z.coerce.boolean(), lb_mode: lbModeSchema.catch('round_robin'), health_check_enabled: z.coerce.boolean().default(false), health_check_type: healthCheckTypeSchema.catch('tcp'), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3000), health_check_verify_tls: z.coerce.boolean().default(false), @@ -99,19 +99,19 @@ export const serviceDomainBindingSchema = z record_type: z.enum(['A', 'CNAME']).default('A'), target_ips: z.array(z.string()).optional(), target_ip: z.string().nullable().optional(), - target_ip_weights: z.record(z.string(), z.number()).optional(), - target_ip_priorities: z.record(z.string(), z.number()).optional(), + target_ip_weights: z.record(z.string(), z.coerce.number()).optional(), + target_ip_priorities: z.record(z.string(), z.coerce.number()).optional(), target_cname: z.string().nullable().optional(), lb_mode: lbModeSchema.catch('round_robin'), health_check_enabled: z.coerce.boolean().default(false), health_check_type: healthCheckTypeSchema.catch('tcp'), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3000), health_check_verify_tls: z.coerce.boolean().default(false), - sync_status: z.string().nullable(), + sync_status: z.string().nullable().default(null), }) .transform((binding) => ({ ...binding, @@ -184,18 +184,18 @@ export const serviceBindingSchema = z service_slug: z.string(), target_ip: z.string().nullable(), target_ips: z.array(z.string()).optional(), - target_ip_weights: z.record(z.string(), z.number()).optional(), - target_ip_priorities: z.record(z.string(), z.number()).optional(), + target_ip_weights: z.record(z.string(), z.coerce.number()).optional(), + target_ip_priorities: z.record(z.string(), z.coerce.number()).optional(), lb_mode: lbModeSchema.catch('round_robin'), health_check_enabled: z.coerce.boolean().default(false), health_check_type: healthCheckTypeSchema.catch('tcp'), - health_check_port: z.number().nullable(), - health_check_path: z.string().nullable(), - health_check_expected_status: z.number().nullable(), + health_check_port: z.number().nullable().default(null), + health_check_path: z.string().nullable().default(null), + health_check_expected_status: z.number().nullable().default(null), health_check_interval_sec: z.number().default(30), health_check_timeout_ms: z.number().default(3000), health_check_verify_tls: z.coerce.boolean().default(false), - sync_status: z.string().nullable(), + sync_status: z.string().nullable().default(null), created_at: z.string(), updated_at: z.string(), })