Blitz 尚在 beat 阶段! 🎉 预计会在今年的 Q3 季度发布 1.0
Back to Documentation Menu

入门教程

Topics

Jump to a Topic

在本教程中,我们将会指导你创建一个简易的投票系统。

我们将假设你已经 安装了 Blitz。你可以通过在终端运行以下命 令来确定 Blitz 是否已经安装或检查安装的版本号:

blitz -v

如果 Blitz 已经安装成功,你应该能看到安装的版本号。否则你会收到一条像这样 的错误提示:“command not found: blitz”。

创建一个新应用

在命令行中,cd 进入你想要创建应用的根目录文件夹后,执行以下命令:

blitz new my-blitz-app

Blitz 会在你当前的文件夹中创建一个 my-blitz-app 文件夹。你接着会收到一个 选择表单库的提示。本教程中将选择其中推荐的 React Final Form 库。

让我们看看 blitz new 命令创建了什么:

my-blitz-app
├── app/
│   ├── api/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── SignupForm.tsx
│   │   ├── mutations/
│   │   │   ├── changePassword.ts
│   │   │   ├── forgotPassword.test.ts
│   │   │   ├── forgotPassword.ts
│   │   │   ├── login.ts
│   │   │   ├── logout.ts
│   │   │   ├── resetPassword.test.ts
│   │   │   ├── resetPassword.ts
│   │   │   └── signup.ts
│   │   ├── pages/
│   │   │   ├── forgot-password.tsx
│   │   │   ├── login.tsx
│   │   │   ├── reset-password.tsx
│   │   │   └── signup.tsx
│   │   └── validations.ts
│   ├── core/
│   │   ├── components/
│   │   │   ├── Form.tsx
│   │   │   └── LabeledTextField.tsx
│   │   ├── hooks/
│   │   │   └── useCurrentUser.ts
│   │   └── layouts/
│   │       └── Layout.tsx
│   ├── pages/
│   │   ├── 404.tsx
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── index.test.tsx
│   │   └── index.tsx
│   └── users/
│       └── queries/
│           └── getCurrentUser.ts
├── db/
│   ├── index.ts
│   ├── schema.prisma
│   └── seeds.ts
├── integrations/
├── mailers/
│   └── forgotPasswordMailer.ts
├── public/
│   ├── favicon.ico*
│   └── logo.png
├── test/
│   ├── setup.ts
│   └── utils.tsx
├── README.md
├── babel.config.js
├── blitz.config.js
├── jest.config.js
├── package.json
├── tsconfig.json
├── types.d.ts
├── types.ts
└── yarn.lock

上述文件有:

  • app/ 文件夹是项目中绝大多数功能的容器。你可以在这里放置任何页面或 API 路由。

  • app/pages/ 文件夹是主要的页面文件夹。如果你使用过 Next.js 你将会立即注 意到两者的不同。在 Blitz 中,你可以有许多 pages 文件夹,它们将在构建时 合并在一起。

  • app/core/ 文件夹是放置整个应用中使用到的通用组件、Hooks 等的主要位置。

  • db/ 是数据库配置所在的位置。如果你正在编写模型或检查迁移情况,可以来这 里。

  • public/ 文件夹可以让你放置任何静态资源。如果你有要在应用中使用的图像、 文件或视频等,都可以放置在其中。

  • .babelrc.js.env 等(“dotfiles 文件”)是各种 JavaScript 工具需要用 到的配置文件。

  • blitz.config.js 用于 Blitz 的高级自定义配置,与 next.config.js 相同

  • tsconfig.json 是我们推荐的 TypeScript 设置。

开发环境服务器

现在,如果你还没有在 my-blitz-app 文件夹下,确保切换到其中,并运行以下命 令:

blitz dev

你将会在命令行中看到如下输出:

✔ Compiled
Loaded env from /private/tmp/my-blitz-app/.env
warn  - You have enabled experimental feature(s).
warn  - Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use them at your own risk.

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info  - Using external babel configuration from /my-blitz-app/babel.config.js
event - compiled successfully

现在服务器已成功运行,浏览器中访问 localhost:3000。你将会看到带有 Blitz logo 的欢迎 页面。成功了!

以用户身份注册

Bliz 应用让用户登录和注册开箱即用!现在让我们来尝试一下。点击 注册 按 钮,输入任何电子邮件和密码,然后单击 创建账户 后,你将被重定向到用户主 页,并在其中可以看到你的用户 idrole

