如何使用Prisma构建一个Express应用程序

867 阅读10分钟

Prisma是一个使用TypeScript的Node.js的ORM(对象关系映射)工具。 该软件集成了当今许多最流行的数据库,包括MySQL、SQL Server、SQLite和MongoDB,并强调了一个具有类型安全的数据库客户端的人类可读模式。 Prisma还包括其他功能,如迁移、种子数据和一个虚拟数据库浏览器。

在这个项目中,你将使用Prisma将你的Express应用程序连接到数据库服务器。 你将建立一个模式来模拟一个锻炼跟踪器应用程序。 然后,你将创建一些种子数据,并使用Prisma来运行迁移和种子数据库。 最后,你将使用PugTailwind CSS创建Web应用程序,以建立应用程序的前端。

前提条件

本教程使用了以下技术,但不需要任何先前的经验:

  • 一个用于JavaScript的IDE。 我将使用Visual Studio Code,但你可以使用Webstorm,或其他你喜欢的IDE。
  • Node.js
  • 一个数据库,如PostgreSQL、MySQL、SQLite、SQL Server或MongoDB。 在本教程中,我们将使用SQLite。
  • Okta CLI

如果你想跳过本教程,查看完整的项目,你可以去GitHub上查看

创建你的OAuth2授权服务器

为你的应用程序创建一个新目录。 使用cd 命令导航到该文件夹。

在你开始之前,你需要一个免费的Okta开发者账户。安装Okta CLI并运行okta register 来注册一个新的账户。如果你已经有一个账户,运行okta login 。然后,运行okta apps create 。选择默认的应用程序名称,或者根据你的需要进行更改。选择网络,然后按回车键

选择 "其他"。 然后,将重定向URI改为http://localhost:3000/authorization-code/callback ,并使用http://localhost:3000/ ,作为注销重定向URI。

Okta CLI是做什么的?

Okta CLI将在您的Okta机构中创建一个OIDC网络应用。它将添加您指定的重定向URI,并授予Everyone组的访问权限。当它完成后,您会看到如下输出:

Okta application configuration has been written to: /path/to/app/.okta.env

运行cat .okta.env (或Windows上的type .okta.env ),查看你的应用程序的发行者和凭证:

export OKTA_OAUTH2_ISSUER="https://dev-133337.okta.com/oauth2/default"
export OKTA_OAUTH2_CLIENT_ID="0oab8eb55Kb9jdMIr5d6"
export OKTA_OAUTH2_CLIENT_SECRET="NEVER-SHOW-SECRETS"

您的Okta域是发行者的第一部分,在/oauth2/default

注意:您也可以使用Okta管理控制台来创建您的应用程序。更多信息请参见创建一个网络应用程序

创建您的Express应用程序

接下来,你将使用express-generator 应用程序生成工具来快速搭建你的应用程序。 运行下面的命令:

npx express-generator@4.16 --view=pug

现在你可以安装你的软件包了:

npm i @prisma/client@3.13.0
npm i dotenv@16.0.0
npm i passport@0.5.2
npm i passport-openidconnect@0.1.1
npm i express-session@1.17
npm i -D tailwindcss@3.0.24
npm i -D prisma@3.13.0
  • @prisma/client 是用来从你的服务器代码中访问数据库的。
  • dotenv 从 文件中读取配置设置,比如由Okta CLI产生的配置。.env
  • passport 是Node.js的一个中间件,足够灵活地处理大多数认证场景,包括Okta。 是passport的一个模块,让你用OpenID Connect进行认证。passport-openidconnect
  • express-session 是passport所需要的。 Express应用程序使用这个包来支持会话。 你的应用程序必须初始化会话支持以使用passport。
  • Tailwind CSS 是你将使用的CSS框架。 如果你以前从未使用过Tailwind,你可能会想知道为什么它是一个开发依赖项。 这是因为Tailwind将根据你的视图和配置动态地构建你的CSS文件。 稍后会有更多关于这个问题的介绍。
  • prisma 是你正在使用的ORM。 这是一个开发依赖项,因为Prisma库处理迁移、种子数据等,而 包则在运行时用于你的应用程序。@prisma/client

在这一点上,你将初始化Tailwind CSS和Prisma。你将在以后配置它们的使用。

从Prisma开始,运行以下命令。

npx prisma init

