高橋  陽子

高橋 陽子

1657280057

打開選項 如何使用 Next.js、Prisma、Postgres 構建全棧應用程序

在本文中,我們將學習如何使用 Next.js、Prisma、Postgres 和 Fastify 構建全棧應用程序。我們將構建一個考勤管理演示應用程序來管理員工的考勤。該應用程序的流程很簡單:管理用戶登錄,創建當天的考勤表,然後每個員工登錄和退出考勤表。

Next.js 是什麼?

Next.js 是一個靈活的 React 框架,它為您提供構建塊來創建快速的 Web 應用程序。它通常被稱為全棧 React 框架,因為它使得前端和後端應用程序可以在同一個代碼庫上使用無服務器功能來實現。

什麼是棱鏡?

Prisma 是一個開源的 Node.js 和 Typescript ORM,它極大地簡化了 SQL 數據庫的數據建模、遷移和數據訪問。在撰寫本文時,Prisma 支持以下數據庫管理系統:PostgreSQL、MySQL、MariaDB、SQLite、AWS Aurora、Microsoft SQL Server、Azure SQL 和 MongoDB。您可能還想單擊此處查看所有受支持的數據庫管理系統的列表。

什麼是 Postgres?

Postgres 也稱為 PostgreSQL,它是一個免費和開源的關係數據庫管理系統。它是 SQL 語言的超集,它具有許多功能,允許開發人員安全地存儲和擴展複雜的數據工作負載。

先決條件

本教程是一個動手演示教程。因此,最好在您的計算機上安裝以下內容以進行後續操作:

本教程的代碼可Github 上找到,因此請隨意克隆它並繼續學習。

項目設置

讓我們從設置 Next.js 應用程序開始。要開始,請運行以下命令。

npx create-next-app@latest

等待安裝完成,然後運行下面的命令來安裝我們的依賴項。

yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev

等待安裝完成。

設置 Next.js 和 Fastify

默認情況下,Next.js 不使用 Fastify 作為其服務器。要使用 Fastify 為我們的 Next.js 應用程序提供服務,請package.json使用下面的代碼片段編輯文件中的腳本字段。

"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

創建我們的 Fastify 服務器

現在讓我們創建一個server.js文件。該文件是我們應用程序的入口點,然後我們添加require('fastify-nextjs')以包含插件,該插件在 fastify 中公開 Next.js API 以處理渲染。

打開server.js文件,並添加以下代碼片段:

const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})

在上面的代碼片段中,我們使用了fastify-nextjs在 Fastify 中公開 Next.js API 的插件,它為我們處理渲染。然後我們使用函數解析傳入的請求,使請求主體可用於我們的 Next.js API 路由處理程序,並使用命令noOpParser為我們的應用程序定義兩個路由。[fastify.next](<http://fastify.next>然後我們創建我們的 Fastify 服務器並讓它監聽 3000 端口。

現在繼續使用yarn dev命令運行應用程序:應用程序將在localhost:3000.

棱鏡設置

首先,運行以下命令以獲取基本的 Prisma 設置:

npx prisma init

上面的命令將創建一個包含schema.prisma文件的 prisma 目錄。這是您的主要 Prisma 配置文件,它將包含您的數據庫模式。此外,.env將在項目的根目錄中添加一個文件。打開.env文件並將虛擬連接 URL 替換為 PostgreSQL 數據庫的連接 URL。

將文件中的代碼替換為prisma/schema.prisma以下內容:

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(EMPLOYEE)
  attendance     Attendance[]
  AttendanceSheet AttendanceSheet[]
}

model AttendanceSheet {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  createdBy    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

model Attendance {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  signIn    Boolean @default(true)
  signOut   Boolean
  signInTime    DateTime @default(now())
  signOutTime   DateTime 
  user    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

enum Role {
  EMPLOYEE
  ADMIN
}

在上面的代碼片段中,我們創建了一個 User、AttendanceSheet 和 Attendance Model,定義了每個模型之間的關係。

接下來,在數據庫中創建這些表。運行以下命令:

npx prisma db push

運行上述命令後,您應該會在終端中看到如下屏幕截圖所示的輸出:

創建實用函數

完成 Prisma 設置後,讓我們創建三個實用程序函數,它們將不時在我們的應用程序中使用。

打開 lib/parseBody.js 文件並添加以下代碼片段。此函數將請求正文解析為 JSON:

export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}

打開 lib/request.js 文件並添加以下代碼片段。該函數發送一個 POST 請求。

export async function postData(url = '', data='') {
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(data)
});
return response.json();
}

打開 /lib/request.js 文件並添加以下代碼片段。此函數返回 Iron 會話 iron-session 的會話屬性對象。

export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}

接下來,添加SESSION_PASSWORD到 .env 文件:它應該是至少 32 個字符的字符串。

為應用程序設計樣式

完成我們的實用程序功能後,讓我們為應用程序添加一些樣式。我們正在為這個應用程序使用 css 模塊,所以打開styles/Home.modules.css文件並添加下面的代碼片段:

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.login {
  width: 450px;
}