如果需要,你也可以尝试注销并重新登录。或在登录页面上单击 忘记密码,以 尝试重置密码流程。

编写你的第一个页面

接下来让我们创建你的第一个页面。

打开 app/pages/index.tsx 文件然后替换掉 Home 组件的所有内容为这段代码 :

//...

const Home: BlitzPage = () => {
  return (
    <div>
      <h1>Hello, world!</h1>

      <Suspense fallback="Loading...">
        <UserInfo />
      </Suspense>
    </div>
  )
}

//...

保存文件后你将会看到浏览器页面进行了更新。你可以如你所愿地添加需要的各种自 定义项, 。准备就绪后,请转到下一节。

数据库配置

好消息是,已经为你建立好了 SQLite 数据库!你可以在终端中运行 blitz prisma studio 来打开一个可以查看数据库数据的 Web 界面。

请注意,在开始第一个实际项目时,你可能希望使用可扩展性更高的数据库(如 PostgreSQL),以避免在将来切换数据库时产生的麻烦。有关更多信息,请参见 数据库概述 篇。目前,我们将继续使用默认的 SQLite 数据 库。

模型的脚手架代码

Blitz 提供了一个方便的 CLI 命令 generate 来构建样板代 码。我们将使用 generate 来创建两个模型:Question(问题) 和 Choice( 选择)。Question 包含问题内容和选择列表。Choice 包含选择内容、投票计数 以及相关的问题。Blitz 将为这两个模型自动生成一个 id、一个创建时间戳以及一 个最新更新的时间戳。

首先,我们将生成与 Question 模型有关的所有信息:

blitz generate all question text:string

当出现提示框时,按 Enter 以运行 prisma migrate,这将使用新的模型来更 新你的数据库架构。此时会要求一个名称,可以输入“add question”之类的值。

CREATE    app/pages/questions/[questionId].tsx
CREATE    app/pages/questions/[questionId]/edit.tsx
CREATE    app/pages/questions/index.tsx
CREATE    app/pages/questions/new.tsx
CREATE    app/questions/components/QuestionForm.tsx
CREATE    app/questions/queries/getQuestion.ts
CREATE    app/questions/queries/getQuestions.ts
CREATE    app/questions/mutations/createQuestion.ts
CREATE    app/questions/mutations/deleteQuestion.ts
CREATE    app/questions/mutations/updateQuestion.ts

✔ Model for 'question' created in schema.prisma:

> model Question {
>   id        Int      @default(autoincrement()) @id
>   createdAt DateTime @default(now())
>   updatedAt DateTime @updatedAt
>   text      String
> }

? Run 'prisma migrate dev' to update your database? (Y/n)true
Environment variables loaded from .env
Prisma schema loaded from db/schema.prisma
Datasource "db": SQLite database "db.sqlite" at "file:./db.sqlite"

✔ Name of migration … add question
The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20210217035805_add_question/
    └─ migration.sql

✔ Generated Prisma Client (2.17.0) to ./node_modules/@prisma/client in 103ms

Everything is now in sync.

generate 命令搭配 all 类型将生成相关的模型、queries、mutation 和页面文 件。请参见 Blitz generate 页面查询更多可用的类型选项。

接下来,我们将生成带有相应 queries 和 mutations 的 Choice 模型。

这次我们搭配 resource 类型,因为我们不需要为 Choice 模型生成页面:

blitz generate resource choice text votes:int:default=0 belongsTo:question

如果你在执行 blitz prisma format 时遇到错误,请注意这里不需要进行数据库 迁移,因为我们还没添加 Choice 字段到 Question 模型上。 因此我们需要在 提示是否执行迁移时选择 false

CREATE    app/choices/queries/getChoice.ts
CREATE    app/choices/queries/getChoices.ts
CREATE    app/choices/mutations/createChoice.ts
CREATE    app/choices/mutations/deleteChoice.ts
CREATE    app/choices/mutations/updateChoice.ts

✔ Model for 'choice' created in schema.prisma:

> model Choice {
>   id         Int      @default(autoincrement()) @id
>   createdAt  DateTime @default(now())
>   updatedAt  DateTime @updatedAt
>   text       String
>   votes      Int      @default(0)
>   question   Question @relation(fields: [questionId], references: [id])
>   questionId Int
> }

? Run 'prisma migrate dev' to update your database? (Y/n)false

最后,让我们更新 Question 模型以关联到 Choice 上。

