Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(import-header-error): add zod validation and showing proper error message for invalid data #1584

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions app/modules/asset/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,18 +46,10 @@ export interface UpdateAssetPayload {
valuation?: Asset["valuation"];
}

export interface CreateAssetFromContentImportPayload
extends Record<string, any> {
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<string, any> {
id: string;
Expand Down
14 changes: 14 additions & 0 deletions app/modules/asset/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
7 changes: 6 additions & 1 deletion app/routes/_layout+/admin-dashboard+/org.$organizationId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions app/routes/_layout+/assets.import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
21 changes: 19 additions & 2 deletions app/utils/import.server.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,15 +27,18 @@ 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<Schema extends ZodSchema>(
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
* Non-printable character: The non-printable character you encountered at the beginning of the title property key ('\ufeff') is known as the Unicode BOM (Byte Order Mark).
*/
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]) {
Expand All @@ -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 */
Expand Down
Loading