From 0561d2318e4bb1c503ff5a6e6883aea7230d1df8 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 17 Jan 2025 12:53:57 +0100 Subject: [PATCH 1/2] fix(import-header-error): add zod validation and throwing proper error message --- app/modules/asset/types.ts | 18 ++++++---------- app/modules/asset/utils.server.ts | 14 +++++++++++++ .../admin-dashboard+/org.$organizationId.tsx | 7 ++++++- app/routes/_layout+/assets.import.tsx | 8 +++++-- app/utils/import.server.ts | 21 +++++++++++++++++-- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/app/modules/asset/types.ts b/app/modules/asset/types.ts index 72315f235..1846a3f03 100644 --- a/app/modules/asset/types.ts +++ b/app/modules/asset/types.ts @@ -12,7 +12,9 @@ import type { CustomFieldType, } from "@prisma/client"; import type { Return } from "@prisma/client/runtime/library"; +import type { z } from "zod"; import type { assetIndexFields } from "./fields"; +import type { importAssetsSchema } from "./utils.server"; export interface ICustomFieldValueJson { raw: string | number | boolean; @@ -44,18 +46,10 @@ export interface UpdateAssetPayload { valuation?: Asset["valuation"]; } -export interface CreateAssetFromContentImportPayload - extends Record { - title: string; - description?: string; - category?: string; - kit?: string; - tags: string[]; - location?: string; - custodian?: string; - bookable?: "yes" | "no"; - imageUrl?: string; // URL of the image to import -} +export type CreateAssetFromContentImportPayload = z.infer< + typeof importAssetsSchema +>; + export interface CreateAssetFromBackupImportPayload extends Record { id: string; diff --git a/app/modules/asset/utils.server.ts b/app/modules/asset/utils.server.ts index 0519c1195..fd32b2013 100644 --- a/app/modules/asset/utils.server.ts +++ b/app/modules/asset/utils.server.ts @@ -205,3 +205,17 @@ export function validateAdvancedFilterParams( return validatedParams; } + +export const importAssetsSchema = z + .object({ + title: z.string(), + description: z.string().optional(), + category: z.string().optional(), + kit: z.string().optional(), + tags: z.string().array(), + location: z.string().optional(), + custodian: z.string().optional(), + bookable: z.enum(["yes", "no"]).optional(), + imageUrl: z.string().url().optional(), + }) + .and(z.record(z.string().startsWith("cf"), z.any())); diff --git a/app/routes/_layout+/admin-dashboard+/org.$organizationId.tsx b/app/routes/_layout+/admin-dashboard+/org.$organizationId.tsx index 43f353257..674921153 100644 --- a/app/routes/_layout+/admin-dashboard+/org.$organizationId.tsx +++ b/app/routes/_layout+/admin-dashboard+/org.$organizationId.tsx @@ -20,6 +20,7 @@ import HorizontalTabs from "~/components/layout/horizontal-tabs"; import { Button } from "~/components/shared/button"; import { db } from "~/database/db.server"; import { createAssetsFromContentImport } from "~/modules/asset/service.server"; +import { importAssetsSchema } from "~/modules/asset/utils.server"; import { toggleOrganizationSso } from "~/modules/organization/service.server"; import { csvDataFromRequest } from "~/utils/csv.server"; import { ShelfError, makeShelfError } from "~/utils/error"; @@ -154,7 +155,11 @@ export const action = async ({ label: "Assets", }); } - const contentData = extractCSVDataFromContentImport(csvData); + + const contentData = extractCSVDataFromContentImport( + csvData, + importAssetsSchema.array() + ); await createAssetsFromContentImport({ data: contentData, userId, diff --git a/app/routes/_layout+/assets.import.tsx b/app/routes/_layout+/assets.import.tsx index a280b1453..240d8b040 100644 --- a/app/routes/_layout+/assets.import.tsx +++ b/app/routes/_layout+/assets.import.tsx @@ -18,6 +18,7 @@ import { TabsTrigger, } from "~/components/shared/tabs"; import { createAssetsFromContentImport } from "~/modules/asset/service.server"; +import { importAssetsSchema } from "~/modules/asset/utils.server"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; import { csvDataFromRequest } from "~/utils/csv.server"; import { ShelfError, makeShelfError } from "~/utils/error"; @@ -52,7 +53,6 @@ export const action = async ({ context, request }: ActionFunctionArgs) => { ); const csvData = await csvDataFromRequest({ request }); - if (csvData.length < 2) { throw new ShelfError({ cause: null, @@ -63,7 +63,11 @@ export const action = async ({ context, request }: ActionFunctionArgs) => { }); } - const contentData = extractCSVDataFromContentImport(csvData); + const contentData = extractCSVDataFromContentImport( + csvData, + importAssetsSchema.array() + ); + await createAssetsFromContentImport({ data: contentData, userId, diff --git a/app/utils/import.server.ts b/app/utils/import.server.ts index 285a15f7f..b063cd14d 100644 --- a/app/utils/import.server.ts +++ b/app/utils/import.server.ts @@ -1,4 +1,6 @@ +import type { ZodSchema } from "zod"; import type { CreateAssetFromContentImportPayload } from "~/modules/asset/types"; +import { ShelfError } from "./error"; /* This function receives an array of object and a key name * It then extracts all the values of that key and makes sure there are no duplicates @@ -25,7 +27,10 @@ export function getUniqueValuesFromArrayOfObjects({ } /** Takes the CSV data from a `content` import and parses it into an object that we can then use to create the entries */ -export function extractCSVDataFromContentImport(data: string[][]) { +export function extractCSVDataFromContentImport( + data: string[][], + schema: Schema +) { /** * The first row of the CSV contains the keys for the data * We need to trim the keys to remove any whitespace and special characters and Non-printable characters as it already causes issues with in the past @@ -33,7 +38,7 @@ export function extractCSVDataFromContentImport(data: string[][]) { */ const keys = data[0].map((key) => key.trim()); // Trim the keys const values = data.slice(1) as string[][]; - return values.map((entry) => + const rawData = values.map((entry) => Object.fromEntries( entry.map((value, index) => { switch (keys[index]) { @@ -48,6 +53,18 @@ export function extractCSVDataFromContentImport(data: string[][]) { }) ) ); + + const parseResult = schema.safeParse(rawData); + if (!parseResult.success) { + throw new ShelfError({ + cause: null, + message: + "Received invalid data, please update the file with proper headers and data.", + label: "Assets", + }); + } + + return parseResult.data as Schema["_output"]; } /** Takes the CSV data from a `backup` import and parses it into an object that we can then use to create the entries */ From 7d2327e1c335ec6dee03925a50f47242037adcf3 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 17 Jan 2025 13:47:31 +0100 Subject: [PATCH 2/2] update zod schema for import assets --- app/modules/asset/utils.server.ts | 2 +- app/utils/import.server.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/modules/asset/utils.server.ts b/app/modules/asset/utils.server.ts index fd32b2013..14989bd66 100644 --- a/app/modules/asset/utils.server.ts +++ b/app/modules/asset/utils.server.ts @@ -218,4 +218,4 @@ export const importAssetsSchema = z bookable: z.enum(["yes", "no"]).optional(), imageUrl: z.string().url().optional(), }) - .and(z.record(z.string().startsWith("cf"), z.any())); + .and(z.record(z.string(), z.any())); diff --git a/app/utils/import.server.ts b/app/utils/import.server.ts index b063cd14d..2656db69e 100644 --- a/app/utils/import.server.ts +++ b/app/utils/import.server.ts @@ -54,8 +54,8 @@ export function extractCSVDataFromContentImport( ) ); - const parseResult = schema.safeParse(rawData); - if (!parseResult.success) { + const parsedResult = schema.safeParse(rawData); + if (!parsedResult.success) { throw new ShelfError({ cause: null, message: @@ -64,7 +64,7 @@ export function extractCSVDataFromContentImport( }); } - return parseResult.data as Schema["_output"]; + return parsedResult.data as Schema["_output"]; } /** Takes the CSV data from a `backup` import and parses it into an object that we can then use to create the entries */