打开 db/schema.prisma 并在 Question 模型中添加 choices Choice[]

model Question {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  text      String
+ choices   Choice[]
}

现在我们可以执行迁移来更新我们的数据库:

blitz prisma migrate dev

同是输入迁移的名称,比如"add choice":

Environment variables loaded from .env
Prisma schema loaded from db/schema.prisma
Datasource "db": SQLite database "db.sqlite" at "file:./db.sqlite"

✔ Name of migration … add choice
The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20210412175528_add_choice/
    └─ migration.sql

Your database is now in sync with your schema.

现在我们的数据库已经准备就绪,并且 Prisma 客户端也生成完毕。让我们继续入手 挑战 Prisma 客户端!

访问 Prisma 数据库客户端

现在,让我们跳进 Blitz 交互式 Shell 中,并使用 Blitz 为你提供的 Primsa 数 据库客户端。要启动 Blitz 控制台,请使用以下命令:

blitz console

一旦你进入控制台后,浏览数据库客户端:

# No questions are in the system yet.> await db.question.findMany()
[]

# Create a new Question:> let q = await db.question.create({data: {text: "What's new?"}})
undefined

# See the entire object:> q
{
  id: 1,
  createdAt: 2020-06-15T15:06:14.959Z,
  updatedAt: 2020-06-15T15:06:14.959Z,
  text: "What's new?"
}

# Or, access individual values on the object:> q.text
"What's new?"

# Change values by using the update function:> q = await db.question.update({where: {id: 1}, data: {text: "What's up?"}})
{
  id: 1,
  createdAt: 2020-06-15T15:06:14.959Z,
  updatedAt: 2020-06-15T15:13:17.394Z,
  text: "What's up?"
}

# db.question.findMany() now displays all the questions in the database:> await db.question.findMany()
[
  {
    id: 1,
    createdAt: 2020-06-15T15:06:14.959Z,
    updatedAt: 2020-06-15T15:13:17.394Z,
    text: "What's up?"
  }
]

更新模型属性生成的代码

Info

在再次运行该应用之前,我们需要自定义一些已生成的代码。 最终这些修复过程将 不再需要——但就目前而言,我们需要解决尚未解决的问题。

自动生成的页面,当前并未使用你在生成过程中定义的实际模型的属性。以后会支持 ,但现在,需要我们手动修复生成的页面。

Question 页面

进入 app/pages/questions/index.tsx. 请注意到一个 QuestionsList 组件已 经为你生成了:

// app/pages/questions/index.tsx

export const QuestionsList = () => {
  const router = useRouter()
  const page = Number(router.query.page) || 0
  const [{ questions, hasMore }, { isPreviousData }] = usePaginatedQuery(
    getQuestions,
    {
      orderBy: { id: "asc" },
      skip: ITEMS_PER_PAGE * page,
      take: ITEMS_PER_PAGE,
    }
  )

  const goToPreviousPage = () =>
    router.push({ query: { page: page - 1 } })

  const goToNextPage = () => {
    if (!isPreviousData && hasMore) {
      router.push({ query: { page: page + 1 } })
    }
  }

  return (
    <div>
      <ul>
        {questions.map((question) => (
          <li key={question.id}>
            <Link href={`/questions/${question.id}`}>
              <a>{question.name}</a>
            </Link>
          </li>
        ))}
      </ul>

      <button disabled={page === 0} onClick={goToPreviousPage}>
        Previous
      </button>
      <button
        disabled={isPreviousData || !hasMore}
        onClick={goToNextPage}
      >
        Next
      </button>
    </div>
  )
}

不过目前跑不通的!请记住我们创建的 Question 模型上面没有任何“name”字段。 要解决此问题,请替换 question.namequestion.text

// app/pages/questions/index.tsx

const QuestionsList = () => {
  const router = useRouter()
  const page = Number(router.query.page) || 0
  const [{questions, hasMore}, {isPreviousData}] = usePaginatedQuery(
    getQuestions, {
      orderBy: {id: "asc"},
      skip: ITEMS_PER_PAGE * page,
      take: ITEMS_PER_PAGE,
    },
  )

  const goToPreviousPage = () => router.push({query: {page: page - 1}})

  const goToNextPage = () => {
    if (!isPreviousData && hasMore) {
      router.push({query: {page: page + 1}})
    }
  }

  return (
    <div>
      <ul>
        {questions.map((question) => (
          <li key={question.id}>
            <Link href={`/questions/${question.id}`}>
-              <a>{question.name}</a>
+              <a>{question.text}</a>
            </Link>
          </li>
        ))}
      </ul>

      <button disabled={page === 0} onClick={goToPreviousPage}>
        Previous
      </button>
      <button disabled={isPreviousData || !hasMore} onClick={goToNextPage}>
        Next
      </button>
    </div>
  )
}