该命令将在你的应用程序中添加一个名为prisma 的新文件夹。它还会在你的项目中添加一个名为.env 的文件,其中包含一些默认配置。用下面的代码替换该文件的内容。

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, and CockroachDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="file:./dev.db"
SEED_USER_NAME={yourOktaUserName}

在这个例子中,我使用了SQLite,因为它既简单又紧凑。 但Prisma和大多数ORM的一个好处是,它们可以支持许多数据库。 模板代码包括关于为其他数据库服务器配置你的应用程序的评论。 请自由使用你觉得最舒服的东西,并使用适当的连接字符串。

你的种子数据将使用SEED_USER_NAME 的设置。 你要确保它与你登录时使用的Okta用户名相同。 这将允许应用程序将您的登录用户与您的数据库中的种子数据联系起来。

接下来,用以下代码更新你的package.json 文件:

{
  "name": "workout-app",
  "version": "0.0.0",
  "private": true,
  "prisma": {
    "seed": "node prisma/seed.js"
  },
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "@prisma/client": "^3.13.0",
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "dotenv": "^16.0.0",
    "express": "~4.16.1",
    "http-errors": "~1.6.3",
    "morgan": "~1.9.1",
    "passport": "^0.5.2",
    "passport-openidconnect": "^0.1.1",
    "pug": "2.0.0-beta11"
  },
  "devDependencies": {
    "prisma": "^3.13.0",
    "tailwindcss": "^3.0.24"
  }
}

这将添加你以后需要的prisma seed 命令。

现在你可以初始化Tailwind CSS了:

npx tailwindcss init

你现在已经在你的应用程序的根部添加了一个名为tailwind.config.js 的新文件。 用下面的代码替换该文件的内容:

module.exports = {
  content: ["./views/**/*.pug"],
  theme: {
    extend: {},
  },
  plugins: [],
}

在这一步,你告诉Tailwind CSS在哪里找到你在应用程序中使用的类。 你希望Tailwind在你的views 目录中的.pug 文件中寻找。 从配置对象的themeplugins 设置中可以看出,Tailwind CSS是高度可扩展的。 深入了解这一点不在本文的范围之内,但我鼓励你去Tailwind CSS的网站上了解更多信息。

使用Prisma来创建你的数据库

下一个任务是为你的应用程序创建数据库。 这些步骤包括编写模式,编写一些种子数据,创建迁移,以及应用迁移,这也将是你的数据种子。

上面的prisma init 任务应该在你的prisma 目录中添加一个叫做schema.prisma 的文件。 用下面的代码替换那里的代码:

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

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

model WorkoutLog {
  id        Int      @id @default(autoincrement())
  userId    Int
  exercise  String
  amount    Decimal
  units     String
  date      DateTime
  minutes   Int
  calories  Int
  user      User     @relation(fields: [userId], references: [id])
}

model User {
  id          Int          @id @default(autoincrement())
  username    String       @unique
  workoutLogs WorkoutLog[]
}

请确保你用你所使用的提供者来替换{yourDatabaseProvider} 。 正如你所看到的,这个文件定义了你将存储在数据库中的UserWorkoutLog 对象。 WorkoutLog 有关于用户在某一天的锻炼的基本信息,然后将该记录与User 对象联系起来。 你也可以从这个文件中定义键、约束和索引。 注意WorkoutLog 上的user 属性有一个由userIdUser 表的id 定义的关系。

prisma 目录中添加一个名为seed.js 的文件,并在其中添加以下代码。

const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

require("dotenv").config();

