ブリッツ.JS : Fullstack反応フレームワークパート2


歓迎する👋


おい、開発者、シリーズの第2の部分へようこそ.

Check part one if you haven't already:


前の部分では、我々は新鮮な電撃を設定完了しました.JSプロジェクトは、レシピを使用してTarwind CSSを追加し、データベースモデルを作成し、このプロジェクトに必要なファイルを生成しました.
今日、スキーマファイルを更新することから始めましょう.
では、始めましょう.

インデックス

  • Update Database Schema

  • Understanding and updating Logics
  • Logics for Project
  • Logics for Tasks

  • Building UI
  • SignUp Page Component
  • Login Page
  • Forgot Password page
  • Reset Password page
  • Recap
  • データベーススキーマの更新


    前の記事では、プロジェクトとタスクテーブルの関係を作成しましたが、タスク名とタスクの記述を格納するフィールドを作成していません.まず最初に、スキームを更新しましょう.必須のフィールドを持つPRIMAファイル.
    // file: db/schema.prisma
    ...
    
    model Project {
      id          Int      @id @default(autoincrement())
      createdAt   DateTime @default(now())
      updatedAt   DateTime @updatedAt
      name        String
      description String
      tasks       Task[]
    }
    
    model Task {
      id          Int      @id @default(autoincrement())
      createdAt   DateTime @default(now())
      updatedAt   DateTime @updatedAt
      project     Project  @relation(fields: [projectId], references: [id])
      projectId   Int
      // Add the following new fields
      name        String
      description String?
    }
    
    
    あなたが我々が変わったものに気がつかなかったならば、チェックしてくださいTask 我々が加えたモデルname フィールドオブString 種類description of String ヌルブルで? .
    それはschema .
    コマンドを実行するblitz prisma migrate dev . そして、マイグレーションのためにどんな名前も与えます、しかし、我々が2つの新しいフィールドを加えることによって、我々はタスクテーブルを更新したので、私はそれに名前をつけますupdate_tasks_table . あなたがPrisma Studioを開くならばblitz prisma studio , タスクテーブルに2つの新しいフィールドが表示されます.
    論理を作りましょう.

    論理の理解と更新


    私たちは、突然変異とクエリを理解して、データベース内のデータを変更し、以前の部分からコード足場によって生成されたデータベースからデータを取得しますが、新しいフィールドを追加したので、突然変異や論理を更新しなければなりません.

    プロジェクトの論理


    まず、プロジェクトのCRUD操作を作成しましょう.
    オープンapp/projects/mutations/createProject.ts を追加します.
    // app/projects/mutations/createProject.ts
    import { resolver } from "blitz"
    import db from "db"
    import { z } from "zod"
    
    const CreateProject = z.object({
      name: z.string(),
      description: z.string(),
    })
    
    export default resolver.pipe(
      resolver.zod(CreateProject), // This is a handly utility for using Zod, an awesome input validation library. It takes a zod schema and runs schema.parse on the input data.
      resolver.authorize(), // Require Authentication
      async (input) => {
        // Create the project
        const project = await db.project.create({ data: input })
        // Return created project
        return project
      }
    )
    
    コードを分割し、各ラインを理解しましょう.
  • import { resolver } from "blitz" : ブリッツはいくつかのユーティリティを含むリゾルバオブジェクトをエクスポートします.ここで使用される「レゾルバ」とは、いくつかの入力を取り、いくつかの出力または副作用に「分解する」機能を意味する.Click here to know more
  • import db from "db" : ヒアdb によって強化されたPrismaクライアントblitz .
  • import { z } from "zod" : ZODはタイプスクリプトの最初のスキーマ宣言と検証ライブラリです.私は、単純なストリングから複雑な入れ子になったオブジェクトまでどんなデータ型でも広く参照するために「スキーマ」という用語を使用しています.Click here to know more
  • const CreateProject : CreateProject 指定した入力が含まれているかどうかを検証するオブジェクトスキーマですname フィールドオブstring 種類description ' string '型のフィールド.
  • resolver.pipe : これは複雑なリゾルバを書くことをより簡単で、よりきれいにする機能的なパイプです.パイプは、1つの関数の出力を自動的に次の関数にパイプします.( Blitz.js Docs )
  • resolver.zod(CreateProject) : これは、ZOD、素晴らしい入力検証ライブラリを使用するための便利なユーティリティです.これは、ZODスキーマと実行スキーマを取得します.入力データをパースします.( Blitz.js Docs )
  • resolver.authorize() : リゾルバの使用解雇リゾルバ.パイプは、ユーザーがクエリまたは突然変異を呼び出す権限を持っているかどうかを確認する簡単な方法です.( Blitz.js Docs )
  • async (input) => {} : このasync関数はコールバックです.
  • db.project.create : データベースに新しいプロジェクトを作成します.
  • return project : 作成したデータを返します.
  • さて、プロジェクトを作成するロジックを構築しました.
    プロジェクトを取得するロジックを構築しましょう.
    // file: app/projects/queries/getProjects.ts
    import { paginate, resolver } from "blitz"
    import db, { Prisma } from "db"
    
    interface GetProjectsInput
      extends Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}
    
    export default resolver.pipe(
      resolver.authorize(),
      async ({ where, orderBy, skip = 0, take = 100 }: GetProjectsInput) => {
        // TODO: in multi-tenant app, you must add validation to ensure correct tenant
        const {
          items: projects,
          hasMore,
          nextPage,
          count,
        } = await paginate({
          skip,
          take,
          count: () => db.project.count({ where }),
          query: (paginateArgs) =>
            db.project.findMany({ ...paginateArgs, where, orderBy, include: { tasks: true } }),
        })
    
        return {
          projects,
          nextPage,
          hasMore,
          count,
        }
      }
    )
    
    このファイルでは、1つの変更を行い、私はinclude オプションdb.project.findMany() .
    これは、それぞれのプロジェクトに属するすべてのタスクを含みます.
    さて、このコードの各ラインを理解しましょう.私は、私がすでに建設しているものを繰り返すつもりでありませんcreate project ロジック.インポートもスキップします.
  • interface GetProjectsInput
    extends Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}
    : これを行うには、プロパティのセットを選択してインターフェイスを作成しますPrisma.ProjectFindManyArgs . ( TS Docs )
  • Prisma.ProjectFindManyArgs : Prismaはモデルと引数の型を生成します.ここではPrismaによって生成されたProjectFindManallers `を使用します.
  • paginate : これは、クエリのページ付けの便利なユーティリティです.( Blitz.js Docs ).
  • db.project.count({where}) : 引数が通過した条件に続くデータベースからのデータ数を返します.( Prisma Docs )
  • db.project.findMany() : すべてのデータをプロジェクト表から取得します.これをもとに生成したものと比較すれば、私たちはinclude オプションを指定します.からこのテーブルに属するすべてのタスクを取得します.
  • では、一つのプロジェクトをどうやって入手するかを見てみましょう.`
    // app/projects/queries/getProject.ts
    import { resolver, NotFoundError } from "blitz"
    import db from "db"
    import { z } from "zod"

    const GetProject = 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(GetProject), resolver.authorize(), async ({ id }) => {
    // TODO: in multi-tenant app, you must add validation to ensure correct tenant
    const project = await db.project.findFirst({ where: { id }, include: { tasks: true } })

    if (!project) throw new NotFoundError()

    return project
    })
    `

    • .refine() : ( ZOD Docs )

    • db.project.findFirst() : 与えられた条件を満たす最初のデータを返します.( Prisma Docs )
    • throw new NotFoundError() : 404エラーをスローします.
    • さて、プロジェクトを更新するロジックを見ましょう.`
      // app/projects/mutations/updateProject.ts
      import { resolver } from "blitz"
      import db from "db"
      import { z } from "zod"

      const UpdateProject = z.object({
      id: z.number(),
      name: z.string(),
      description: z.string(),
      })

      export default resolver.pipe(
      resolver.zod(UpdateProject),
      resolver.authorize(),
      async ({ id, ...data }) => {
      // TODO: in multi-tenant app, you must add validation to ensure correct tenant
      const project = await db.project.update({ where: { id }, data })

      return project
      

      }
      )

      `

      • db.project.update() : 指定したIDを持つプロジェクト行のデータを更新します.Prisma Docs )
      • 最後に、ロジックがプロジェクトを削除するまでの時間です.`
        // app/projects/mutations/deleteProject.ts

        import { resolver } from "blitz"
        import db from "db"
        import { z } from "zod"

        const DeleteProject = z.object({
        id: z.number(),
        })

        export default resolver.pipe(resolver.zod(DeleteProject), resolver.authorize(), async ({ id }) => {
        // TODO: in multi-tenant app, you must add validation to ensure correct tenant
        const tasks = await db.task.deleteMany({ where: { projectId: id } })
        const project = await db.project.deleteMany({ where: { id } })

        return project
        })

        `
        If you look there, I have added a new line const tasks = = await db.task.deleteMany({ where: { projectId: id } }) . これは、最初にそのプロジェクトに属するすべてのタスクを削除し、その後、実際のプロジェクトが削除されました.

        • db.project.deleteMany : 指定した基準を満たすテーブルから行を削除します.
        • さて、プロジェクトのためのCRUDは完成しました、現在、それはタスクのcrud操作のための時間です.

          タスクの論理


          新しいタスクを作成するためのタスクロジックを更新しましょう.`
          // app/tasks/mutations/createTask.ts

          import { resolver } from "blitz"
          import db from "db"
          import { z } from "zod"

          const CreateTask = z.object({
          name: z.string(),
          projectId: z.number(),
          // This is what we have added
          description: z.string().optional(),
          })

          export default resolver.pipe(resolver.zod(CreateTask), resolver.authorize(), async (input) => {
          // TODO: in multi-tenant app, you must add validation to ensure correct tenant
          const task = await db.task.create({ data: input })

          return task
          })
          `

          Everything looks familiar, Nah. We have already discussed the syntax used up here before.

          After we created tasks, we need to retrieve the tasks, so let getAll the tasks.

          `
          // app/tasks/queries/getTasks.ts

          import { paginate, resolver } from "blitz"
          import db, { Prisma } from "db"

          interface GetTasksInput
          extends Pick {}

          export default resolver.pipe(
          resolver.authorize(),
          async ({ where, orderBy, skip = 0, take = 100 }: GetTasksInput) => {
          // TODO: in multi-tenant app, you must add validation to ensure correct tenant
          const {
          items: tasks,
          hasMore,
          nextPage,
          count,
          } = await paginate({
          skip,
          take,
          count: () => db.task.count({ where }),
          query: (paginateArgs) => db.task.findMany({ ...paginateArgs, where, orderBy }),
          })

          return {
            tasks,
            nextPage,
            hasMore,
            count,
          }
          

          }
          )

          `

          Everything is the same up here as generated.

          Let's see the mutation to update the task.

          `js
          // app/tasks/mutations/updateTask.ts

          import { resolver } from "blitz"
          import db from "db"
          import { z } from "zod"

          const UpdateTask = z.object({
          id: z.number(),
          name: z.string(),
          // The only thing we have added
          description: z.string().optional(),
          })

          export default resolver.pipe(
          resolver.zod(UpdateTask),
          resolver.authorize(),
          async ({ id, ...data }) => {
          // TODO: in multi-tenant app, you must add validation to ensure correct tenant
          const task = await db.task.update({ where: { id }, data })

          return task
          

          }
          )

          `

          For the getTask クエリとdelete 突然変異、そのまま残す.
          現在、我々は論理のためにされます.

          ビルのUI


          既にインストール済みです Tailwind CSS 前の部分のブリッツレシピで.( Read it here ). それで、我々はこのプロジェクトのためにTailwind CSSライブラリを使用しています.そして、私たちはTailWindCSSを使用して簡単なUIを作成します.

          サインアップページコンポーネント


          リンク/signupオープンapp/auth/pages/signup.tsx . カスタムコンポーネントを使用していることがわかりますSignupForm フォームに.だから、それを開きますapp/auth/components/SignupForm.tsx . それから、あなたは彼らがカスタムを使っているのを見るでしょうForm Component and LabeledTextField コンポーネント.
          だから我々の最初の仕事をカスタマイズすることですForm and LabeledTextFieldComponent .
          オープンapp/core/Form.tsx 追加p-5 border rounded クラスでform タグと追加text-sm クラスalert . `jsx
          // app/core/components/Form.tsx

          {submitError && (

          {submitError}

          )}
          ...

          ...
          `

          Now, let's customize LabeledTextFieldComponent .
          これについては、まず最初に、スタイル風クラスを持つ入力のカスタムコンポーネントを作成します.
          移動するapp/core/components ファイルを開くLabeledTextField.tsx そして、次のコードでそれを更新します.`jsx
          // app/core/components/LabeledTextField.tsx

          import { forwardRef, PropsWithoutRef } from "react"
          import { useField } from "react-final-form"

          export interface LabeledTextFieldProps extends PropsWithoutRef {
          /** Field name. /
          name: string
          /
          * Field label. /
          label: string
          /
          * Field type. Doesn't include radio buttons and checkboxes */
          type?: "text" | "password" | "email" | "number"
          outerProps?: PropsWithoutRef
          }

          export const LabeledTextField = forwardRef(
          ({ name, label, outerProps, ...props }, ref) => {
          const {
          input,
          meta: { touched, error, submitError, submitting },
          } = useField(name, {
          parse: props.type === "number" ? Number : undefined,
          })

          const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
          
          return (
            <div {...outerProps}>
              <label className="flex flex-col items-start">
                {label}
                <input
                  {...input}
                  className="px-1 py-2 border rounded focus:ring focus:outline-none ring-purple-200 block w-full my-2"
                  disabled={submitting}
                  {...props}
                  ref={ref}
                />
              </label>
          
              {touched && normalizedError && (
                <div role="alert" className="text-sm" style={{ color: "red" }}>
                  {normalizedError}
                </div>
              )}
            </div>
          )
          

          }
          )

          export default LabeledTextField

          `

          Always remember that the components that are required for a specific model, we have to create that inside the components folder in that model, for example. if we want a form to create a project then we add that form component inside app/project/components. But if that component is not model specific, then we create those components inside app/core/components.

          Let's create a new core Button コンポーネントは、サイトの至る所で使用する.`jsx
          // app/core/components/Button.tsx

          export const Button = ({ children, ...props }) => {
          return (

          {children}

          )
          }
          `
          Now let's use this new Button コンポーネントForm.tsx .

          In app/core/components/Form.tsx 置換
          {submitText && (
          <button type="submit" disabled={submitting}>
          {submitText}
          </button>
          )}

          with

          {submitText && (
          <Button type="submit" disabled={submitting}>
          {submitText}
          </Button>
          )}

          And don't forget to import the Button .
          import { Button } from "./Button"

          Now, you should have something like this.

          Let's customize this page more.

          We'll use a separate layout for the authentication pages. So, go to app/core/layouts という名前の新しいファイルを作成しますAuthLayout.tsx 次のコンテンツを追加します.`
          // app/core/layouts/AuthLayout.tsx

          import { ReactNode } from "react"
          import { Head } from "blitz"

          type LayoutProps = {
          title?: string
          heading: string
          children: ReactNode
          }

          const AuthLayout = ({ title, heading, children }: LayoutProps) => {
          return (
          <>


          {title || "ProjectManagement"}

            <div className="flex justify-center">
              <div className="w-full md:w-2/3 lg:max-w-2xl mt-5">
                <h2 className="text-xl mb-2">{heading}</h2>
                <div>{children}</div>
              </div>
            </div>
          </>
          

          )
          }

          export default AuthLayout

          `

          Now go to the SignupForm コンポーネントの削除h1 タグ.除去後
          <h1>Create an Account</h1>
          ファイルは次のようになります.`
          import { useMutation } from "blitz"
          import { LabeledTextField } from "app/core/components/LabeledTextField"
          import { Form, FORM_ERROR } from "app/core/components/Form"
          import signup from "app/auth/mutations/signup"
          import { Signup } from "app/auth/validations"

          type SignupFormProps = {
          onSuccess?: () => void
          }

          export const SignupForm = (props: SignupFormProps) => {
          const [signupMutation] = useMutation(signup)

          return (

            <Form
              submitText="Create Account"
              schema={Signup}
              initialValues={{ email: "", password: "" }}
              onSubmit={async (values) => {
                try {
                  await signupMutation(values)
                  props.onSuccess?.()
                } catch (error) {
                  if (error.code === "P2002" && error.meta?.target?.includes("email")) {
                    // This error comes from Prisma
                    return { email: "This email is already being used" }
                  } else {
                    return { [FORM_ERROR]: error.toString() }
                  }
                }
              }}
            >
              <LabeledTextField name="email" label="Email" placeholder="Email" />
              <LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
            </Form>
          </div>
          

          )
          }

          export default SignupForm
          `

          Now, we have to tell signup 使用するページAuthLayout レイアウトとして.
          そのためにはapp/auth/pages/signup.tsx そして、変更する行を変更します.
          SignupPage.getLayout = (page) => <Layout title="Sign Up">{page}</Layout>

          to

          SignupPage.getLayout = (page) => <AuthLayout heading="Create an account" title="Sign Up">{page}</AuthLayout>

          and import AuthLayout .
          import AuthLayout from "app/core/layouts/AuthLayout"

          Now, your signup page should look like this.

          ⚠️ Ignore that LastPass sign in the input field.

          Let's include a link to go to the login page in the signup page.

          For this, we'll create our own custom Link component with tailwind style.

          Go to /app/core/components と新しいファイルを作成するCustomLink.tsx を追加します.`
          // app/core/components/CustomLink.tsx

          import { Link } from "blitz"

          export const CustomLink = ({ children, href }: any) => {
          return (

          {children}

          )
          }
          `

          Now, to include the go-to login link you have to add the following line after the Form タグ.`
          ...

           Already have account? Login
          

          `

          After all this, your signup page should look like this.

          Now, since we have already styled many components in the SignUp UI セクションは、他のページでは、あまりにも多くの仕事をする必要はありません他のページです.

          ログインページ


          ログイン/ログイン
          ログインページのカスタマイズでは、次の行を置き換えますlogin.tsx :`
          // app/auth/pages/login

          LoginPage.getLayout = (page) => {page}
          `

          to


          LoginPage.getLayout = (page) => (
          <AuthLayout heading="Welcome back, login here" title="Log In">
          {page}
          </AuthLayout>
          )

          and import AuthLayout .
          import AuthLayout from "app/core/layouts/AuthLayout"

          After doing this, your login page should look like this.

          Now, remove <h1>Login</h1> からapp/auth/components/LoginForm.tsx .
          また、以下の行を置き換えますLoginForm.tsx :`
          // from

          Forgot your password?

          // to

          Forgot your password?

          `

          and
          `
          // from
          Sign Up

          // to
          Sign Up
          `

          After getting up to this, your login page should look like.

          パスワードのページ

          Link : '/forgot-password'

          As before, change the layout to AuthLayout .`
          // app/auth/pages/forgot-password.tsx

          import AuthLayout from "app/core/layouts/AuthLayout"
          ...

          ForgotPasswordPage.getLayout = (page) => (

          {page}

          )
          `

          and remove <h1>Forgot your password?</h1> からapp/auth/pages/forgot-password.tsx .
          今、忘れられたパスワードページが行われ、それはように見えるはずです.

          今、最終的に認証の最終ページ.

          パスワードのリセット


          パスワードをリセット
          レイアウトを変更する前にAuthLayout .`
          // app/auth/pages/reset-password.tsx

          import AuthLayout from "app/core/layouts/AuthLayout"

          ...

          ResetPasswordPage.getLayout = (page) => (

          {page}

          )
          `

          and remove <h1>Set a New Password</h1> そして、それはこのように見えるはずです.

          これは今日の人々のため.

          回収する

        • スキーマの更新
        • TruWindCSSを使用した認証ページの編集UI
        • カスタムコンポーネント
        • AuthLayoutの作成と使用