接下来,我们将类似的修复方法应用于 app/questions/components/QuestionForm.tsx 中。在表单提交中,将 LabeledTextFieldname 变为 text

export function QuestionForm<S extends z.ZodType<any, any>>(
  props: FormProps<S>,
) {
  return (
    <Form<S> {...props}>
-     <LabeledTextField name="name" label="Name" placeholder="Name" />
+     <LabeledTextField name="text" label="Text" placeholder="Text" />
    </Form>
  )
}

createQuestion mutation

app/questions/mutations/createQuestion.ts 中,我们需要更新 CreateQuestion zod 验证模式,使用 text 而不是 name

// app/questions/mutations/createQuestion.ts

const CreateQuestion = z
  .object({
-   name: z.string(),
+   text: z.string(),
  })
  .nonstrict()
// ...

updateQuestion mutation

app/questions/mutations/updateQuestion.ts 中,我们需要更新 UpdateQuestion zod 验证模式,使用 text 而不是 name

// app/questions/mutations/updateQuestion.ts

const UpdateQuestion = z
  .object({
    id: z.number(),
-   name: z.string(),
+   text: z.string(),
  })
  .nonstrict()
// ...

deleteQuestion mutation

Prisma 尚不支持“级联删除”。在本教程的上下文中,这意味着它在删除 Question 时不会删除相关的 Choice数据。我们需要临时改动生成的 deleteQuestion mutation,以便手动做这件事。在文本编辑框中打开 app/questions/mutations/deleteQuestion.ts 并将以下内容添加到函数主体的顶 部。

await db.choice.deleteMany({ where: { questionId: id } })

最终的效果应该为:

// app/questions/mutations/deleteQuestion.ts

export default resolver.pipe(
  resolver.zod(DeleteQuestion),
  resolver.authorize(),
  async ({id}) => {
+   await db.choice.deleteMany({where: {questionId: id}})
    const question = await db.question.deleteMany({where: {id}})

    return question
  },
)

现在,此 mutation 将在删除问题本身之前,删除与问题相关的选择。

现在尝试创建、更新和删除问题

太棒了!现在确保你的程序正常运行。否则在你的终端中执行 blitz dev,然后访 问 localhost:3000/questions。尝试创建问题并编辑、删除它们。

在问题表格中添加选择项

到目前为止,你做的很棒!我们要做的下一件事是在我们的问题中添加选择。在你的 编辑器中打开 app/questions/components/QuestionForm.tsx

添加另外三个 <LabeledTextField> 组件作为选择。

export function QuestionForm<S extends z.ZodType<any, any>>(
  props: FormProps<S>,
) {
  return (
    <Form<S> {...props}>
      <LabeledTextField name="text" label="Text" placeholder="Text" />
+     <LabeledTextField name="choices.0.text" label="Choice 1" />
+     <LabeledTextField name="choices.1.text" label="Choice 2" />
+     <LabeledTextField name="choices.2.text" label="Choice 3" />
    </Form>
  )
}

现在打开 app/pages/questions/new.tsx 并设置 initialValues 为如下:

      <QuestionForm
        submitText="Create Question"
-       // initialValues={{ }}
+       initialValues={{choices: []}}
        onSubmit={async (values) => {
          try {
            const question = await createQuestionMutation(values)
            router.push(`/questions/${question.id}`)
          } catch (error) {
            console.error(error)
            return {
              [FORM_ERROR]: error.toString(),
            }
          }
        }}
      />

接下来打开 app/questions/mutations/createQuestion.ts 并更新 zod 模式,来 让 mutation 接收 choice 数据。而且我们还需要更新 db.question.create 调用 ,以便 choice 也可以被创建。

// app/questions/mutations/createQuestion.ts

const CreateQuestion = z
  .object({
    text: z.string(),
+   choices: z.array(z.object({text: z.string()})),
  })
  .nonstrict()

export default resolver.pipe(
  resolver.zod(CreateQuestion),
  resolver.authorize(),
  async (input) => {
-   const question = await db.question.create({data: input})
+   const question = await db.question.create({
+     data: {
+       ...input,
+       choices: {create: input.choices},
+     },
+   })

    return question
  },
)

