简介
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? @unique
emailVerified DateTime?
password String @password @omit
image 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-assertion
session.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 hash
const 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 String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
published 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>
<button
className="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 hooks
const { mutateAsync: createPost } = useCreatePost();
const { mutateAsync: updatePost } = useUpdatePost();
const { mutateAsync: deletePost } = useDeletePost();
// list all posts that're visible to the current user, together with their authors
const { 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">
<button
className="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">
<button
className="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;