Skip to content

Instantly share code, notes, and snippets.

@jjhiggz
Created July 18, 2022 14:08
Show Gist options
  • Save jjhiggz/a75d7d62bb786ea0bc931b9ede29b1f7 to your computer and use it in GitHub Desktop.
Save jjhiggz/a75d7d62bb786ea0bc931b9ede29b1f7 to your computer and use it in GitHub Desktop.
Action Helpers
import { useActionData } from "@remix-run/react";
import useFormReset from "~/hooks/useFormReset";
import type { ActionFunction, LoaderArgs } from "@remix-run/server-runtime";
import invariant from "tiny-invariant";
import { prisma } from "~/db.server";
import { requireUserId } from "~/session.server";
import FormHeader from "~/components/UI/Forms/FormHeader";
import { Form } from "@remix-run/react";
import TextInput from "~/components/UI/Forms/Inputs/TextInput";
import { updateItemSchema } from "~/models/item.server";
import { updateItem } from "~/models/item.server";
import {
handleActionErrors,
throwIfInvalidAction,
validateAction,
} from "~/utils/action-helper/action-helper";
import HiddenInput from "~/components/UI/Forms/Inputs/HiddenInput";
import { useRef } from "react";
import { superjsonify, useSuperLoaderData } from "~/utils/remix-superjson";
type ActionData = Awaited<ReturnType<typeof updateItem>>;
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const userId = await requireUserId(request);
formData.set("userId", userId);
return validateAction({
rawData: formData,
schema: updateItemSchema,
dataFn: updateItem,
})
.then(throwIfInvalidAction)
.catch((errors) => {
console.log("errors", errors);
return handleActionErrors(errors);
});
};
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
invariant(params.itemId, "item not found");
const item = await prisma.item.findFirst({
where: {
userId,
id: params.itemId,
},
});
if (!item) {
throw new Response("Not Found", { status: 404 });
}
return superjsonify({ item });
};
export default function UpdateItemPage() {
const data = useSuperLoaderData<typeof loader>();
const { item } = data;
const formRef = useRef<HTMLFormElement>(null);
const actionData = useActionData<ActionData>();
useFormReset({ formRef, watch: [data] });
return (
<div className="form-container">
<Form method="post" className="main-form" ref={formRef}>
<FormHeader title={item.name} />
<TextInput
inputProps={{
placeholder: "Item name",
name: "name",
defaultValue: item.name,
}}
labelProps={{
htmlFor: "name",
}}
labelText="Item Name"
errorMessage={actionData?.errors?.name}
/>
<TextInput
inputProps={{
placeholder: "Item description",
name: "description",
defaultValue: item.description,
}}
labelProps={{
htmlFor: "description",
}}
labelText="Item Description"
/>
<TextInput
inputProps={{
placeholder: "Item price",
name: "price",
}}
labelProps={{
htmlFor: "price",
}}
labelText="Item Price"
/>
<TextInput
inputProps={{
placeholder: "URL Link",
name: "photourl",
}}
labelProps={{
htmlFor: "photo-url",
}}
labelText="Photo URL"
/>
<fieldset className="dietary-restrictions">
<legend>Dietary Restrictions</legend>
<label htmlFor="">Gluten Free?</label>
<input
type="checkbox"
name="isGlutenFree"
defaultChecked={item.isGlutenFree}
/>
</fieldset>
<input type="hidden" name="chill-bruh" value={"fuckmyass"} />
<input type="hidden" name="yo" value={1} />
<HiddenInput defaultValue={item.id} name="id" />
<div className="form-submit">
<button type="submit" className="">
Update item
</button>
</div>
</Form>
</div>
);
}
import { json } from "@remix-run/server-runtime";
import type * as z from "zod";
import type { ZodError, ZodSchema } from "zod";
import type { NestedKeyOf } from "../types/nested-key.types";
export const loaderHelper = () => {};
export type ErrorMap<T extends Object> = Partial<
Record<NestedKeyOf<T>, string>
>;
export type DataFn<DataReturnType, ZSchema extends ZodSchema> = (
data: z.infer<ZSchema>
) => Promise<ValidatorReturn<DataReturnType>>;
// Will return the type of a validator
export type ValidatorReturn<ReturnDataType extends Object> =
| {
isValid: false;
errors: ErrorMap<ReturnDataType>; // for example {name: "Name is required", age: "age can't be a string"}
error?: string; // if an error isn't specific to a key
data: null;
}
| {
isValid: true;
errors?: null;
error?: null;
data: ReturnDataType;
};
export const validateAction = async <
ZSchema extends ZodSchema,
DataFnReturnType
>({
schema,
rawData,
dataFn,
}: {
schema: ZSchema;
rawData: Partial<z.infer<ZSchema>> | FormData;
dataFn: DataFn<DataFnReturnType, ZSchema>;
}) => {
// First just validate the shape of the data coming in and reject early if anything is wrong
const validateSchemaData = validateSchema(schema, rawData);
const { data: validatedData, isValid } = validateSchemaData;
if (!isValid) {
return validateSchemaData;
}
// Then we need to run the dataFn and validate that.
// For example if you try to create a user and the name is already taken,
// That is a database problem and will fail here. So this just means we have to write our actual methods to return
// isValid, error, data, and errors.
return await dataFn(validatedData);
};
/**
* generates zod errors as a map of key to error message
* ```ts
* const schema = z.object({
* name: z.string(),
* age: z.number(),
* })
*
* try {
* schema.parse({})
* } catch(e) {
* console.log(generateZodErrors(e))
* // {name: "Required", age: "Required"}
* }
*
* ```
*/
export const generateZodErrors = <ZSchema extends ZodSchema>(
error: ZodError
): ErrorMap<z.infer<ZSchema>> => {
let errors = new Map();
error.issues.forEach((issues) => {
errors.set(issues.path.join("."), issues.message);
});
return Object.fromEntries(errors);
};
/**
* generates schema data, as {errors, data, isValid}
* ```ts
* const schema = z.object({name: z.string()})
* console.log(validateSchema(schema, {}))
* // {errors: {name: "Required"}, data: null, isValid: false}
* console.log(validateSchema(schema, {name: "John"}))
* // {errors: null, data: {name: "John"}, isValid: true}
*
* ```
*/
export const validateSchema = <ZSchema extends ZodSchema>(
zodSchema: ZSchema,
data: any
): ValidatorReturn<z.infer<ZSchema>> => {
try {
return {
isValid: true,
errors: null,
data: zodSchema.parse(data),
};
} catch (e) {
return {
isValid: false,
errors: generateZodErrors(e as ZodError),
data: null,
};
}
};
export const throwIfInvalidAction = <T extends { isValid: boolean }>(
input: T
) => {
if (!input.isValid) {
throw new Error(JSON.stringify(input));
}
return input as T & { isValid: true };
};
export const handleActionErrors = (error: Error) => {
return json(JSON.parse(error.message));
};
export const updateItemSchema = zfd.formData({
name: z.string().min(1),
description: z.string(),
isGlutenFree: zfd.checkbox(),
id: z.string(),
userId: z.string().min(1),
});
export const updateItem: DataFn<Item, typeof updateItemSchema> = async (
data: z.infer<typeof updateItemSchema>
) => {
try {
return {
data: await prisma.item.update({
where: { id: data.id },
data,
}),
isValid: true,
errors: null,
};
} catch (e) {
return {
errors: createLoaderErrorFromPrismaError(e as Error),
isValid: false,
data: null,
};
}
};
export const createItemSchema = z.object({
userId: z.string().min(1), // cannot be empty
sectionId: z.string().min(1),
name: z.string().default("").optional(),
description: z.string().default("").optional(),
});
export const createItem: DataFn<Item, typeof createItemSchema> = async ({
name,
description,
userId,
sectionId,
}) => {
const section = await prisma.section
.findUnique({
where: {
id: sectionId,
},
})
.then(falsify)
.catch(returnFalse);
if (!section) {
return {
errors: { sectionId: "The parent menu was not found." },
isValid: false,
data: null,
};
}
try {
return {
isValid: true,
data: await prisma.item.create({
data: {
menuId: section.menuId,
sectionId,
name,
description,
userId,
restaurantId: section.restaurantId,
order: (await getMaxOrder({ userId, sectionId: section.id })) + 1,
},
}),
};
} catch (error) {
console.error("error", error);
return {
errors: createLoaderErrorFromPrismaError(error as Error),
isValid: false,
data: null,
};
}
};
export type NestedFiveLayers<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
export type NestedFourLayers<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? ObjectType[Key] extends Function
? never
: `${Key}` | `${Key}.${NestedFiveLayers<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
export type NestedThreeLayers<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? ObjectType[Key] extends Function
? never
: `${Key}` | `${Key}.${NestedFourLayers<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
export type NestedTwoLayers<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? ObjectType[Key] extends Function
? never
: `${Key}` | `${Key}.${NestedThreeLayers<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
export type NestedOneLayer<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? ObjectType[Key] extends Function
? never
: `${Key}` | `${Key}.${NestedTwoLayers<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? ObjectType[Key] extends Function
? never
: `${Key}` | `${Key}.${NestedOneLayer<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment