fix: create server actions
This commit is contained in:
parent
c7dfbee0e0
commit
0bb8eac362
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
|
@ -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 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
revalidatePath("/story")
|
||||||
|
return res
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error("database failure")
|
console.error(error)
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
revalidatePath("/story")
|
|
||||||
redirect("/story")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function createPub(data) {
|
export async function createPub(data: Pub & { genres: number[] }): Promise<Pub | undefined> {
|
||||||
"use server"
|
"use server"
|
||||||
|
//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 } })
|
const genresArray = data.genres.map(e => { return { id: e } })
|
||||||
const res = await prisma.pub.create({
|
|
||||||
data: {
|
//prepare schemas
|
||||||
title: data.title,
|
const schema = pubSchema.omit({ genres: true })
|
||||||
link: data.link,
|
const genreSchema = z.object({ id: z.number() })
|
||||||
query_after_days: data.query_after_days
|
const genresSchema = z.array(genreSchema)
|
||||||
}
|
|
||||||
})
|
try {
|
||||||
console.log(res)
|
|
||||||
const genresRes = await prisma.pub.update({
|
//validate
|
||||||
where: { id: res.id },
|
schema.parse(pubData)
|
||||||
data:
|
genresSchema.safeParse(genresArray)
|
||||||
{ genres: { set: genresArray } }
|
|
||||||
})
|
//submit
|
||||||
console.log(genresRes)
|
const res = await prisma.pub.create({
|
||||||
revalidatePath("/publication")
|
data: pubData
|
||||||
redirect("/publication")
|
})
|
||||||
|
const genresRes = await prisma.pub.update({
|
||||||
|
where: { id: res.id },
|
||||||
|
data:
|
||||||
|
{ genres: { set: genresArray } }
|
||||||
|
})
|
||||||
|
revalidatePath("/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"
|
||||||
const res = await prisma.sub.create({ data })
|
try {
|
||||||
console.log(res)
|
subSchema.parse(data)
|
||||||
revalidatePath("/submission")
|
const res = await prisma.sub.create({ data })
|
||||||
redirect("/submission")
|
revalidatePath("/submission")
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
|
@ -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", {
|
||||||
|
|
|
@ -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)
|
||||||
toast({
|
if (res === undefined) throw new Error("something went wrong")
|
||||||
title: "You submitted the following values:",
|
toast({
|
||||||
description: (
|
title: "Successfuly submitted:",
|
||||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
description: values.title
|
||||||
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
|
})
|
||||||
</pre>
|
} catch (error) {
|
||||||
),
|
toast({
|
||||||
})
|
title: "UH-OH",
|
||||||
createPub(values)
|
description: error.message
|
||||||
console.log(values)
|
})
|
||||||
|
}
|
||||||
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onErrors(errors) {
|
function onErrors(errors) {
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
toast({
|
if (res === undefined) throw new Error("something went wrong")
|
||||||
title: "You submitted the following values:",
|
toast({ title: "Successfully created new submission!" })
|
||||||
description: (
|
} catch (error) {
|
||||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
toast({
|
||||||
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
|
title: "UH-OH",
|
||||||
</pre>
|
description: error.message
|
||||||
),
|
})
|
||||||
})
|
|
||||||
if (values.id) {
|
|
||||||
editSubmission(values)
|
|
||||||
} else {
|
|
||||||
createSub(values)
|
|
||||||
}
|
}
|
||||||
console.log(values)
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onErrors(errors) {
|
function onErrors(errors) {
|
||||||
|
|
Loading…
Reference in New Issue