.login input {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.login button {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.dashboard {
  display: grid;
  grid-template-columns: 3fr 9fr;
  grid-template-rows: 1fr;
  grid-column-gap: 0px;
  grid-row-gap: 0px;
  height: calc(100vh - 60px);
}

.navbar {
  height: 60px;
  background-color: black;
}

創建側邊欄組件

完成樣式後,讓我們創建側邊欄組件來幫助我們導航到應用儀表板上的不同頁面。打開 components/SideBar.js 文件,然後粘貼下面的代碼片段。

import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'

const SideBar = () => {

    const router = useRouter()

    const logout = async () => {

        try {

            const response = await fetch('/api/logout', {
                method: 'GET', 
                credentials: 'same-origin', 
            });

            if(response.status === 200)  router.push('/')

        } catch (e) {
            alert(e)
        }
  
    }
      

    return (
        <nav className={styles.sidebar}>

            <ul>

                <li> <Link href="/dashboard"> Dashboard</Link> </li>

                <li> <Link href="/dashboard/attendance"> Attendance </Link> </li>

                <li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>

                <li onClick={logout}> Logout </li>

            </ul>

        </nav>
    )

}

export default SideBar

登錄頁面

現在打開 page/index.js 文件,刪除那裡的所有代碼並添加以下代碼片段。下面的代碼將使用通過表單提供的電子郵件和密碼發送到 localhost:3000/api/login 路由的發布請求。驗證憑據後,它會調用router.push('/dashboard')將用戶重定向到 localhost:3000/api/dashboard 的方法:

import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'

export default function Home({posts}) {

  const [data, setData] = useState({email: null, password: null});

  const router = useRouter()

  const submit = (e) => {
    e.preventDefault()

    if(data.email && data.password) {
      postData('/api/login', data).then(data => {
        console.log(data); 

        if (data.status === "success") router.push('/dashboard')
      
      });
    }

  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Login</title>
        <meta name="description" content="Login" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>

        <form  className={styles.login}>

          <input 
            type={"text"} 
            placeholder="Enter Your Email" 
            onChange={(e) => setData({...data, email: e.target.value})} />

          <input 
            type={"password"}  
            placeholder="Enter Your Password"
            onChange={(e) => setData({...data, password: e.target.value})} />

          <button onClick={submit}>Login</button>

        </form>
        
      </main>

    </div>
  )
}

設置登錄 API 路由

現在打開 page/api/login.js 文件並添加以下代碼片段。我們將使用它PrismaClient來進行數據庫查詢,並且withIronSessionApiRoute是用於在 RESTful 應用程序中處理用戶會話的鐵會話功能。

此路由處理對 localhost:3000/api/login 的登錄 POST 請求,並在用戶通過身份驗證後生成身份驗證 cookie。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

export default withIronSessionApiRoute(
    async function loginRoute(req, res) {

      const { email, password } = parseBody(req.body)

      const prisma = new PrismaClient()

      // By unique identifier
      const user = await prisma.user.findUnique({
        where: {
        email
      },})

      if(user.password === password) {

        // get user from database then:
        user.password = undefined
        req.session.user = user
        await req.session.save();

        return res.send({ status: 'success', data: user });

      };

    res.send({ status: 'error', message: "incorrect email or password" });

  },
  sessionCookie(),
);

設置註銷 API 路由

打開 /page/api/logout 文件並添加下面的代碼片段。此路由處理對 localhost:3000/api/logout 的 GET 請求,該請求通過銷毀會話 cookie 來註銷用戶。

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";

export default withIronSessionApiRoute(
  function logoutRoute(req, res, session) {
    req.session.destroy();
    res.send({ status: "success" });
  },
  sessionCookie()
);

創建儀表板頁面

該頁面為用戶提供了簽到和退出考勤表的界面。管理員還可以創建考勤表。打開 page/dashboard/index.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";

export default function Page(props) {

  const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));

  const sign = useCallback((action="") => {

    const body = {
      attendanceSheetId: attendanceSheet[0]?.id,
      action
    }

    postData("/api/sign-attendance", body).then(data => {

      if (data.status === "success") {

        setState(prevState => {

          const newState = [...prevState]

          newState[0].attendance[0] = data.data

          return newState

        })
     
      }

    })

  }, [attendanceSheet])

  const createAttendance = useCallback(() => {

    postData("/api/create-attendance").then(data => {

      if (data.status === "success") {
        alert("New Attendance Sheet Created")
        setState([{...data.data, attendance:[]}])
      }

    })

  }, [])

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

          {
            props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
          }
            
          { attendanceSheet.length > 0 &&

            <table className={dashboard.table}>
              <thead>
                <tr> 
                  <th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th> 
                </tr>
              </thead>

              <tbody>
                <tr>
                  <td>{attendanceSheet[0]?.id}</td>
                  <td>{attendanceSheet[0]?.createdAt}</td>

                  {
                    attendanceSheet[0]?.attendance.length != 0 ? 
                      <>
                        <td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
                        <td>{
                          attendanceSheet[0]?.attendance[0]?.signOut ? 
                          attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
                      </>
                      :
                      <>
                        <td> <button onClick={() => sign()}> Sign In </button> </td>
                        <td>{""}</td>
                      </>
                  }
                </tr>
              </tbody>

            </table>

          }
          
        </div>

      </main>

    </div>
  )
}

我們使用getServerSideProps來生成頁面數據,並且withIronSessionSsr是用於處理服務器端渲染頁面的 iron-session 函數。在下面的代碼片段中,我們使用出勤表中的一行查詢出勤表的最後一行,其中userId等於存儲在用戶會話中的用戶 ID。我們還檢查用戶是否是管理員。

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({  
    take: 1,
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
      isAdmin: user.role === "ADMIN"
    }
  }

}, sessionCookie())

設置創建出勤 API 路由

打開 page/api/create-attendance.js 文件並添加下面的代碼片段。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const user = req.session.user

    const attendanceSheet = await prisma.attendanceSheet.create({
        data: {
          userId: user.id,
        },
    })

    res.json({status: "success", data: attendanceSheet});
    
}, sessionCookie())

設置簽到 API 路由

該路由處理我們對 localhost:3000/api/sign-attendance 的 API POST 請求。該路由接受 POST 請求,而attendanceSheetIdaction用於登錄和註銷attendanceSheet