async function main() {
  const user = await prisma.user.upsert({
    where: { username: process.env.SEED_USER_NAME },
    update: {},
    create: {
      username: process.env.SEED_USER_NAME,
      workoutLogs: {
        create: [
          {
            exercise: "Running",
            amount: 1,
            units: "Miles",
            date: new Date(2022, 1, 1),
            minutes: 8,
            calories: 100
          },
          {
            exercise: "Running",
            amount: 1.2,
            units: "Miles",
            date: new Date(2022, 1, 3),
            minutes: 10,
            calories: 120
          },
          {
            exercise: "Running",
            amount: 1.5,
            units: "Miles",
            date: new Date(2022, 1, 5),
            minutes: 12,
            calories: 150
          },
          {
            exercise: "Heavy Bag",
            amount: 4,
            units: "Rounds",
            date: new Date(2022, 1, 1),
            minutes: 15,
            calories: 100
          },
          {
            exercise: "Heavy Bag",
            amount: 6,
            units: "Rounds",
            date: new Date(2022, 1, 3),
            minutes: 22,
            calories: 150
          },
          {
            exercise: "Heavy Bag",
            amount: 4,
            units: "Rounds",
            date: new Date(2022, 1, 5),
            minutes: 15,
            calories: 100
          },
          {
            exercise: "Situps",
            amount: 50,
            units: "Reps",
            date: new Date(2022, 1, 2),
            minutes: 5,
            calories: 50
          },
          {
            exercise: "Pushups",
            amount: 100,
            units: "Reps",
            date: new Date(2022, 1, 2),
            minutes: 10,
            calories: 100
          },
          {
            exercise: "Situps",
            amount: 50,
            units: "Reps",
            date: new Date(2022, 1, 4),
            minutes: 5,
            calories: 50
          },
          {
            exercise: "Pushups",
            amount: 100,
            units: "Reps",
            date: new Date(2022, 1, 4),
            minutes: 10,
            calories: 100
          },
        ],
      },
    },
  });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

Prisma知道使用你添加到package.json 的命令中的seed.js 文件。这个文件将在你运行该命令时插入数据。

现在你可以使用Prisma CLI添加和应用迁移。从你的应用程序目录的根部,运行以下命令:

npx prisma migrate dev --name init

CLI有几种方法来添加和应用迁移。 我建议你了解为你的环境管理迁移的最佳方法。 上面的方法是准备数据库的最简单和最快的方法。 seed 作为迁移过程的一部分,这个命令会从你的package.json ,并且也会运行这个命令。 一旦完成,你的数据库就应该准备好了,有一个充满种子数据的数据库可以使用。

添加OIDC认证

现在你的数据库已经准备好了,你可以把注意力转向你的应用程序的核心部分。

首先,在你的应用程序的根部添加一个新的文件,名为ensureLoggedIn.js ,并在其中添加以下代码:

function ensureLoggedIn(req, res, next) {
    if (req.isAuthenticated()) {
        return next();
    }
    res.redirect('/login')
}

module.exports = ensureLoggedIn;

这个小的中间件将确保用户是经过认证的。 如果他们没有,你将把他们重定向到login 路线,你将配置该路线以使用Okta。 否则,你将允许用户到达下一个屏幕。

接下来,你可以在routes 目录中更新你的路由。 首先,删除users.js ,因为你将不会使用它。 用下面的代码替换index.js 中的代码。

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'The Workout Tracker - Home', isAuthenticated: req.isAuthenticated() });
});

module.exports = router;

在这里,你将为isAuthenticated 传递一个属性,以便你的布局页面能够正确地显示登录或注销按钮。

接下来,在routes 目录中为dashboard.js 添加一个文件,代码如下。

var express = require("express");
var router = express.Router();

const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

const ensureLoggedIn = require("../ensureLoggedIn");

/* GET home page. */
router.get("/", ensureLoggedIn, async function (req, res) {
  const username = req.user.username;

  const dbUser = await prisma.user.findUnique({
    where: {
      username: username,
    },
    include: {
      workoutLogs: true,
    },
  });

  res.render("dashboard", {
    title: "The Workout Tracker - Dashboard",
    isAuthenticated: req.isAuthenticated(),
    user: dbUser
  });
});

module.exports = router;

这条路线有很多神奇的地方。 首先,你使用ensureLoggedIn 中间件来保护这个路由。 然后你可以从请求中提取用户名,并使用Prisma客户端在数据库中查找用户。 由于用户有一个用于其锻炼日志的属性,你可以将用户作为模型传递,并在你将创建的pug视图上解析日志。 这将使视图也能访问到用户的名字。 当然,你可以直接查询workoutLogs 表,并包括分页和搜索等功能,如果这种方法更适合你的工作流程。

现在,将app.js 中的代码替换为以下内容:

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var dashboardRouter = require('./routes/dashboard');

const session = require('express-session');
const passport = require('passport');
const { Strategy } = require('passport-openidconnect');

var app = express();

const { PrismaClient } = require('@prisma/client')

const prisma = new PrismaClient();

require('dotenv').config({path:'./.okta.env'});

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(session({
  secret: 'CanYouLookTheOtherWay',
  resave: false,
  saveUninitialized: true
}));

app.use(passport.initialize());
app.use(passport.session());