试试看

现在你可以转到 localhost:3000/questions/new 并创建一个带有选择的新问题!

列出选择

该轻松一下了。返回浏览器中的 localhost:3000/questions 并查看你创建的所有 问题。让我们在这些问题下列出相关的选择如何?首先,我们需要自定义问题查询函 数。在 Prisma 中,你需要手动让客户端知道你需要查询的嵌套关系,将你的 getQuestion.tsgetQuestions.ts 文件更改为如下所示:

// app/questions/queries/getQuestion.ts

const GetQuestion = z.object({
  // This accepts type of undefined, but is required at runtime
  id: z.number().optional().refine(Boolean, "Required"),
})

export default resolver.pipe(
  resolver.zod(GetQuestion),
  resolver.authorize(),
  async ({id}) => {
-   const question = await db.question.findFirst({where: {id}})
+   const question = await db.question.findFirst({
+     where: {id},
+     include: {choices: true},
+   })

    if (!question) throw new NotFoundError()

    return question
  },
)
// app/questions/queries/getQuestions.ts

interface GetQuestionsInput
  extends Pick<
    Prisma.QuestionFindManyArgs,
    "where" | "orderBy" | "skip" | "take"
  > {}

export default resolver.pipe(
  resolver.authorize(),
  async ({where, orderBy, skip = 0, take = 100}: GetQuestionsInput) => {
    const {items: questions, hasMore, nextPage, count} = await paginate({
      skip,
      take,
      count: () => db.question.count({where}),
      query: (paginateArgs) =>
        db.question.findMany({
          ...paginateArgs,
          where,
          orderBy,
+         include: {choices: true},
        }),
    })

    return {
      questions,
      nextPage,
      hasMore,
      count,
    }
  },
)

现在在浏览器中跳回我们主要的 Question 页面 (app/pages/questions/index.tsx),我们可以列出每个问题的选择。并将此代码 添加到我们 QuestionsList 中的 Link 下:

// app/pages/questions/index.tsx

// ...
{
  questions.map((question) => (
    <li key={question.id}>
      <Link href={`/questions/${question.id}`}>
        <a>{question.text}</a>
      </Link>
+     <ul>
+       {question.choices.map((choice) => (
+         <li key={choice.id}>
+           {choice.text} - {choice.votes} votes
+         </li>
+       ))}
+     </ul>
    </li>
  ))
}
// ...

现在在浏览器中访问 /questions 路由。神奇吧!

让我们允许用户对这些问题投票!

在浏览器中打开 app/pages/questions/[questionId].tsx。首先,我们将对该页 面进行一些改造。

  1. 替换 <h1>Question {question.id}</h1><h1>{question.text}</h1>.

  2. 删除 pre 元素,并将如下复制到之前写的选择列表中:

<ul>
  {question.choices.map((choice) => (
    <li key={choice.id}>
      {choice.text} - {choice.votes} votes
    </li>
  ))}
</ul>

如果返回浏览器,页面目前看起来像这样!

Screenshot

现在是时候来增加投票功能!

首先我们需要打开 app/choices/mutations/updateChoice.ts,更新 zod 模式, 添加新增投票功能。

const UpdateChoice = z
  .object({
    id: z.number(),
-   name: z.string(),
  })
  .nonstrict()

export default resolver.pipe(
  resolver.zod(UpdateChoice),
  resolver.authorize(),
  async ({id, ...data}) => {
-   const choice = await db.choice.update({where: {id}, data})
+   const choice = await db.choice.update({
+     where: {id},
+     data: {votes: {increment: 1}},
+   })

    return choice
  },
)

返回到 app/pages/questions/[questionId].tsx 中进行如下更改:

在我们的 li 中,新增一个如下的 button

<li key={choice.id}>
  {choice.text} - {choice.votes} votes
  <button>Vote</button>
</li>

接下来,导入我们更新的 updateChoice mutation,并在页面中创建 handleVote 函数。

// app/pages/questions/[questionId].tsx
+import updateChoice from "app/choices/mutations/updateChoice"

//...

