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..14989bd66 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(), 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..2656db69e 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 parsedResult = schema.safeParse(rawData); + if (!parsedResult.success) { + throw new ShelfError({ + cause: null, + message: + "Received invalid data, please update the file with proper headers and data.", + label: "Assets", + }); + } + + 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 */