From 4cfa5f21ae995f62d6997aaca8607fed90353c04 Mon Sep 17 00:00:00 2001 From: Solomon Hawk Date: Sat, 9 Nov 2024 12:38:42 -0500 Subject: [PATCH] Add max length to table title and slug, handle validation errors in auto-generated table slugs --- packages/db/src/service/table.ts | 15 ++++++++++-- packages/router/src/error.ts | 34 +++++++++++++++++++++------- packages/router/src/routers/table.ts | 9 ++++++++ packages/validators/src/error.ts | 5 ++++ packages/validators/src/index.ts | 2 ++ packages/validators/src/schemas.ts | 2 ++ packages/validators/src/table.ts | 10 +++++--- 7 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 packages/validators/src/error.ts diff --git a/packages/db/src/service/table.ts b/packages/db/src/service/table.ts index ba7e549..7895771 100644 --- a/packages/db/src/service/table.ts +++ b/packages/db/src/service/table.ts @@ -1,6 +1,9 @@ import { slugify } from "@manifold/lib"; import { + invalidTableSlugMessage, + slug, type TableCreateInput, + tableCreateInput, type TableDeleteInput, type TableGetInput, type TableListInput, @@ -47,12 +50,20 @@ export async function listTables(userId: string, input: TableListInput) { } export async function createTable(userId: string, input: TableCreateInput) { + const inputWithDefaultSlug = tableCreateInput + .extend({ + slug: slug({ message: invalidTableSlugMessage }), + }) + .parse({ + ...input, + slug: input.slug ?? slugify(input.title), + }); + const [table] = await db .insert(schema.tables) .values({ - ...input, + ...inputWithDefaultSlug, ownerId: userId, - slug: input.slug ?? slugify(input.title), }) .returning() .execute(); diff --git a/packages/router/src/error.ts b/packages/router/src/error.ts index 50425bb..01f3018 100644 --- a/packages/router/src/error.ts +++ b/packages/router/src/error.ts @@ -1,20 +1,38 @@ import { TRPCError } from "@trpc/server"; import { ZodError } from "zod"; -export function validationError({ - path, - message, -}: { +type ErrorPathAndMessage = { path: string[]; message: string; -}) { +}; + +type ErrorCauseAndMessage = { + cause: ZodError; + message: string; +}; + +export function validationError( + causeAndMessage: ErrorCauseAndMessage, +): TRPCError; +export function validationError(pathAndMessage: ErrorPathAndMessage): TRPCError; +export function validationError( + pathOrCauseAndMessage: ErrorPathAndMessage | ErrorCauseAndMessage, +): TRPCError { + if ("cause" in pathOrCauseAndMessage) { + return new TRPCError({ + code: "BAD_REQUEST", + message: pathOrCauseAndMessage.message, + cause: pathOrCauseAndMessage.cause, + }); + } + return new TRPCError({ code: "BAD_REQUEST", - message, + message: pathOrCauseAndMessage.message, cause: new ZodError([ { - message, - path, + message: pathOrCauseAndMessage.message, + path: pathOrCauseAndMessage.path, code: "custom", }, ]), diff --git a/packages/router/src/routers/table.ts b/packages/router/src/routers/table.ts index d126815..cf4f813 100644 --- a/packages/router/src/routers/table.ts +++ b/packages/router/src/routers/table.ts @@ -1,5 +1,6 @@ import { isUniqueConstraintViolation, tableService } from "@manifold/db"; import { + isValidationError, tableCreateInput, tableDeleteInput, tableGetInput, @@ -31,6 +32,14 @@ export const tableRouter = t.router({ }); } + if (isValidationError(e)) { + throw validationError({ + cause: e, + message: + "We couldn’t generate a valid identifier for this table, please specify one explicitly", + }); + } + throw e; } }), diff --git a/packages/validators/src/error.ts b/packages/validators/src/error.ts new file mode 100644 index 0000000..82b6c55 --- /dev/null +++ b/packages/validators/src/error.ts @@ -0,0 +1,5 @@ +import { ZodError } from "zod"; + +export function isValidationError(e: unknown): e is ZodError { + return e instanceof ZodError; +} diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts index 48f5951..5b3d428 100644 --- a/packages/validators/src/index.ts +++ b/packages/validators/src/index.ts @@ -1,3 +1,5 @@ +export * from "./error"; +export * from "./schemas"; export * from "./table"; export * from "./user"; export * from "zod"; diff --git a/packages/validators/src/schemas.ts b/packages/validators/src/schemas.ts index 481c7f0..a27298b 100644 --- a/packages/validators/src/schemas.ts +++ b/packages/validators/src/schemas.ts @@ -5,6 +5,7 @@ export function slug({ message }: { message: string }) { return z .string() .min(1, { message: "Can’t be blank" }) + .max(64, { message: "Must be 64 characters or less" }) .refine((slug) => slug === slugify(slug), { message, }); @@ -13,6 +14,7 @@ export function slug({ message }: { message: string }) { export function optionalSlug({ message }: { message: string }) { return z .string() + .max(64, { message: "Must be 64 characters or less" }) .optional() .transform((x) => (x === "" ? undefined : x)) .refine( diff --git a/packages/validators/src/table.ts b/packages/validators/src/table.ts index fe8660e..a356912 100644 --- a/packages/validators/src/table.ts +++ b/packages/validators/src/table.ts @@ -31,11 +31,15 @@ export const tableListInput = z.object({ export type TableListInput = z.infer; +export const invalidTableSlugMessage = + "Identifier is invalid. Can only contain lowercase letters, numbers, and hyphens"; + export const tableCreateInput = z.object({ - title: z.string().min(1, { message: "Title can’t be blank" }), + title: z.string().min(1, { message: "Title can’t be blank" }).max(64, { + message: "Title must be 64 characters or less", + }), slug: optionalSlug({ - message: - "Identifier is invalid. Can only contain lowercase letters, numbers, and hyphens", + message: invalidTableSlugMessage, }), description: z.string().optional(), definition: z.string(),