const Question = () => {
  const router = useRouter()
  const questionId = useParam("questionId", "number")
  const [deleteQuestionMutation] = useMutation(deleteQuestion)
  const [question] = useQuery(getQuestion, {id: questionId})
+ const [updateChoiceMutation] = useMutation(updateChoice)
+
+ const handleVote = async (id: number) => {
+   try {
+     await updateChoiceMutation({id})
+     refetch()
+   } catch (error) {
+     alert("Error updating choice " + JSON.stringify(error, null, 2))
+   }
+ }

  return (

然后我们需要更新问题相关的 useQuery 调用以返回需要在 handleVote 内部使 用的 refetch 函数。

// app/pages/questions/[questionId].tsx

//...
- const [question] = useQuery(getQuestion, {id: questionId})
+ const [question, {refetch}] = useQuery(getQuestion, {id: questionId})
//...

最后,我们将告诉新的 button 来条用该函数!

<button onClick={() => handleVote(choice.id)}>Vote</button>

最终的 Question 组件应该是这个样子:

export const Question = () => {
  const router = useRouter()
  const questionId = useParam("questionId", "number")
  const [deleteQuestionMutation] = useMutation(deleteQuestion)
  const [question, { refetch }] = useQuery(getQuestion, {
    id: questionId,
  })
  const [updateChoiceMutation] = useMutation(updateChoice)

  const handleVote = async (id: number) => {
    try {
      await updateChoiceMutation({ id })
      refetch()
    } catch (error) {
      alert("Error updating choice " + JSON.stringify(error, null, 2))
    }
  }

  return (
    <>
      <Head>
        <title>Question {question.id}</title>
      </Head>

      <div>
        <h1>{question.text}</h1>
        <ul>
          {question.choices.map((choice) => (
            <li key={choice.id}>
              {choice.text} - {choice.votes} votes
              <button onClick={() => handleVote(choice.id)}>Vote</button>
            </li>
          ))}
        </ul>

        <Link href={`/questions/${question.id}/edit`}>
          <a>Edit</a>
        </Link>

        <button
          type="button"
          onClick={async () => {
            if (window.confirm("This will be deleted")) {
              await deleteQuestionMutation({ id: question.id })
              router.push("/questions")
            }
          }}
          style={{ marginLeft: "0.5rem" }}
        >
          Delete
        </button>
      </div>
    </>
  )
}

最后,让我们支持编辑某问题下的一个选择

如果单击现有问题之一上的“编辑”按钮,你将看到它使用与创建问题相同的形式。至 此,该部分已经完成!我们只需要更新我们的 mutation。

打开 app/questions/mutations/updateQuestion.ts 并进行如下改动:

// app/questions/mutations/updateQuestion.ts
import {resolver} from "blitz"
import db from "db"
import * as z from "zod"

const UpdateQuestion = z
  .object({
    id: z.number(),
    text: z.string(),
+   choices: z.array(
+     z.object({id: z.number().optional(), text: z.string()}),
+   ),
  })
  .nonstrict()

export default resolver.pipe(
  resolver.zod(UpdateQuestion),
  resolver.authorize(),
  async ({id, ...data}) => {
-   const question = await db.question.update({where: {id}, data})
+   const question = await db.question.update({
+     where: {id},
+     data: {
+       ...data,
+       choices: {
+         upsert: data.choices.map((choice) => ({
+           // Appears to be a prisma bug,
+           // because `|| 0` shouldn't be needed
+           where: {id: choice.id || 0},
+           create: {text: choice.text},
+           update: {text: choice.text},
+         })),
+       },
+     },
+     include: {
+       choices: true,
+     },
+   })

    return question
  },
)

upsert 是一种特殊的操作,表示“如果存在此项目,请对其进行更新。否则创建它”。这对于 当前情况是完美的,因为我们不需要用户在创建问题时同时添加三个选择。所以如果 用户通过编辑问题添加另一个选择,则将在此处创建它。

清理工作

为了让 yarn tscgit push 能成功,你需要删除 app/choices/mutations/createChoice.ts (没有用到)或更新 CreateChoice zod schema 来包含必须的字段。

结尾

🥳 恭喜!你创建了自己的 Blitz 应用!祝你玩得开心,也欢迎与你的朋友分享。现 在,你已经完成了本教程,为什么不尝试使你的投票应用变得更好呢?你可以尝试:

  • 添加样式 (提示, 试试 blitz install tailwindblitz install chakra-ui)
  • 显示更多有关选票的统计信息
  • RenderVercel 上实时部署。

如果你想与全球 Blitz 社区分享你的项目,没有比 Discord 更好的地方了。

访问 discord.blitzjs.com。然后,将连接发布 到 #built-with-blitz 频道来与所有人共享!


Idea for improving this page? Edit it on GitHub.