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" "use server"
import { Genre, Story } from "@prisma/client" import { Genre, Pub, Story, Sub } from "@prisma/client"
import prisma from "./db" import prisma from "./db"
import { revalidatePath } from "next/cache" import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { z } from "zod" import { z } from "zod"
import { storySchema } from "app/ui/forms/schemas" 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 //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 // will return undefined if middleware authorization fails
"use server" "use server"
//Prepare data //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 schema = storySchema.omit({ id: true, genres: true })
const genreSchema = z.object({ id: z.number() }) const genreSchema = z.object({ id: z.number() })
const genresSchema = z.array(genreSchema) 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 { try {
//validate
schema.safeParse(storyData)
genresSchema.safeParse(genresArray)
//submit
const res = await prisma.story.create({ data: storyData }) const res = await prisma.story.create({ data: storyData })
await prisma.story.update({ await prisma.story.update({
where: { id: res.id }, where: { id: res.id },
@ -33,42 +36,65 @@ export async function createStory(data: Story & { genres: number[] }): Promise<S
genres: { set: genresArray } genres: { set: genresArray }
} }
}) })
} catch (error) {
throw new Error("database failure")
}
revalidatePath("/story") 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" "use server"
const genresArray = data.genres.map(e => { return { id: e } }) //prepare data
const res = await prisma.pub.create({ const pubData = {
data: {
title: data.title, title: data.title,
link: data.link, link: data.link,
query_after_days: data.query_after_days 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({ const genresRes = await prisma.pub.update({
where: { id: res.id }, where: { id: res.id },
data: data:
{ genres: { set: genresArray } } { genres: { set: genresArray } }
}) })
console.log(genresRes)
revalidatePath("/publication") 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" "use server"
try {
subSchema.parse(data)
const res = await prisma.sub.create({ data }) const res = await prisma.sub.create({ data })
console.log(res)
revalidatePath("/submission") 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() { export default function LoginForm() {
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const redirect = searchParams.get("from") const redirect = searchParams.get("from") ?? "submission"
const form = useForm<z.infer<typeof loginSchema>>({ const form = useForm<z.infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema), resolver: zodResolver(loginSchema),
}) })

View File

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

View File

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

View File

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

View File

@ -5,3 +5,49 @@ export const storySchema = z.object({
word_count: z.number(), word_count: z.number(),
genres: z.object({ id: z.number(), name: z.string() }).array() 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:", title: "Successfully submitted:",
description: values.title, description: values.title,
}) })
//TODO refresh data without reloading page?
} catch (error) { } catch (error) {
toast({ toast({
title: "UH-OH", title: "UH-OH",
description: error.message description: error.message
}) })
console.error(error)
} }
window.location.reload()
} }

View File

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