refactor: dry validation rules

This commit is contained in:
Lorenzo Migliorero
2024-07-25 11:03:14 +02:00
parent 4cacc6b3d1
commit ee58672d58
3 changed files with 57 additions and 53 deletions

View File

@@ -28,23 +28,14 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
const domain = z.object({ import { domain } from "@/server/db/validations";
host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }), import { zodResolver } from "@hookform/resolvers/zod";
path: z.string().min(1), import { flushSync } from "react-dom";
port: z import type z from "zod";
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" }),
https: z.boolean(),
certificateType: z.enum(["letsencrypt", "none"]),
});
type Domain = z.infer<typeof domain>; type Domain = z.infer<typeof domain>;
@@ -74,16 +65,7 @@ export const AddDomain = ({
? api.domain.update.useMutation() ? api.domain.update.useMutation()
: api.domain.create.useMutation(); : api.domain.create.useMutation();
const defaultValues: Domain = {
host: "",
https: false,
path: "/",
port: 3000,
certificateType: "none",
};
const form = useForm<Domain>({ const form = useForm<Domain>({
defaultValues,
resolver: zodResolver(domain), resolver: zodResolver(domain),
}); });
@@ -91,8 +73,9 @@ export const AddDomain = ({
if (data) { if (data) {
form.reset({ form.reset({
...data, ...data,
path: data.path || defaultValues.path, /* Convert null to undefined */
port: data.port || defaultValues.port, path: data.path || undefined,
port: data.port || undefined,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@@ -120,11 +103,19 @@ export const AddDomain = ({
applicationId, applicationId,
}); });
await utils.application.readTraefikConfig.invalidate({ applicationId }); await utils.application.readTraefikConfig.invalidate({ applicationId });
/*
Reset form if it was a new domain
Flushsync is needed for a bug witht he react-hook-form reset method
https://github.com/orgs/react-hook-form/discussions/7589#discussioncomment-10060621
*/
if (!domainId) {
flushSync(() => form.reset());
}
setIsOpen(false); setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error(dictionary.error); toast.error(dictionary.error);
setIsOpen(false);
}); });
}; };
return ( return (
@@ -239,6 +230,7 @@ export const AddDomain = ({
<FormDescription> <FormDescription>
Automatically provision SSL Certificate. Automatically provision SSL Certificate.
</FormDescription> </FormDescription>
<FormMessage />
</div> </div>
<FormControl> <FormControl>
<Switch <Switch

View File

@@ -1,8 +1,8 @@
import { domain } from "@/server/db/validations";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core"; import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application"; import { applications } from "./application";
import { certificateType } from "./shared"; import { certificateType } from "./shared";
@@ -31,27 +31,17 @@ export const domainsRelations = relations(domains, ({ one }) => ({
references: [applications.applicationId], references: [applications.applicationId],
}), }),
})); }));
const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
const createSchema = createInsertSchema(domains, {
domainId: z.string().min(1),
host: z.string().min(1),
path: z.string().min(1),
port: z.number(),
https: z.boolean(),
applicationId: z.string(),
certificateType: z.enum(["letsencrypt", "none"]),
});
export const apiCreateDomain = createSchema const createSchema = createInsertSchema(domains, domain._def.schema.shape);
.pick({
host: true, export const apiCreateDomain = createSchema.pick({
path: true, host: true,
port: true, path: true,
https: true, port: true,
applicationId: true, https: true,
certificateType: true, applicationId: true,
}) certificateType: true,
.required(); });
export const apiFindDomain = createSchema export const apiFindDomain = createSchema
.pick({ .pick({
@@ -59,19 +49,16 @@ export const apiFindDomain = createSchema
}) })
.required(); .required();
export const apiFindDomainByApplication = createSchema export const apiFindDomainByApplication = createSchema.pick({
.pick({ applicationId: true,
applicationId: true, });
})
.required();
export const apiUpdateDomain = createSchema export const apiUpdateDomain = createSchema
.pick({ .pick({
domainId: true,
host: true, host: true,
path: true, path: true,
port: true, port: true,
https: true, https: true,
certificateType: true, certificateType: true,
}) })
.required(); .merge(createSchema.pick({ domainId: true }).required());

View File

@@ -0,0 +1,25 @@
import { z } from "zod";
export const domain = z
.object({
host: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/, {
message: "Invalid hostname",
}),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});