打開 /page/api/sign-attendance.js 文件並添加下面的代碼片段。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const {attendanceSheetId, action} = parseBody(req.body)

    const user = req.session.user

    const attendance = await prisma.attendance.findMany({
        where: {
            userId: user.id,
            attendanceSheetId: attendanceSheetId
        }
    })

    //check if atendance have been created
    if (attendance.length === 0) {
        const attendance = await prisma.attendance.create({
            data: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId,
                signIn: true,
                signOut: false,
                signOutTime: new Date()
            },
        })   

        return res.json({status: "success", data: attendance});

    } else if (action === "sign-out") {
        await prisma.attendance.updateMany({
            where: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId
            },
            data: {
              signOut: true,
              signOutTime: new Date()
            },
        })

        return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
    }

    res.json({status: "success", data: attendance});
    
}, sessionCookie())

創建考勤頁面

此服務器端呈現的頁面顯示登錄用戶的所有考勤表。打開 /page/dashboard/attendance.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        <table className={dashboard.table}>

          <thead>

            <tr> 
              <th> Attendance Id</th> <th>Date</th> 
              <th>Sign In Time</th> <th>Sign Out Time</th> 
            </tr> 

          </thead>

            <tbody>

              {
                data.map(data =>   {

                  const {id, createdAt, attendance } = data

  
                  return (
                    <tr key={id}> 

                      <td>{id}</td> <td>{createdAt}</td>  

                      { attendance.length === 0 ? 
                      
                        (
                          <>
                            <td>You did not Sign In</td>
                            <td>You did not Sign Out</td>
                          </>
                        )
                        :
                        (
                          <>
                            <td>{attendance[0]?.signInTime}</td>
                            <td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
                          </>
                        )
                        
                      }
              
                    </tr>
                  )

                })

              }  

            </tbody>

          </table>

        </div>

      </main>

    </div>
  )
}

在下面的代碼片段中,我們從表中查詢所有行,attendanceSheet並獲取與userId存儲在用戶會話中的用戶 ID 相同的出勤率。

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()
  
  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

創建考勤表頁面

此服務器端呈現的頁面顯示所有考勤表和登錄到該考勤表的員工。打開 /page/dashboard/attendance.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        {
          data?.map(data => {

            const {id, createdAt, attendance } = data

            return (
              <>

                <table key={data.id} className={dashboard.table}>

                  <thead>
                    
                    <tr> 
                      <th> Attendance Id</th> <th>Date</th> 
                      <th> Name </th> <th> Email </th> <th> Role </th>
                      <th>Sign In Time</th> <th>Sign Out Time</th> 
                    </tr> 

                  </thead>

                  <tbody>

                    {
                      (attendance.length === 0)  &&
                      (
                        <>
                        <tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
                        </>
                      )
                    }

                    {
                      attendance.map(data => {

                        const {name, email, role} = data.user

                      
                        return (
                          <tr key={id}> 

                            <td>{id}</td> <td>{createdAt}</td>  

                            <td>{name}</td> <td>{email}</td>

                            <td>{role}</td>

                            <td>{data.signInTime}</td>

                            <td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>  
                    
                          </tr>
                        )

                      })

                    }  

                  </tbody>
                  
                </table>
              </>
            )
          })
          
          }

        </div>

      </main>

    </div>
  )
}

在下面的代碼片段中,我們從表中查詢所有行,attendanceSheet並選擇姓名、電子郵件和角色來獲取出勤率。

export const getServerSideProps = withIronSessionSsr(async () => {

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        include: { 
          user: {
            select: {
              name: true, 
              email: true, 
              role: true
            }
          }
        }
      },
    },

  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

測試應用程序

首先,我們必須將用戶添加到我們的數據庫中。我們將使用 Prisma Studio 來做到這一點。要啟動 Prisma studio,請運行以下命令:

npx prisma studio

Prisma 索引頁面如下所示:

棱鏡索引頁面


要創建具有 ADMIN 角色的數據庫用戶和具有 EMPLOYEE 角色的多個用戶,請轉到此頁面:

添加用戶

單擊添加記錄,然後填寫必填字段:密碼、姓名、電子郵件和角色。完成後,單擊綠色的 Save 1 change 按鈕。請注意,為簡單起見,我們沒有散列密碼。

