简介
ZenStack是一个TypeScript工具,通过灵活的授权和自动生成的类型安全的 API/钩子来增强 Prisma ORM,从而简化全栈开发
数据库-》应用接口
数据库-》前端
参考官方网站:https://zenstack.dev/
如果我们想做一个全栈开发的web应用程序,之前有选择的是java的jsp页面,后面流行的使用TypeScript,node.js来实现后端业务逻辑,而node.js最流行的ORM框架就是Prisma。
ZenStack 是一个构建在 Prisma 之上的开源工具包 - 最流行的 Node.js ORM。ZenStack 将 Prisma 的功能提升到一个新的水平,并提高了堆栈每一层的开发效率 - 从访问控制到 API 开发,一直到前端。
ZenStack可以做什么,更方便做什么
ZenStack 的一些最常见用例包括:
- 多租户 SaaS
- 具有复杂访问控制要求的应用程序
- CRUD 密集型 API 或 Web 应用程序
ZenStack 对您选择的框架没有主见。它可以与它们中的任何一个一起使用。
特征
具有内置访问控制、数据验证、多态关系等的 ORM
自动生成的CRUD API - RESTful & tRPC
自动生成的 OpenAPI 文档
自动生成的前端数据查询钩子 - SWR & TanStack查询
与流行的身份验证服务和全栈/后端框架集成
具有出色可扩展性的插件系统
出色的能力
后端能力
带访问控制的 ORM:ZenStack 通过强大的访问控制层扩展了 Prisma ORM。通过在数据模型中定义策略,您的 Schema 成为单一事实来源。通过使用启用策略的数据库客户端,您可以享受您已经喜欢的相同 Prisma API,ZenStack 会自动执行访问控制规则。它的核心与框架无关,可以在 Prisma 运行的任何位置运行。
应用程序接口能力
自动 CRUD API:将 API 包装到数据库中是乏味且容易出错的。ZenStack 只需几行代码即可内省架构并将 CRUD API 安装到您选择的框架中。由于内置访问控制支持,API 是完全安全的,可以直接向公众公开。文档呢?打开一个插件,几秒钟内就会生成一个 OpenAPI 规范。
全栈能力
数据查询和变更是前端开发中最难的话题之一。ZenStack 通过生成针对您选择的数据查询库(SWR、TanStack Query 等)的全类型客户端数据访问代码(又名钩子)来简化它。钩子调用自动生成的 API,这些 API 由访问策略保护。
搭建我们的是全栈应用程序
参考官方文档:https://zenstack.dev/docs/quick-start/nextjs-app-router
我们搭建一个专门做crud的全栈程序
前置准备工作
- 确保您已安装 Node.js 18 或更高版本。
- 安装 VSCode 扩展以编辑数据模型。
1、构建应用程序
使用样板创建 Next.js 项目的最简单方法是使用 。运行以下命令以使用 Prisma、NextAuth 和 TailwindCSS 创建新项目。create-t3-app
npx create-t3-app@latest --prisma --nextAuth --tailwind --appRouter --CI my-crud-app
cd my-crud-app
从 中删除相关代码,因为我们不打算使用 Discord 进行身份验证。之后,启动 dev 服务器:DISCORD_CLIENT_IDDISCORD_CLIENT_SECRETsrc/env.js
npm run dev
如果一切正常,您应该在 http://localhost:3000 有一个正在运行的 Next.js 应用程序。
2、初始化 ZenStack 的项目
让我们运行 CLI 来准备您的项目以使用 ZenStack。zenstack
npx zenstack@latest init
3. 准备用于身份验证的 User 模型
首先,在 中,对模型进行一些更改:schema.zmodel User
schema.zmodel
model User {id String @id @default(cuid())name String?email String? @uniqueemailVerified DateTime?password String @password @omitimage String?accounts Account[]sessions Session[]posts Post[]// everyone can signup, and user profile is also publicly readable@@allow('create,read', true)// only the user can update or delete their own profile@@allow('update,delete', auth() == this)
}
4. 将 NextAuth 配置为使用基于凭证的身份验证
/src/server/auth.ts
这里可以改造成通过外部API验证身份信息
也可以先不配置
import { PrismaAdapter } from "@auth/prisma-adapter";
import type { PrismaClient } from "@prisma/client";
import { compare } from "bcryptjs";
import {getServerSession,type DefaultSession,type NextAuthOptions,
} from "next-auth";
import { type Adapter } from "next-auth/adapters";
import CredentialsProvider from "next-auth/providers/credentials";import { db } from "~/server/db";/*** Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`* object and keep type safety.** @see https://next-auth.js.org/getting-started/typescript#module-augmentation*/
declare module "next-auth" {interface Session extends DefaultSession {user: {id: string;} & DefaultSession["user"];}
}/*** Options for NextAuth.js used to configure adapters, providers, callbacks, etc.** @see https://next-auth.js.org/configuration/options*/
export const authOptions: NextAuthOptions = {session: {strategy: "jwt",},callbacks: {session({ session, token }) {if (session.user) {// eslint-disable-next-line @typescript-eslint/no-non-null-assertionsession.user.id = token.sub!;}return session;},},adapter: PrismaAdapter(db) as Adapter,providers: [CredentialsProvider({credentials: {email: { type: "email" },password: { type: "password" },},authorize: authorize(db),}),/*** ...add more providers here.** Most other providers require a bit more work than the Discord provider. For example, the* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account* model. Refer to the NextAuth.js docs for the provider you want to use. Example:** @see https://next-auth.js.org/providers/github*/],
};function authorize(prisma: PrismaClient) {return async (credentials: Record<"email" | "password", string> | undefined,) => {if (!credentials) throw new Error("Missing credentials");if (!credentials.email)throw new Error('"email" is required in credentials');if (!credentials.password)throw new Error('"password" is required in credentials');const maybeUser = await prisma.user.findFirst({where: { email: credentials.email },select: { id: true, email: true, password: true },});if (!maybeUser?.password) return null;// verify the input password with stored hashconst isValid = await compare(credentials.password, maybeUser.password);if (!isValid) return null;return { id: maybeUser.id, email: maybeUser.email };};
}/*** Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.** @see https://next-auth.js.org/configuration/nextjs*/
export const getServerAuthSession = () => getServerSession(authOptions);
5. 挂载 CRUD 服务并生成钩子
ZenStack 内置了对 Next.js 的支持,可以提供数据库 CRUD 服务 自动编写,因此您无需自己编写。
首先安装 、 和 包:@zenstackhq/server@tanstack/react-query@zenstackhq/tanstack-query
npm install @zenstackhq/server@latest @tanstack/react-query
npm install -D @zenstackhq/tanstack-query@latest
让我们将其挂载到终端节点。创建文件并填写以下内容:/api/model/[…path]/src/app/api/model/[…path]/route.ts
/src/app/api/model/[…path]/route.ts
import { enhance } from "@zenstackhq/runtime";
import { NextRequestHandler } from "@zenstackhq/server/next";
import { getServerAuthSession } from "~/server/auth";
import { db } from "~/server/db";// create an enhanced Prisma client with user context
async function getPrisma() {const session = await getServerAuthSession();return enhance(db, { user: session?.user });
}const handler = NextRequestHandler({ getPrisma, useAppDir: true });export {handler as DELETE,handler as GET,handler as PATCH,handler as POST,handler as PUT,
};
该路由现在已准备好访问数据库查询和更改请求。 但是,手动调用该服务将很繁琐。幸运的是,ZenStack 可以 自动生成 React 数据查询钩子。/api/model
让我们通过在顶层将以下代码段添加到 :schema.zmodel
加入post作为增删改查的模型,他的增删改查都是基于模型的API
/schema.zmodel
plugin hooks {provider = '@zenstackhq/tanstack-query'target = 'react'version = 'v5'output = "./src/lib/hooks"
}model Post {id Int @id @default(autoincrement())name StringcreatedAt DateTime @default(now())updatedAt DateTime @updatedAtpublished Boolean @default(false)createdBy User @relation(fields: [createdById], references: [id])createdById String @default(auth().id)@@index([name])// author has full access@@allow('all', auth() == createdBy)// logged-in users can view published posts@@allow('read', auth() != null && published)
}
现在再次运行;你会在 folder 下找到生成的钩子:zenstack generate/src/lib/hooks
注意:每次增加模型都要运行下面命令,让它生产API方法
npx zenstack generate
6、建立页面访问增删改查
现在让我们替换为下面的内容,并使用它来查看和管理帖子。/src/app/page.tsx
"use client";import type { Post } from "@prisma/client";
import { type NextPage } from "next";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {useFindManyPost,useCreatePost,useUpdatePost,useDeletePost,
} from "../lib/hooks";type AuthUser = { id: string; email?: string | null };const Welcome = ({ user }: { user: AuthUser }) => {const router = useRouter();async function onSignout() {await signOut({ redirect: false });router.push("/signin");}return (<div className="flex gap-4"><h3 className="text-lg">Welcome back, {user?.email}</h3><buttonclassName="text-gray-300 underline"onClick={() => void onSignout()}>Signout</button></div>);
};const SigninSignup = () => {return (<div className="flex gap-4 text-2xl"><Link href="/signin" className="rounded-lg border px-4 py-2">Signin</Link><Link href="/signup" className="rounded-lg border px-4 py-2">Signup</Link></div>);
};const Posts = ({ user }: { user: AuthUser }) => {// Post crud hooksconst { mutateAsync: createPost } = useCreatePost();const { mutateAsync: updatePost } = useUpdatePost();const { mutateAsync: deletePost } = useDeletePost();// list all posts that're visible to the current user, together with their authorsconst { data: posts } = useFindManyPost({include: { createdBy: true },orderBy: { createdAt: "desc" },});async function onCreatePost() {const name = prompt("Enter post name");if (name) {await createPost({ data: { name } });}}async function onTogglePublished(post: Post) {await updatePost({where: { id: post.id },data: { published: !post.published },});}async function onDelete(post: Post) {await deletePost({ where: { id: post.id } });}return (<div className="container flex flex-col text-white"><buttonclassName="rounded border border-white p-2 text-lg"onClick={() => void onCreatePost()}>+ Create Post</button><ul className="container mt-8 flex flex-col gap-2">{posts?.map((post) => (<li key={post.id} className="flex items-end justify-between gap-4"><p className={`text-2xl ${!post.published ? "text-gray-400" : ""}`}>{post.name}<span className="text-lg"> by {post.createdBy.email}</span></p><div className="flex w-32 justify-end gap-1 text-left"><buttonclassName="underline"onClick={() => void onTogglePublished(post)}>{post.published ? "Unpublish" : "Publish"}</button><button className="underline" onClick={() => void onDelete(post)}>Delete</button></div></li>))}</ul></div>);
};const Home: NextPage = () => {const { data: session, status } = useSession();if (status === "loading") return <p>Loading ...</p>;return (<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c]"><div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 text-white"><h1 className="text-5xl font-extrabold">My Awesome Blog</h1>{session?.user ? (// welcome & blog posts<div className="flex flex-col"><Welcome user={session.user} /><section className="mt-10"><Posts user={session.user} /></section></div>) : (// if not logged in<SigninSignup />)}</div></main>);
};export default Home;