const {OKTA_OAUTH2_CLIENT_ID, OKTA_OAUTH2_CLIENT_SECRET, OKTA_OAUTH2_ISSUER} = process.env;

// set up passport
passport.use('oidc', new Strategy({
  issuer: OKTA_OAUTH2_ISSUER,
  authorizationURL: `${OKTA_OAUTH2_ISSUER}/v1/authorize`,
  tokenURL: `${OKTA_OAUTH2_ISSUER}/v1/token`,
  userInfoURL: `${OKTA_OAUTH2_ISSUER}/v1/userinfo`,
  clientID: OKTA_OAUTH2_CLIENT_ID,
  clientSecret: OKTA_OAUTH2_CLIENT_SECRET,
  callbackURL: 'http://localhost:3000/authorization-code/callback',
  scope: 'openid profile'
}, (issuer, profile, done) => {
  return done(null, profile);
}));

passport.serializeUser((user, next) => {
  next(null, user);
});

passport.deserializeUser((obj, next) => {
  next(null, obj);
});

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/login', passport.authenticate('oidc'));

app.use('/authorization-code/callback',
  passport.authenticate('oidc', { failureRedirect: '/error' }),
  (req, res, next) => {
    res.redirect('/dashboard');
  }
);

app.post('/logout', (req, res) => {
  req.logout();
  req.session.destroy();
  res.redirect('/');
});

