ZenStack全栈开发工具(一)快速使用指南

1 篇文章 0 订阅
1 篇文章 0 订阅

简介

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的全栈程序

前置准备工作

  1. 确保您已安装 Node.js 18 或更高版本。
  2. 安装 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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值