From 65a7f436febe2907128892a3af31c58a1544c939 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Fri, 17 Oct 2025 21:01:04 +0200 Subject: [PATCH] fix: clean undefined values before posting form --- .../app/components/create-volume-dialog.tsx | 2 +- .../app/components/create-volume-form.tsx | 21 ++++++++++--------- apps/client/app/utils/object.ts | 14 +++++++++++++ apps/client/package.json | 2 +- apps/server/src/modules/volumes/volume.dto.ts | 10 ++++----- bun.lock | 16 ++++++++++---- packages/schemas/src/index.ts | 11 +++------- 7 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 apps/client/app/utils/object.ts diff --git a/apps/client/app/components/create-volume-dialog.tsx b/apps/client/app/components/create-volume-dialog.tsx index 6098c46..e4d8880 100644 --- a/apps/client/app/components/create-volume-dialog.tsx +++ b/apps/client/app/components/create-volume-dialog.tsx @@ -39,7 +39,7 @@ export const CreateVolumeDialog = ({ open, setOpen }: Props) => { - + Create volume diff --git a/apps/client/app/components/create-volume-form.tsx b/apps/client/app/components/create-volume-form.tsx index 2f0e138..deb6606 100644 --- a/apps/client/app/components/create-volume-form.tsx +++ b/apps/client/app/components/create-volume-form.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { testConnectionMutation } from "~/api-client/@tanstack/react-query.gen"; import { cn, slugify } from "~/lib/utils"; +import { deepClean } from "~/utils/object"; import { Button } from "./ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Input } from "./ui/input"; @@ -15,6 +16,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". export const formSchema = type({ name: "2<=string<=32", }).and(volumeConfigSchema); +const cleanSchema = type.pipe((d) => formSchema(deepClean(d))); export type FormValues = typeof formSchema.inferIn; @@ -36,7 +38,7 @@ const defaultValuesForType = { export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => { const form = useForm({ - resolver: arktypeResolver(formSchema), + resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema), defaultValues: initialValues, resetOptions: { keepDefaultValues: true, @@ -311,7 +313,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for Server - + SMB server IP address or hostname. @@ -325,7 +327,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for Share - + SMB share name on the server. @@ -339,7 +341,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for Username - + Username for SMB authentication. @@ -353,7 +355,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for Password - + Password for SMB authentication. @@ -450,11 +452,10 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for {testMessage && (
{testMessage.message}
diff --git a/apps/client/app/utils/object.ts b/apps/client/app/utils/object.ts new file mode 100644 index 0000000..d9d0ffc --- /dev/null +++ b/apps/client/app/utils/object.ts @@ -0,0 +1,14 @@ +export function deepClean(obj: T): T { + if (Array.isArray(obj)) { + return obj.map(deepClean).filter((v) => v !== undefined && v !== null) as T; + } + + if (obj && typeof obj === "object") { + return Object.entries(obj).reduce((acc, [key, value]) => { + const cleaned = deepClean(value); + if (cleaned !== undefined) acc[key as keyof T] = cleaned; + return acc; + }, {} as T); + } + return obj; +} diff --git a/apps/client/package.json b/apps/client/package.json index 39a3ae6..0dc436a 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -25,7 +25,7 @@ "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", "@tanstack/react-table": "^8.21.3", - "arktype": "^2.1.22", + "arktype": "^2.1.23", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/apps/server/src/modules/volumes/volume.dto.ts b/apps/server/src/modules/volumes/volume.dto.ts index 0b59d44..d3c4ae5 100644 --- a/apps/server/src/modules/volumes/volume.dto.ts +++ b/apps/server/src/modules/volumes/volume.dto.ts @@ -1,4 +1,4 @@ -import { volumeConfigSchemaNoUndefined } from "@ironmount/schemas"; +import { volumeConfigSchema } from "@ironmount/schemas"; import { type } from "arktype"; import { describeRoute, resolver } from "hono-openapi"; @@ -11,7 +11,7 @@ const volumeSchema = type({ createdAt: "number", updatedAt: "number", lastHealthCheck: "number", - config: volumeConfigSchemaNoUndefined, + config: volumeConfigSchema, autoRemount: "boolean", }); @@ -46,7 +46,7 @@ export const listVolumesDto = describeRoute({ */ export const createVolumeBody = type({ name: "string", - config: volumeConfigSchemaNoUndefined, + config: volumeConfigSchema, }); export const createVolumeResponse = type({ @@ -135,7 +135,7 @@ export const getVolumeDto = describeRoute({ */ export const updateVolumeBody = type({ autoRemount: "boolean?", - config: volumeConfigSchemaNoUndefined.optional(), + config: volumeConfigSchema.optional(), }); export type UpdateVolumeBody = typeof updateVolumeBody.infer; @@ -170,7 +170,7 @@ export type UpdateVolumeResponseDto = typeof updateVolumeResponse.infer; * Test connection */ export const testConnectionBody = type({ - config: volumeConfigSchemaNoUndefined, + config: volumeConfigSchema, }); export const testConnectionResponse = type({ diff --git a/bun.lock b/bun.lock index 3cef516..39f41ac 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", "@tanstack/react-table": "^8.21.3", - "arktype": "^2.1.22", + "arktype": "^2.1.23", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -96,9 +96,11 @@ "@tailwindcss/oxide", ], "packages": { - "@ark/schema": ["@ark/schema@0.49.0", "", { "dependencies": { "@ark/util": "0.49.0" } }, "sha512-GphZBLpW72iS0v4YkeUtV3YIno35Gimd7+ezbPO9GwEi9kzdUrPVjvf6aXSBAfHikaFc/9pqZOpv3pOXnC71tw=="], + "@ark/regex": ["@ark/regex@0.0.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg=="], - "@ark/util": ["@ark/util@0.49.0", "", {}, "sha512-/BtnX7oCjNkxi2vi6y1399b+9xd1jnCrDYhZ61f0a+3X8x8DxlK52VgEEzyuC2UQMPACIfYrmHkhD3lGt2GaMA=="], + "@ark/schema": ["@ark/schema@0.50.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ=="], + + "@ark/util": ["@ark/util@0.50.0", "", {}, "sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], @@ -570,7 +572,7 @@ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "arktype": ["arktype@2.1.22", "", { "dependencies": { "@ark/schema": "0.49.0", "@ark/util": "0.49.0" } }, "sha512-xdzl6WcAhrdahvRRnXaNwsipCgHuNoLobRqhiP8RjnfL9Gp947abGlo68GAIyLtxbD+MLzNyH2YR4kEqioMmYQ=="], + "arktype": ["arktype@2.1.23", "", { "dependencies": { "@ark/regex": "0.0.0", "@ark/schema": "0.50.0", "@ark/util": "0.50.0" } }, "sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], @@ -1294,6 +1296,8 @@ "@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@ironmount/server/arktype": ["arktype@2.1.22", "", { "dependencies": { "@ark/schema": "0.49.0", "@ark/util": "0.49.0" } }, "sha512-xdzl6WcAhrdahvRRnXaNwsipCgHuNoLobRqhiP8RjnfL9Gp947abGlo68GAIyLtxbD+MLzNyH2YR4kEqioMmYQ=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -1426,6 +1430,10 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@ironmount/server/arktype/@ark/schema": ["@ark/schema@0.49.0", "", { "dependencies": { "@ark/util": "0.49.0" } }, "sha512-GphZBLpW72iS0v4YkeUtV3YIno35Gimd7+ezbPO9GwEi9kzdUrPVjvf6aXSBAfHikaFc/9pqZOpv3pOXnC71tw=="], + + "@ironmount/server/arktype/@ark/util": ["@ark/util@0.49.0", "", {}, "sha512-/BtnX7oCjNkxi2vi6y1399b+9xd1jnCrDYhZ61f0a+3X8x8DxlK52VgEEzyuC2UQMPACIfYrmHkhD3lGt2GaMA=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index 44c8c87..28d8604 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -24,7 +24,7 @@ export const smbConfigSchema = type({ username: "string", password: "string", vers: type("'1.0' | '2.0' | '2.1' | '3.0'").default("3.0"), - domain: "string | undefined?", + domain: "string?", port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(445), }); @@ -36,17 +36,12 @@ export const webdavConfigSchema = type({ backend: "'webdav'", server: "string", path: "string", - username: "string | undefined?", - password: "string | undefined?", + username: "string?", + password: "string?", port: type("string.integer").or(type("number")).to("1 <= number <= 65536").default(80), ssl: "boolean?", }); -export const volumeConfigSchemaNoUndefined = nfsConfigSchema - .or(smbConfigSchema.omit("domain").and(type({ domain: "string?" }))) - .or(webdavConfigSchema.omit("username", "password").and(type({ username: "string?", password: "string?" }))) - .or(directoryConfigSchema); - export const volumeConfigSchema = nfsConfigSchema.or(smbConfigSchema).or(webdavConfigSchema).or(directoryConfigSchema); export type BackendConfig = typeof volumeConfigSchema.infer;