app.use('/', indexRouter);
app.use('/dashboard', dashboardRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

你更新了你的路由,以正确使用dashboard 路由,以及Okta将使用的任何登录或注销路由。 你还设置了passport,使用Okta CLI在.okta.env 文件中产生的值来使用Okta。

给你的Express应用程序添加视图

默认情况下,express-generator 会为你添加一些视图。 你将需要编辑它们,并添加一个你自己的新视图。 首先,打开views 目录中的layout.pug 文件,用下面的代码替换其中的内容:

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/output.css')
  body
    nav.flex.items-center.justify-between.flex-wrap.bg-teal-500.p-6.flex.items-center.flex-shrink-0.text-white.mr-6
      span.font-semibold.text-xl.tracking-tight.pr-2 The Workout Tracker
      .w-full.block.flex-grow(class='lg:flex lg:items-center lg:w-auto')
        .text-sm(class='lg:flex-grow')
        div
          if(isAuthenticated)
            form(action="logout" method="POST") 
              button(type="submit").inline-block.text-sm.px-4.py-2.leading-none.border.rounded.text-white.border-white.mt-4 Logout
          else 
            a.inline-block.text-sm.px-4.py-2.leading-none.border.rounded.text-white.border-white.mt-4(href='Login' class='hover:border-transparent hover:text-teal-500 hover:bg-white lg:mt-0') Login
    
    block content

你会注意到这个文件的几件事。 首先,这些类是非常具体和丰富的。 这是典型的Tailwind CSS。 与其说Tailwind CSS定义了一个特定元素的外观,不如说它致力于定义一个特定样式的外观,然后你可以从元素中添加和删除样式,以适应所需的外观。

你将看到的另一件事是登录/注销按钮,它将根据isAuthenticated 属性而改变。

接下来,你可以通过以下方式更新index.pug

extends layout

block content
  .py-12.bg-white
    .max-w-7xl.mx-auto.px-4(class='sm:px-6 lg:px-8')
      div.c(class='lg:text-center')
        p.mt-2.text-3xl.leading-8.font-extrabold.tracking-tight.text-gray-900(class='sm:text-4xl') The Workout Tracker
        p.mt-4.max-w-2xl.text-xl.text-gray-500(class='lg:mx-auto')
          | A small demo built with 
          a(href="https://expressjs.com/") Express on 
          a(href="https://nodejs.org/en/") Node.JS 
          | Secured with 
          a(href="https://developer.okta.com/signup") Okta.
          br          
          | This app uses 
          a(href="https://tailwindcss.com/") Tailwind CSS 
          | for its CSS framework and 
          a(href="https://www.prisma.io/") Prisma.JS 
          | for its ORM.  

这里没有什么可谈的。 只是一个带有更多Tailwind CSS和一些所用技术的链接的闪亮页面。

最后,用以下代码为dashboard.pug 添加一个文件:

extends layout

block content 
    .flex.flex-wrap
        .w-full.p-6(class='md:w-1/2 xl:w-1/3')
            .bg-gradient-to-b.from-green-200.to-green-100.border-b-4.border-green-600.rounded-lg.shadow-xl.p-5
                .flex.flex-row.items-center
                    .flex-shrink.pr-4
                        .rounded-full.p-5.bg-green-600
                            i.fa.fa-wallet.fa-2x.fa-inverse
                    .flex-1.text-right(class='md:text-center')
                        h2.font-bold.uppercase.text-gray-600 Total Minutes Worked
                        p.font-bold.text-3xl
                            span= user.workoutLogs.reduce(function(total, item) { return total + item.minutes }, 0) 
                            span.text-green-500
                                i.fas.fa-caret-up
        .w-full.p-6(class='md:w-1/2 xl:w-1/3')
            .bg-gradient-to-b.from-pink-200.to-pink-100.border-b-4.border-pink-500.rounded-lg.shadow-xl.p-5
                .flex.flex-row.items-center
                    .flex-shrink.pr-4
                        .rounded-full.p-5.bg-pink-600
                            i.fas.fa-users.fa-2x.fa-inverse
                    .flex-1.text-right(class='md:text-center')
                        h2.font-bold.uppercase.text-gray-600 Total Calories Burned
                        p.font-bold.text-3xl
                            span= user.workoutLogs.reduce(function(total, item) { return total + item.calories }, 0)  
                            span.text-pink-500
                                i.fas.fa-exchange-alt
        .w-full.p-6(class='md:w-1/2 xl:w-1/3')
            .bg-gradient-to-b.from-yellow-200.to-yellow-100.border-b-4.border-yellow-600.rounded-lg.shadow-xl.p-5
                .flex.flex-row.items-center
                    .flex-shrink.pr-4
                        .rounded-full.p-5.bg-yellow-600
                            i.fas.fa-user-plus.fa-2x.fa-inverse
                    .flex-1.text-right(class='md:text-center')
                        h2.font-bold.uppercase.text-gray-600 Total Days Worked
                        p.font-bold.text-3xl
                            span= user.workoutLogs.map(r => r.date).filter((date, i, self) => self.findIndex(d => d.getTime() === date.getTime()) === i).length
                            span.text-yellow-600
                                i.fas.fa-caret-up
    .w-full.p-6
        .bg-white.border-transparent.rounded-lg.shadow-xl
            .bg-gradient-to-b.from-gray-300.to-gray-100.uppercase.text-gray-800.border-b-2.border-gray-300.rounded-tl-lg.rounded-tr-lg.p-2
                h2.font-bold.uppercase.text-gray-600 Workout Log
            .p-5
                table.w-full.p-5.text-gray-700
                    thead
                        tr
                            th.text-left.text-blue-900 Date
                            th.text-left.text-blue-900 Execise
                            th.text-left.text-blue-900 Amount
                            th.text-left.text-blue-900 Time
                            th.text-left.text-blue-900 Calories Burned
                    tbody
                        each log in user.workoutLogs.sort((a,b) => a.date - b.date)
                            tr
                                td=log.date.toLocaleDateString()
                                td=log.exercise
                                td=log.amount + ' ' + log.units 
                                td=log.minutes + ' Minutes'
                                td=log.calories                        

正如你之前看到的,这个页面是受保护的。 因此,它要求你有一个用户来访问这个页面。 该应用程序将提取用户的详细资料和锻炼日志,并在页面顶部形成一个摘要,并根据用户所做的锻炼,列出几个日志的表格。

使用Tailwind CSS创建CSS文件

这个过程的最后一部分是创建你的应用程序将使用的实际CSS文件。 在你的public/stylesheets 目录中,你会看到一个名为style.css 的文件。 将该文件中的代码替换为下面的代码:

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  padding: 50px;
  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}

a {
  color: #00B7FF;
}

这大体上是相同的。然而,你正在为Tailwind的每个层导入指令。

接下来,你可以编译你在pug文件中使用的类和你刚刚修改的样式表,以创建一个新的CSS文件。运行下面的命令。

npx tailwindcss -i ./public/stylesheets/style.css -o ./public/stylesheets/output.css --watch

现在你可以打开output.css 文件,看到完整的Tailwind CSS代码。 这里的--watch 参数对本地开发很有帮助。 这将在你每次改变你的一个布局时重建output.css 文件。 如果你不想做任何改变,你可以不考虑这个参数。

测试你的应用程序

现在你已经准备好启动你的应用程序了。 运行以下命令:

npm run start