使用 啟動服務器yarn dev。這將啟動服務器並在 [localhost:3000](<http://localhost:3000>) 上運行應用程序,登錄頁面如下所示。

登錄頁面

使用具有 ADMIN 角色的用戶登錄,因為只有管理用戶才能創建考勤表。登錄成功後,該應用程序會將您重定向到儀表板。

點擊創建考勤表按鈕創建考勤表,然後等待請求完成,考勤表就會出現。用戶儀表板如下所示。

創建考勤表

考勤表如下圖,點擊Sign In按鈕進行簽到。簽到成功後,會顯示簽到時間,並且會出現Sign Out按鈕。單擊“退出”按鈕退出,並對不同的用戶重複此過程多次。

成功登錄

接下來單擊側邊欄中的出勤鏈接以查看用戶的出勤情況。結果應與下面顯示的匹配:

出勤頁面

接下來點擊側邊欄的考勤錶鍊接,查看所有用戶的考勤情況。結果如下所示:

考勤表頁面

結論

在本文中,您學習瞭如何將自定義 Fastify 服務器與 Next.js 一起使用。您還了解了 Prisma 和 Prisma studio。我已經向您介紹瞭如何將 Prisma 連接到 Postgres 數據庫,以及如何使用 Prisma 客戶端和 Prisma studio 創建、讀取和更新數據庫。

鏈接:https ://arctype.com/blog/fullstack-nextjs-postgres-fastify/

#fullstack #nextjs #postgre #fastify

What is GEEK

Buddha Community

 打開選項 如何使用 Next.js、Prisma、Postgres 構建全棧應用程序

NBB: Ad-hoc CLJS Scripting on Node.js

Nbb

Not babashka. Node.js babashka!?

Ad-hoc CLJS scripting on Node.js.

Status

Experimental. Please report issues here.

Goals and features

Nbb's main goal is to make it easy to get started with ad hoc CLJS scripting on Node.js.

Additional goals and features are:

  • Fast startup without relying on a custom version of Node.js.
  • Small artifact (current size is around 1.2MB).
  • First class macros.
  • Support building small TUI apps using Reagent.
  • Complement babashka with libraries from the Node.js ecosystem.

Requirements

Nbb requires Node.js v12 or newer.

How does this tool work?

CLJS code is evaluated through SCI, the same interpreter that powers babashka. Because SCI works with advanced compilation, the bundle size, especially when combined with other dependencies, is smaller than what you get with self-hosted CLJS. That makes startup faster. The trade-off is that execution is less performant and that only a subset of CLJS is available (e.g. no deftype, yet).

Usage

Install nbb from NPM:

$ npm install nbb -g

Omit -g for a local install.

Try out an expression:

$ nbb -e '(+ 1 2 3)'
6

And then install some other NPM libraries to use in the script. E.g.:

$ npm install csv-parse shelljs zx

Create a script which uses the NPM libraries:

(ns script
  (:require ["csv-parse/lib/sync$default" :as csv-parse]
            ["fs" :as fs]
            ["path" :as path]
            ["shelljs$default" :as sh]
            ["term-size$default" :as term-size]
            ["zx$default" :as zx]
            ["zx$fs" :as zxfs]
            [nbb.core :refer [*file*]]))

(prn (path/resolve "."))

(prn (term-size))

(println (count (str (fs/readFileSync *file*))))

(prn (sh/ls "."))

(prn (csv-parse "foo,bar"))

(prn (zxfs/existsSync *file*))

(zx/$ #js ["ls"])

Call the script:

$ nbb script.cljs
"/private/tmp/test-script"
#js {:columns 216, :rows 47}
510
#js ["node_modules" "package-lock.json" "package.json" "script.cljs"]
#js [#js ["foo" "bar"]]
true
$ ls
node_modules
package-lock.json
package.json
script.cljs

Macros

Nbb has first class support for macros: you can define them right inside your .cljs file, like you are used to from JVM Clojure. Consider the plet macro to make working with promises more palatable:

(defmacro plet
  [bindings & body]
  (let [binding-pairs (reverse (partition 2 bindings))
        body (cons 'do body)]
    (reduce (fn [body [sym expr]]
              (let [expr (list '.resolve 'js/Promise expr)]
                (list '.then expr (list 'clojure.core/fn (vector sym)
                                        body))))
            body
            binding-pairs)))

Using this macro we can look async code more like sync code. Consider this puppeteer example:

(-> (.launch puppeteer)
      (.then (fn [browser]
               (-> (.newPage browser)
                   (.then (fn [page]
                            (-> (.goto page "https://clojure.org")
                                (.then #(.screenshot page #js{:path "screenshot.png"}))
                                (.catch #(js/console.log %))
                                (.then #(.close browser)))))))))

Using plet this becomes:

(plet [browser (.launch puppeteer)
       page (.newPage browser)
       _ (.goto page "https://clojure.org")
       _ (-> (.screenshot page #js{:path "screenshot.png"})
             (.catch #(js/console.log %)))]
      (.close browser))

See the puppeteer example for the full code.

Since v0.0.36, nbb includes promesa which is a library to deal with promises. The above plet macro is similar to promesa.core/let.

Startup time

$ time nbb -e '(+ 1 2 3)'
6
nbb -e '(+ 1 2 3)'   0.17s  user 0.02s system 109% cpu 0.168 total

The baseline startup time for a script is about 170ms seconds on my laptop. When invoked via npx this adds another 300ms or so, so for faster startup, either use a globally installed nbb or use $(npm bin)/nbb script.cljs to bypass npx.

Dependencies

NPM dependencies

Nbb does not depend on any NPM dependencies. All NPM libraries loaded by a script are resolved relative to that script. When using the Reagent module, React is resolved in the same way as any other NPM library.

Classpath

To load .cljs files from local paths or dependencies, you can use the --classpath argument. The current dir is added to the classpath automatically. So if there is a file foo/bar.cljs relative to your current dir, then you can load it via (:require [foo.bar :as fb]). Note that nbb uses the same naming conventions for namespaces and directories as other Clojure tools: foo-bar in the namespace name becomes foo_bar in the directory name.

To load dependencies from the Clojure ecosystem, you can use the Clojure CLI or babashka to download them and produce a classpath:

$ classpath="$(clojure -A:nbb -Spath -Sdeps '{:aliases {:nbb {:replace-deps {com.github.seancorfield/honeysql {:git/tag "v2.0.0-rc5" :git/sha "01c3a55"}}}}}')"

and then feed it to the --classpath argument:

$ nbb --classpath "$classpath" -e "(require '[honey.sql :as sql]) (sql/format {:select :foo :from :bar :where [:= :baz 2]})"
["SELECT foo FROM bar WHERE baz = ?" 2]

Currently nbb only reads from directories, not jar files, so you are encouraged to use git libs. Support for .jar files will be added later.

Current file

The name of the file that is currently being executed is available via nbb.core/*file* or on the metadata of vars:

(ns foo
  (:require [nbb.core :refer [*file*]]))

(prn *file*) ;; "/private/tmp/foo.cljs"

(defn f [])
(prn (:file (meta #'f))) ;; "/private/tmp/foo.cljs"

Reagent

Nbb includes reagent.core which will be lazily loaded when required. You can use this together with ink to create a TUI application:

$ npm install ink

ink-demo.cljs:

(ns ink-demo
  (:require ["ink" :refer [render Text]]
            [reagent.core :as r]))

(defonce state (r/atom 0))

(doseq [n (range 1 11)]
  (js/setTimeout #(swap! state inc) (* n 500)))

(defn hello []
  [:> Text {:color "green"} "Hello, world! " @state])

(render (r/as-element [hello]))

Promesa

Working with callbacks and promises can become tedious. Since nbb v0.0.36 the promesa.core namespace is included with the let and do! macros. An example:

(ns prom
  (:require [promesa.core :as p]))

(defn sleep [ms]
  (js/Promise.
   (fn [resolve _]
     (js/setTimeout resolve ms))))

(defn do-stuff
  []
  (p/do!
   (println "Doing stuff which takes a while")
   (sleep 1000)
   1))

(p/let [a (do-stuff)
        b (inc a)
        c (do-stuff)
        d (+ b c)]
  (prn d))
$ nbb prom.cljs
Doing stuff which takes a while
Doing stuff which takes a while
3

Also see API docs.

Js-interop

Since nbb v0.0.75 applied-science/js-interop is available:

(ns example
  (:require [applied-science.js-interop :as j]))

(def o (j/lit {:a 1 :b 2 :c {:d 1}}))

(prn (j/select-keys o [:a :b])) ;; #js {:a 1, :b 2}
(prn (j/get-in o [:c :d])) ;; 1

Most of this library is supported in nbb, except the following:

  • destructuring using :syms
  • property access using .-x notation. In nbb, you must use keywords.

See the example of what is currently supported.

Examples

See the examples directory for small examples.

Also check out these projects built with nbb:

API

See API documentation.

Migrating to shadow-cljs

See this gist on how to convert an nbb script or project to shadow-cljs.

Build

Prequisites:

  • babashka >= 0.4.0
  • Clojure CLI >= 1.10.3.933
  • Node.js 16.5.0 (lower version may work, but this is the one I used to build)

To build:

  • Clone and cd into this repo
  • bb release

Run bb tasks for more project-related tasks.

Download Details:
Author: borkdude
Download Link: Download The Source Code
Official Website: https://github.com/borkdude/nbb 
License: EPL-1.0

#node #javascript

Eva  Murphy

Eva Murphy

1625674200

Google analytics Setup with Next JS, React JS using Router Events - 14

In this video, we are going to implement Google Analytics to our Next JS application. Tracking page views of an application is very important.

Google analytics will allow us to track analytics information.

Frontend: https://github.com/amitavroy/video-reviews
API: https://github.com/amitavdevzone/video-review-api
App link: https://video-reviews.vercel.app

You can find me on:
Twitter: https://twitter.com/amitavroy7​
Discord: https://discord.gg/Em4nuvQk

#next js #js #react js #react #next #google analytics

Kiera Smart

Kiera Smart

1597654680

Building a Backend for React with Next.js, Prisma 2, and Postgres

Want to persist your data between page loads? You need a backend! Becoming a full stack developer means learning the ins and outs of how to add a backend that your frontend in React can interact with. In this video we will take an existing React app using Google Maps, and using Next.js (API routes), Prisma 2, and Postgres, we will learn how to read data from, and save data to our database.

  • 00:00 - Introduction
  • 05:40 - Setting up Prisma 2
  • 09:05 - Defining Prisma Schema
  • 14:30 - Next.js API Routes
  • 19:30 - Creating Sighting w/ Prisma
  • 24:20 - Fetching Sightings w/ Prisma
  • 27:01 - react-query useQuery
  • 31:30 - react-query useMutation
  • 40:00 - Optimistic UI w/ react-query

#react #next #prisma #postgres #database

Eva  Murphy

Eva Murphy

1625751960

Laravel API and React Next JS frontend development - 28

In this video, I wanted to touch upon the functionality of adding Chapters inside a Course. The idea was to not think much and start the development and pick up things as they come.

There are places where I get stuck and trying to find answers to it up doing what every developer does - Google and get help. I hope this will help you understand the flow and also how developers debug while doing development.

App url: https://video-reviews.vercel.app
Github code links below:
Next JS App: https://github.com/amitavroy/video-reviews
Laravel API: https://github.com/amitavdevzone/video-review-api

You can find me on:
Twitter: https://twitter.com/amitavroy7​
Discord: https://discord.gg/Em4nuvQk

#next js #api #react next js #next #frontend #development

高橋  陽子

高橋 陽子

1657280057

打開選項 如何使用 Next.js、Prisma、Postgres 構建全棧應用程序

在本文中,我們將學習如何使用 Next.js、Prisma、Postgres 和 Fastify 構建全棧應用程序。我們將構建一個考勤管理演示應用程序來管理員工的考勤。該應用程序的流程很簡單:管理用戶登錄,創建當天的考勤表,然後每個員工登錄和退出考勤表。

Next.js 是什麼?

Next.js 是一個靈活的 React 框架,它為您提供構建塊來創建快速的 Web 應用程序。它通常被稱為全棧 React 框架,因為它使得前端和後端應用程序可以在同一個代碼庫上使用無服務器功能來實現。

什麼是棱鏡?

Prisma 是一個開源的 Node.js 和 Typescript ORM,它極大地簡化了 SQL 數據庫的數據建模、遷移和數據訪問。在撰寫本文時,Prisma 支持以下數據庫管理系統:PostgreSQL、MySQL、MariaDB、SQLite、AWS Aurora、Microsoft SQL Server、Azure SQL 和 MongoDB。您可能還想單擊此處查看所有受支持的數據庫管理系統的列表。

什麼是 Postgres?

Postgres 也稱為 PostgreSQL,它是一個免費和開源的關係數據庫管理系統。它是 SQL 語言的超集,它具有許多功能,允許開發人員安全地存儲和擴展複雜的數據工作負載。

先決條件

本教程是一個動手演示教程。因此,最好在您的計算機上安裝以下內容以進行後續操作:

本教程的代碼可Github 上找到,因此請隨意克隆它並繼續學習。

項目設置

讓我們從設置 Next.js 應用程序開始。要開始,請運行以下命令。

npx create-next-app@latest

等待安裝完成,然後運行下面的命令來安裝我們的依賴項。

yarn add fastify fastify-nextjs iron-session @prisma/client
yarn add prisma nodemon --dev

等待安裝完成。

設置 Next.js 和 Fastify

默認情況下,Next.js 不使用 Fastify 作為其服務器。要使用 Fastify 為我們的 Next.js 應用程序提供服務,請package.json使用下面的代碼片段編輯文件中的腳本字段。

"scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

創建我們的 Fastify 服務器

現在讓我們創建一個server.js文件。該文件是我們應用程序的入口點,然後我們添加require('fastify-nextjs')以包含插件,該插件在 fastify 中公開 Next.js API 以處理渲染。

打開server.js文件,並添加以下代碼片段:

const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})

在上面的代碼片段中,我們使用了fastify-nextjs在 Fastify 中公開 Next.js API 的插件,它為我們處理渲染。然後我們使用函數解析傳入的請求,使請求主體可用於我們的 Next.js API 路由處理程序,並使用命令noOpParser為我們的應用程序定義兩個路由。[fastify.next](<http://fastify.next>然後我們創建我們的 Fastify 服務器並讓它監聽 3000 端口。

現在繼續使用yarn dev命令運行應用程序:應用程序將在localhost:3000.

棱鏡設置

首先,運行以下命令以獲取基本的 Prisma 設置:

npx prisma init

上面的命令將創建一個包含schema.prisma文件的 prisma 目錄。這是您的主要 Prisma 配置文件,它將包含您的數據庫模式。此外,.env將在項目的根目錄中添加一個文件。打開.env文件並將虛擬連接 URL 替換為 PostgreSQL 數據庫的連接 URL。

將文件中的代碼替換為prisma/schema.prisma以下內容:

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(EMPLOYEE)
  attendance     Attendance[]
  AttendanceSheet AttendanceSheet[]
}

model AttendanceSheet {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  createdBy    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

model Attendance {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  signIn    Boolean @default(true)
  signOut   Boolean
  signInTime    DateTime @default(now())
  signOutTime   DateTime 
  user    User?    @relation(fields: [userId], references: [id])
  userId  Int?
}

enum Role {
  EMPLOYEE
  ADMIN
}

在上面的代碼片段中,我們創建了一個 User、AttendanceSheet 和 Attendance Model,定義了每個模型之間的關係。

接下來,在數據庫中創建這些表。運行以下命令:

npx prisma db push

運行上述命令後,您應該會在終端中看到如下屏幕截圖所示的輸出:

創建實用函數

完成 Prisma 設置後,讓我們創建三個實用程序函數,它們將不時在我們的應用程序中使用。

打開 lib/parseBody.js 文件並添加以下代碼片段。此函數將請求正文解析為 JSON:

export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}

打開 lib/request.js 文件並添加以下代碼片段。該函數發送一個 POST 請求。

export async function postData(url = '', data='') {
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(data)
});
return response.json();
}

打開 /lib/request.js 文件並添加以下代碼片段。此函數返回 Iron 會話 iron-session 的會話屬性對象。

export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}

接下來,添加SESSION_PASSWORD到 .env 文件:它應該是至少 32 個字符的字符串。

為應用程序設計樣式

完成我們的實用程序功能後,讓我們為應用程序添加一些樣式。我們正在為這個應用程序使用 css 模塊,所以打開styles/Home.modules.css文件並添加下面的代碼片段:

.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  padding: 4rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.login {
  width: 450px;
}

.login input {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.login button {
  width: 100%;
  height: 50px;
  margin: 4px;
}

.dashboard {
  display: grid;
  grid-template-columns: 3fr 9fr;
  grid-template-rows: 1fr;
  grid-column-gap: 0px;
  grid-row-gap: 0px;
  height: calc(100vh - 60px);
}

.navbar {
  height: 60px;
  background-color: black;
}

創建側邊欄組件

完成樣式後,讓我們創建側邊欄組件來幫助我們導航到應用儀表板上的不同頁面。打開 components/SideBar.js 文件,然後粘貼下面的代碼片段。

import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'

const SideBar = () => {

    const router = useRouter()

    const logout = async () => {

        try {

            const response = await fetch('/api/logout', {
                method: 'GET', 
                credentials: 'same-origin', 
            });

            if(response.status === 200)  router.push('/')

        } catch (e) {
            alert(e)
        }
  
    }
      

    return (
        <nav className={styles.sidebar}>

            <ul>

                <li> <Link href="/dashboard"> Dashboard</Link> </li>

                <li> <Link href="/dashboard/attendance"> Attendance </Link> </li>

                <li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>

                <li onClick={logout}> Logout </li>

            </ul>

        </nav>
    )

}

export default SideBar

登錄頁面

現在打開 page/index.js 文件,刪除那裡的所有代碼並添加以下代碼片段。下面的代碼將使用通過表單提供的電子郵件和密碼發送到 localhost:3000/api/login 路由的發布請求。驗證憑據後,它會調用router.push('/dashboard')將用戶重定向到 localhost:3000/api/dashboard 的方法:

import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'

export default function Home({posts}) {

  const [data, setData] = useState({email: null, password: null});

  const router = useRouter()

  const submit = (e) => {
    e.preventDefault()

    if(data.email && data.password) {
      postData('/api/login', data).then(data => {
        console.log(data); 

        if (data.status === "success") router.push('/dashboard')
      
      });
    }

  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Login</title>
        <meta name="description" content="Login" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>

        <form  className={styles.login}>

          <input 
            type={"text"} 
            placeholder="Enter Your Email" 
            onChange={(e) => setData({...data, email: e.target.value})} />

          <input 
            type={"password"}  
            placeholder="Enter Your Password"
            onChange={(e) => setData({...data, password: e.target.value})} />

          <button onClick={submit}>Login</button>

        </form>
        
      </main>

    </div>
  )
}

設置登錄 API 路由

現在打開 page/api/login.js 文件並添加以下代碼片段。我們將使用它PrismaClient來進行數據庫查詢,並且withIronSessionApiRoute是用於在 RESTful 應用程序中處理用戶會話的鐵會話功能。

此路由處理對 localhost:3000/api/login 的登錄 POST 請求,並在用戶通過身份驗證後生成身份驗證 cookie。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

export default withIronSessionApiRoute(
    async function loginRoute(req, res) {

      const { email, password } = parseBody(req.body)

      const prisma = new PrismaClient()

      // By unique identifier
      const user = await prisma.user.findUnique({
        where: {
        email
      },})

      if(user.password === password) {

        // get user from database then:
        user.password = undefined
        req.session.user = user
        await req.session.save();

        return res.send({ status: 'success', data: user });

      };

    res.send({ status: 'error', message: "incorrect email or password" });

  },
  sessionCookie(),
);

設置註銷 API 路由

打開 /page/api/logout 文件並添加下面的代碼片段。此路由處理對 localhost:3000/api/logout 的 GET 請求,該請求通過銷毀會話 cookie 來註銷用戶。

import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";

export default withIronSessionApiRoute(
  function logoutRoute(req, res, session) {
    req.session.destroy();
    res.send({ status: "success" });
  },
  sessionCookie()
);

創建儀表板頁面

該頁面為用戶提供了簽到和退出考勤表的界面。管理員還可以創建考勤表。打開 page/dashboard/index.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";

export default function Page(props) {

  const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));

  const sign = useCallback((action="") => {

    const body = {
      attendanceSheetId: attendanceSheet[0]?.id,
      action
    }

    postData("/api/sign-attendance", body).then(data => {

      if (data.status === "success") {

        setState(prevState => {

          const newState = [...prevState]

          newState[0].attendance[0] = data.data

          return newState

        })
     
      }

    })

  }, [attendanceSheet])

  const createAttendance = useCallback(() => {

    postData("/api/create-attendance").then(data => {

      if (data.status === "success") {
        alert("New Attendance Sheet Created")
        setState([{...data.data, attendance:[]}])
      }

    })

  }, [])

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

          {
            props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
          }
            
          { attendanceSheet.length > 0 &&

            <table className={dashboard.table}>
              <thead>
                <tr> 
                  <th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th> 
                </tr>
              </thead>

              <tbody>
                <tr>
                  <td>{attendanceSheet[0]?.id}</td>
                  <td>{attendanceSheet[0]?.createdAt}</td>

                  {
                    attendanceSheet[0]?.attendance.length != 0 ? 
                      <>
                        <td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
                        <td>{
                          attendanceSheet[0]?.attendance[0]?.signOut ? 
                          attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
                      </>
                      :
                      <>
                        <td> <button onClick={() => sign()}> Sign In </button> </td>
                        <td>{""}</td>
                      </>
                  }
                </tr>
              </tbody>

            </table>

          }
          
        </div>

      </main>

    </div>
  )
}

我們使用getServerSideProps來生成頁面數據,並且withIronSessionSsr是用於處理服務器端渲染頁面的 iron-session 函數。在下面的代碼片段中,我們使用出勤表中的一行查詢出勤表的最後一行,其中userId等於存儲在用戶會話中的用戶 ID。我們還檢查用戶是否是管理員。

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({  
    take: 1,
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
      isAdmin: user.role === "ADMIN"
    }
  }

}, sessionCookie())

設置創建出勤 API 路由

打開 page/api/create-attendance.js 文件並添加下面的代碼片段。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const user = req.session.user

    const attendanceSheet = await prisma.attendanceSheet.create({
        data: {
          userId: user.id,
        },
    })

    res.json({status: "success", data: attendanceSheet});
    
}, sessionCookie())

設置簽到 API 路由

該路由處理我們對 localhost:3000/api/sign-attendance 的 API POST 請求。該路由接受 POST 請求,而attendanceSheetIdaction用於登錄和註銷attendanceSheet

打開 /page/api/sign-attendance.js 文件並添加下面的代碼片段。

import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';

  
export default withIronSessionApiRoute( async function handler(req, res) {

    const prisma = new PrismaClient()

    const {attendanceSheetId, action} = parseBody(req.body)

    const user = req.session.user

    const attendance = await prisma.attendance.findMany({
        where: {
            userId: user.id,
            attendanceSheetId: attendanceSheetId
        }
    })

    //check if atendance have been created
    if (attendance.length === 0) {
        const attendance = await prisma.attendance.create({
            data: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId,
                signIn: true,
                signOut: false,
                signOutTime: new Date()
            },
        })   

        return res.json({status: "success", data: attendance});

    } else if (action === "sign-out") {
        await prisma.attendance.updateMany({
            where: {
                userId: user.id,
                attendanceSheetId: attendanceSheetId
            },
            data: {
              signOut: true,
              signOutTime: new Date()
            },
        })

        return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
    }

    res.json({status: "success", data: attendance});
    
}, sessionCookie())

創建考勤頁面

此服務器端呈現的頁面顯示登錄用戶的所有考勤表。打開 /page/dashboard/attendance.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        <table className={dashboard.table}>

          <thead>

            <tr> 
              <th> Attendance Id</th> <th>Date</th> 
              <th>Sign In Time</th> <th>Sign Out Time</th> 
            </tr> 

          </thead>

            <tbody>

              {
                data.map(data =>   {

                  const {id, createdAt, attendance } = data

  
                  return (
                    <tr key={id}> 

                      <td>{id}</td> <td>{createdAt}</td>  

                      { attendance.length === 0 ? 
                      
                        (
                          <>
                            <td>You did not Sign In</td>
                            <td>You did not Sign Out</td>
                          </>
                        )
                        :
                        (
                          <>
                            <td>{attendance[0]?.signInTime}</td>
                            <td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
                          </>
                        )
                        
                      }
              
                    </tr>
                  )

                })

              }  

            </tbody>

          </table>

        </div>

      </main>

    </div>
  )
}

在下面的代碼片段中,我們從表中查詢所有行,attendanceSheet並獲取與userId存儲在用戶會話中的用戶 ID 相同的出勤率。

export const getServerSideProps = withIronSessionSsr( async ({req}) => {

  const user = req.session.user

  const prisma = new PrismaClient()
  
  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        where: {
          userId: user.id
        },
      }
    }
  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

創建考勤表頁面

此服務器端呈現的頁面顯示所有考勤表和登錄到該考勤表的員工。打開 /page/dashboard/attendance.js 文件並添加下面的代碼片段。

import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";

export default function Page(props) {

  const data = JSON.parse(props.attendanceSheet)

  return (
    <div>

      <Head>
        <title>Attendance Management Dashboard</title>
        <meta name="description" content="dashboard" />
      </Head>

      <div className={styles.navbar}></div>

      <main className={styles.dashboard}>

        <SideBar />

        <div className={dashboard.users}>

        {
          data?.map(data => {

            const {id, createdAt, attendance } = data

            return (
              <>

                <table key={data.id} className={dashboard.table}>

                  <thead>
                    
                    <tr> 
                      <th> Attendance Id</th> <th>Date</th> 
                      <th> Name </th> <th> Email </th> <th> Role </th>
                      <th>Sign In Time</th> <th>Sign Out Time</th> 
                    </tr> 

                  </thead>

                  <tbody>

                    {
                      (attendance.length === 0)  &&
                      (
                        <>
                        <tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
                        </>
                      )
                    }

                    {
                      attendance.map(data => {

                        const {name, email, role} = data.user

                      
                        return (
                          <tr key={id}> 

                            <td>{id}</td> <td>{createdAt}</td>  

                            <td>{name}</td> <td>{email}</td>

                            <td>{role}</td>

                            <td>{data.signInTime}</td>

                            <td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>  
                    
                          </tr>
                        )

                      })

                    }  

                  </tbody>
                  
                </table>
              </>
            )
          })
          
          }

        </div>

      </main>

    </div>
  )
}

在下面的代碼片段中,我們從表中查詢所有行,attendanceSheet並選擇姓名、電子郵件和角色來獲取出勤率。

export const getServerSideProps = withIronSessionSsr(async () => {

  const prisma = new PrismaClient()

  const attendanceSheet = await prisma.attendanceSheet.findMany({
    orderBy: {
      id: 'desc',
    },
    include: { 
      attendance: {
        include: { 
          user: {
            select: {
              name: true, 
              email: true, 
              role: true
            }
          }
        }
      },
    },

  })

  return {
    props: {
      attendanceSheet: JSON.stringify(attendanceSheet),
    }
  }

}, sessionCookie())

測試應用程序

首先,我們必須將用戶添加到我們的數據庫中。我們將使用 Prisma Studio 來做到這一點。要啟動 Prisma studio,請運行以下命令:

npx prisma studio

Prisma 索引頁面如下所示:

棱鏡索引頁面


要創建具有 ADMIN 角色的數據庫用戶和具有 EMPLOYEE 角色的多個用戶,請轉到此頁面:

添加用戶

單擊添加記錄,然後填寫必填字段:密碼、姓名、電子郵件和角色。完成後,單擊綠色的 Save 1 change 按鈕。請注意,為簡單起見,我們沒有散列密碼。

使用 啟動服務器yarn dev。這將啟動服務器並在 [localhost:3000](<http://localhost:3000>) 上運行應用程序,登錄頁面如下所示。

登錄頁面

使用具有 ADMIN 角色的用戶登錄,因為只有管理用戶才能創建考勤表。登錄成功後,該應用程序會將您重定向到儀表板。

點擊創建考勤表按鈕創建考勤表,然後等待請求完成,考勤表就會出現。用戶儀表板如下所示。

創建考勤表

考勤表如下圖,點擊Sign In按鈕進行簽到。簽到成功後,會顯示簽到時間,並且會出現Sign Out按鈕。單擊“退出”按鈕退出,並對不同的用戶重複此過程多次。

成功登錄

接下來單擊側邊欄中的出勤鏈接以查看用戶的出勤情況。結果應與下面顯示的匹配:

出勤頁面

接下來點擊側邊欄的考勤錶鍊接,查看所有用戶的考勤情況。結果如下所示:

考勤表頁面

結論

在本文中,您學習瞭如何將自定義 Fastify 服務器與 Next.js 一起使用。您還了解了 Prisma 和 Prisma studio。我已經向您介紹瞭如何將 Prisma 連接到 Postgres 數據庫,以及如何使用 Prisma 客戶端和 Prisma studio 創建、讀取和更新數據庫。

鏈接:https ://arctype.com/blog/fullstack-nextjs-postgres-fastify/

#fullstack #nextjs #postgre #fastify