fix: create server actions

This commit is contained in:
andrzej 2024-09-19 11:37:01 +02:00
parent c7dfbee0e0
commit 0bb8eac362
9 changed files with 146 additions and 123 deletions

Binary file not shown.

View File

@ -1,14 +1,16 @@
"use server"
import { Genre, Story } from "@prisma/client"
import { Genre, Pub, Story, Sub } from "@prisma/client"
import prisma from "./db"
import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import { z } from "zod"
import { storySchema } from "app/ui/forms/schemas"
import { pubSchema } from "app/ui/forms/schemas"
import { subSchema } from "app/ui/forms/schemas"
//TODO - data validation, error handling, unauthorized access handling
export async function createStory(data: Story & { genres: number[] }): Promise<Story> | undefined {
export async function createStory(data: Story & { genres: number[] }): Promise<Story | undefined> {
// will return undefined if middleware authorization fails
"use server"
//Prepare data
@ -21,11 +23,12 @@ export async function createStory(data: Story & { genres: number[] }): Promise<S
const schema = storySchema.omit({ id: true, genres: true })
const genreSchema = z.object({ id: z.number() })
const genresSchema = z.array(genreSchema)
//validate
const isSafe = schema.safeParse(storyData) && genresSchema.safeParse(genresArray)
if (!isSafe) throw new Error("server-side validation failed")
//submit
try {
//validate
schema.safeParse(storyData)
genresSchema.safeParse(genresArray)
//submit
const res = await prisma.story.create({ data: storyData })
await prisma.story.update({
where: { id: res.id },
@ -33,42 +36,65 @@ export async function createStory(data: Story & { genres: number[] }): Promise<S
genres: { set: genresArray }
}
})
} catch (error) {
throw new Error("database failure")
}
revalidatePath("/story")
redirect("/story")
return res
} catch (error) {
console.error(error)
return undefined
}
}
export async function createPub(data) {
export async function createPub(data: Pub & { genres: number[] }): Promise<Pub | undefined> {
"use server"
const genresArray = data.genres.map(e => { return { id: e } })
const res = await prisma.pub.create({
data: {
//prepare data
const pubData = {
title: data.title,
link: data.link,
query_after_days: data.query_after_days
}
const genresArray = data.genres.map(e => { return { id: e } })
//prepare schemas
const schema = pubSchema.omit({ genres: true })
const genreSchema = z.object({ id: z.number() })
const genresSchema = z.array(genreSchema)
try {
//validate
schema.parse(pubData)
genresSchema.safeParse(genresArray)
//submit
const res = await prisma.pub.create({
data: pubData
})
console.log(res)
const genresRes = await prisma.pub.update({
where: { id: res.id },
data:
{ genres: { set: genresArray } }
})
console.log(genresRes)
revalidatePath("/publication")
redirect("/publication")
return res
} catch (error) {
console.error(error)
return undefined
}
}
export async function createSub(data) {
export async function createSub(data: Sub): Promise<Sub | undefined> {
"use server"
try {
subSchema.parse(data)
const res = await prisma.sub.create({ data })
console.log(res)
revalidatePath("/submission")
redirect("/submission")
return res
} catch (error) {
console.error(error)
return undefined
}
}

View File

@ -17,7 +17,7 @@ import Link from "next/link";
export default function LoginForm() {
const router = useRouter()
const searchParams = useSearchParams()
const redirect = searchParams.get("from")
const redirect = searchParams.get("from") ?? "submission"
const form = useForm<z.infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema),
})

View File

@ -7,7 +7,7 @@ import { PubsWithGenres } from "./page"
import { TextInputCell } from "app/ui/tables/inputs/textInput"
import { selectCol } from "app/ui/tables/selectColumn"
import NumberInputCell from "app/ui/tables/inputs/numberInput"
import { formSchema } from "app/ui/forms/pub"
import { pubSchema } from "app/ui/forms/schemas"
import GenrePickerInputCell from "app/ui/tables/inputs/genrePickerInput"
@ -29,14 +29,14 @@ export const columns: ColumnDef<PubsWithGenres>[] = [
)
},
cell: TextInputCell,
meta: { formSchema }
meta: { formSchema: pubSchema }
},
{
accessorKey: "link",
header: "Link",
cell: TextInputCell,
meta: { formSchema }
meta: { formSchema: pubSchema }
},
columnHelper.accessor("genres", {
@ -51,7 +51,7 @@ export const columns: ColumnDef<PubsWithGenres>[] = [
cell: NumberInputCell,
meta: {
step: 10,
formSchema
formSchema: pubSchema
},
},

View File

@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"
import GenreBadges from "app/ui/genreBadges"
import { selectCol } from "app/ui/tables/selectColumn"
import NumberInputCell from "app/ui/tables/inputs/numberInput"
import { formSchema } from "app/ui/forms/story"
import { storySchema } from "app/ui/forms/schemas"
import { TextInputCell } from "app/ui/tables/inputs/textInput"
import GenrePickerInputCell from "app/ui/tables/inputs/genrePickerInput"
const columnHelper = createColumnHelper<StoryWithGenres>()
@ -27,7 +27,7 @@ export const columns: ColumnDef<StoryWithGenres>[] = [
)
},
cell: TextInputCell,
meta: { formSchema }
meta: { formSchema: storySchema }
},
{
@ -47,7 +47,7 @@ export const columns: ColumnDef<StoryWithGenres>[] = [
cell: NumberInputCell,
meta: {
step: 50,
formSchema
formSchema: storySchema
}
},
columnHelper.accessor("genres", {

View File

@ -19,17 +19,11 @@ import { randomPublicationTitle } from "app/lib/shortStoryTitleGenerator"
import { ComponentProps } from "react"
import { Genre } from "@prisma/client"
import GenrePicker from "./genrePicker"
export const formSchema = z.object({
title: z.string().min(2).max(50),
link: z.string(),
query_after_days: z.coerce.number().min(30),
genres: z.array(z.number()),
})
import { pubSchema } from "./schemas"
export default function PubForm({ genres, createPub, className }: ComponentProps<"div"> & { genres: Array<Genre>, createPub: (data: any) => void }) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
const form = useForm<z.infer<typeof pubSchema>>({
resolver: zodResolver(pubSchema),
defaultValues: {
title: "",
link: "",
@ -38,19 +32,21 @@ export default function PubForm({ genres, createPub, className }: ComponentProps
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
async function onSubmit(values: z.infer<typeof pubSchema>) {
try {
const res = await createPub(values)
if (res === undefined) throw new Error("something went wrong")
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
),
title: "Successfuly submitted:",
description: values.title
})
createPub(values)
console.log(values)
} catch (error) {
toast({
title: "UH-OH",
description: error.message
})
}
window.location.reload()
}
function onErrors(errors) {

View File

@ -5,3 +5,49 @@ export const storySchema = z.object({
word_count: z.number(),
genres: z.object({ id: z.number(), name: z.string() }).array()
})
export const pubSchema = z.object({
title: z.string().min(2).max(50),
link: z.string(),
query_after_days: z.coerce.number().min(30),
genres: z.array(z.number()),
})
export const subSchema = z.object({
id: z.number().optional(),
storyId: z.coerce.number(),
pubId: z.coerce.number(),
submitted: z.coerce.date().transform((date) => date.toString()),
responded: z.coerce.date().transform((date) => {
if (date.toString() !== new Date(null).toString()) {
return date.toString()
}
return null
}).optional(),
responseId: z.coerce.number()
})
.refine(object => {
const submitted = new Date(object.submitted)
const responded = object.responded ? new Date(object.responded) : null
return responded >= submitted || responded === null
},
{
path: ["responded"],
message: "'Responded' must be a later date than 'submitted'"
})
.refine(object => {
if (object.responded) {
//there is a 'responded' date and the response is not 'pending'
return object.responseId !== 7
}
if (!object.responded) {
//there is not a 'responded' date and the response is pending
return object.responseId === 7
}
},
{
path: ["responseId"],
message: "A pending response cannot have a date, and a non-pending response must have a date"
}
)

View File

@ -49,14 +49,13 @@ export default function StoryForm({ genres, createStory, className, existingData
title: "Successfully submitted:",
description: values.title,
})
//TODO refresh data without reloading page?
} catch (error) {
toast({
title: "UH-OH",
description: error.message
})
console.error(error)
}
window.location.reload()
}

View File

@ -32,54 +32,15 @@ import {
SelectValue,
} from "@/components/ui/select"
import { useState } from "react"
import { editSubmission } from "app/lib/edit"
import { createSub } from "app/lib/create"
import { subSchema } from "./schemas"
export const formSchema = z.object({
id: z.number().optional(),
storyId: z.coerce.number(),
pubId: z.coerce.number(),
submitted: z.coerce.date().transform((date) => date.toString()),
responded: z.coerce.date().transform((date) => {
if (date.toString() !== new Date(null).toString()) {
return date.toString()
}
return null
}).optional(),
responseId: z.coerce.number()
})
.refine(object => {
const submitted = new Date(object.submitted)
const responded = object.responded ? new Date(object.responded) : null
return responded >= submitted || responded === null
},
{
path: ["responded"],
message: "'Responded' must be a later date than 'submitted'"
})
.refine(object => {
if (object.responded) {
//there is a 'responded' date and the response is not 'pending'
return object.responseId !== 7
}
if (!object.responded) {
//there is not a 'responded' date and the response is pending
return object.responseId === 7
}
},
{
path: ["responseId"],
message: "A pending response cannot have a date, and a non-pending response must have a date"
}
)
export type SubForm = z.infer<typeof formSchema>
export type SubForm = z.infer<typeof subSchema>
export default function SubmissionForm({ stories, pubs, responses, defaults }) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
const form = useForm<z.infer<typeof subSchema>>({
resolver: zodResolver(subSchema),
defaultValues: {
responseId: responses[0].id,
...defaults
@ -106,23 +67,18 @@ export default function SubmissionForm({ stories, pubs, responses, defaults }) {
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
async function onSubmit(values: z.infer<typeof subSchema>) {
try {
const res = await createSub(values)
if (res === undefined) throw new Error("something went wrong")
toast({ title: "Successfully created new submission!" })
} catch (error) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
),
title: "UH-OH",
description: error.message
})
if (values.id) {
editSubmission(values)
} else {
createSub(values)
}
console.log(values)
window.location.reload()
}
function onErrors(errors) {