برنامه نویسی

راه حل کم کد Backend برای Refine.dev با استفاده از Prisma و ZenStack

Refine.dev یک چارچوب بسیار قدرتمند و محبوب مبتنی بر React برای ساخت برنامه های وب با کد کمتر است. تمرکز آن بر ارائه قطعات و قلاب های سطح بالا برای پوشش موارد استفاده رایج مانند احراز هویت، مجوز و CRUD است. یکی از دلایل اصلی محبوبیت آن این است که امکان ادغام آسان با انواع مختلف سیستم های پشتیبان را از طریق طراحی آداپتور انعطاف پذیر فراهم می کند.

این پست بر روی مهمترین نوع یکپارچه سازی تمرکز خواهد کرد: پایگاه داده CRUD. من نشان خواهم داد که چقدر آسان است، با کمک Prisma و ZenStack، شمای پایگاه داده خود را به یک API کاملاً ایمن تبدیل کنید که برنامه را اصلاح کنید. خواهید دید که چگونه با تعریف طرح داده و خط مشی های دسترسی شروع می کنیم، یک CRUD API خودکار از آن استخراج می کنیم و در نهایت با برنامه Refine از طریق «Data Provider» یکپارچه می شویم.

مروری سریع بر ابزارها

پریسما

Prisma یک ORM مدرن TypeScript است که به شما امکان می دهد طرحواره های پایگاه داده را به راحتی مدیریت کنید، پرس و جوها و جهش ها را با انعطاف پذیری زیاد انجام دهید و از ایمنی نوع عالی اطمینان حاصل کنید.

ZenStack

ZenStack یک جعبه ابزار است که در بالای Prisma ساخته شده است که کنترل دسترسی، CRUD وب API خودکار و غیره را اضافه می کند. این ابزار قدرت کامل ORM را برای توسعه تمام پشته آزاد می کند.

Auth.js

Auth.js (جانشین NextAuth) یک کتابخانه احراز هویت انعطاف پذیر است که از بسیاری از ارائه دهندگان و استراتژی های احراز هویت پشتیبانی می کند. اگرچه می‌توانید از بسیاری از سرویس‌های خارجی برای احراز هویت استفاده کنید، ذخیره کردن همه چیز در پایگاه داده‌تان اغلب ساده‌ترین راه برای شروع است.

یک برنامه وبلاگ نویسی

من از یک برنامه وبلاگ نویسی ساده به عنوان مثال برای تسهیل بحث استفاده خواهم کرد. ابتدا روی اجرای احراز هویت و CRUD با کنترل دسترسی ضروری تمرکز می کنیم و سپس به موضوعات پیشرفته تر می پردازیم.

می توانید پیوند مخزن GitHub پروژه تکمیل شده را در انتهای پست پیدا کنید.

داربست کردن برنامه

این create-refine-app CLI چندین الگوی مفید برای داربست یک برنامه جدید ارائه می دهد. ما از “Next.js” استفاده می کنیم تا بتوانیم به راحتی هر دو قسمت جلویی و باطنی را در یک پروژه قرار دهیم. بسیاری از ایده های این پست را می توان در یک پروژه مستقل نیز اعمال کرد.

همچنین باید Prisma و NextAuth را نصب کنیم:

npm install --save-dev prisma
npm install @prisma/client next-auth@beta
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نهایت، ما طرح پایگاه داده را برای برنامه خود ایجاد می کنیم (schema.prisma):

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

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

model User {
  id            String    @id() @default(cuid())
  name          String?
  email         String?   @unique()
  emailVerified DateTime?
  image         String?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt()
  accounts      Account[]
  sessions      Session[]
  password      String
  posts         Post[]
}

model Post {
  id        String   @id() @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt()
  title     String
  content   String
  status    String   @default("draft")
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
}

model Account {
  ...
}

model Session {
  ...
}

model VerificationToken {
  ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این Account، Session، و VerificationToken مدل ها توسط Auth.js مورد نیاز هستند.

احراز هویت ساختمان

تمرکز این پست بر دسترسی به داده ها و کنترل دسترسی خواهد بود. با این حال، آنها فقط با یک سیستم احراز هویت در محل امکان پذیر هستند. ما از احراز هویت ساده مبتنی بر اعتبار در این برنامه استفاده خواهیم کرد. این پیاده‌سازی شامل ایجاد یک پیکربندی Auth.js، نصب یک مسیر API برای رسیدگی به درخواست‌های احراز هویت، و پیاده‌سازی یک «ارائه‌دهنده احراز هویت» را اصلاح می‌کند.

در مورد جزئیات این قسمت توضیحی نمی دهم، اما می توانید کد تکمیل شده را اینجا پیدا کنید. باید قسمت های ثبت نام، ورود به سیستم و مدیریت جلسه کار کند.

کنترل دسترسی را تنظیم کنید

راه های زیادی برای پیاده سازی کنترل دسترسی وجود دارد. افراد معمولاً چک را با کد ضروری در لایه API قرار می دهند. ZenStack یک راه منحصر به فرد و قدرتمند برای انجام آن به صورت اعلامی در طرحواره پایگاه داده ارائه می دهد. بیایید ببینیم چگونه کار می کند.

ابتدا بیایید پروژه را برای ZenStack مقداردهی کنیم:

npx zenstack@latest init
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چند وابستگی و کپی روی آن نصب می کند prisma/schema.prisma فایل به /schema.zmodel. ZModel ابر مجموعه ای از زبان طرحواره Prisma است که ویژگی های بیشتری مانند کنترل دسترسی را اضافه می کند.

در مرحله بعد، قوانین خط مشی را به طرح اضافه می کنیم:

model User {
  ...

  // everybody can signup
  @@allow('create', true)

  // full access by self
  @@allow('all', auth() == this)
}


model Post {
  ...

  // allow read for all signin users
  @@allow('read', auth() != null && status == 'published')

  // full access by author
  @@allow('all', author == auth())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، طرح کلی همچنان بسیار شبیه به طرح اصلی Prisma است. این @@allow دستورالعمل قوانین کنترل دسترسی را تعریف می کند. این auth() تابع کاربر تایید شده فعلی را برمی گرداند. در ادامه خواهیم دید که چگونه با سیستم احراز هویت مرتبط می شود.

ساده ترین راه برای استفاده از ZenStack ایجاد یک بسته بندی “بهبود” در اطراف کلاینت Prisma است. ابتدا، CLI را اجرا کنید تا ماژول های JS تولید کنید که از اجرای سیاست ها پشتیبانی می کنند:

npx zenstack generate
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس، شما می توانید تماس بگیرید enhance API برای ایجاد یک PrismaClient پیشرفته.

const session = await auth();
const user = session?.user?.id ? { id: session.user.id } : undefined;
const db = enhance(prisma, { user });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

علاوه بر prisma به عنوان مثال، enhance تابع همچنین آرگومان دومی را می گیرد که شامل کاربر فعلی است. شی کاربر مقداری را برای auth() فراخوانی تابع در طرحواره در زمان اجرا

PrismaClient بهبودیافته همان API اصلی را دارد، اما قوانین خط مشی را به طور خودکار برای شما اجرا می کند.

CRUD API خودکار

تقویت نمونه ORM با قابلیت های کنترل دسترسی عالی است. تا زمانی که از کلاینت پیشرفته استفاده می کنیم، اکنون می توانیم API های CRUD را بدون نوشتن کد مجوز ضروری پیاده سازی کنیم. با این حال، آیا اگر APIهای CRUD به طور خودکار از این طرح مشتق شوند، جالب‌تر نخواهد بود؟

ZenStack با ارائه مجموعه ای از آداپتورهای سرور برای فریمورک های محبوب Node.js این امکان را فراهم می کند. استفاده از آن با Next.js آسان است. شما فقط باید یک API route handler ایجاد کنید:

// src/app/model/[...path]/route.ts

import { auth } from '@/auth';
import { prisma } from '@/db';
import { enhance } from '@zenstackhq/runtime';
import { NextRequestHandler } from '@zenstackhq/server/next';

// create an enhanced Prisma client with user context
async function getPrisma() {
  const session = await auth();
  const user = session?.user?.id ? { id: session.user.id } : undefined;
  return enhance(prisma, { user });
}

const handler = NextRequestHandler({ getPrisma, useAppDir: true });

export {
    handler as DELETE,
    handler as GET,
    handler as PATCH,
    handler as POST,
    handler as PUT,
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس مجموعه‌ای از APIهای CRUD دارید که در “/api/model/ استفاده می‌شوند.[Model Name]/…”. API ها بسیار شبیه به API PrismaClient هستند:

  • /api/model/post/findMany
  • /api/model/post/create

شما می توانید مشخصات API دقیق را در اینجا بیابید.

پیاده سازی ارائه دهنده داده

ما API های Backend را آماده کرده ایم. اکنون، تنها قطعه‌ای که از دست رفته است، «ارائه‌دهنده داده» را اصلاح کنید، که برای واکشی و به‌روزرسانی داده‌ها با API صحبت می‌کند. قطعه کد زیر نشان می دهد که چگونه getList روش اجرا می شود. ساختار داده‌های ارائه‌دهنده داده Refine از نظر مفهومی بسیار نزدیک به Prisma است و ما فقط باید ترجمه سبک‌وزنی انجام دهیم:

// src/providers/data-provider/index.ts

export const dataProvider: DataProvider = {

  getList: async function <TData extends BaseRecord = BaseRecord>(
      params: GetListParams
  ): Promise<GetListResponse<TData>> {
    const queryArgs: any = {};

    // filtering
    if (params.filters && params.filters.length > 0) {
      const filters = params.filters.map((filter) =>
          transformFilter(filter)
      );
      if (filters.length > 1) {
          queryArgs.where = { AND: filters };
      } else {
          queryArgs.where = filters[0];
      }
    }

    // sorting
    if (params.sorters && params.sorters.length > 0) {
      queryArgs.orderBy = params.sorters.map((sorter) => ({
          [sorter.field]: sorter.order,
      }));
    }

    // pagination
    if (
      params.pagination?.mode === 'server' &&
      params.pagination.current !== undefined &&
      params.pagination.pageSize !== undefined
    ) {
      queryArgs.take = params.pagination.pageSize;
      queryArgs.skip =
          (params.pagination.current - 1) * params.pagination.pageSize;
    }

    // call the API to fetch data and count
    const [data, count] = await Promise.all([
      fetchData(params.resource, '/findMany', queryArgs),
      fetchData(params.resource, '/count', queryArgs),
    ]);

    return { data, total: count };
  },

  ...
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با وجود ارائه‌دهنده داده، اکنون یک رابط کاربری CRUD کاملاً کارآمد داریم.

UI خام

می‌توانید برای دو حساب ثبت‌نام کنید و بررسی کنید که قوانین کنترل دسترسی طبق انتظار کار می‌کنند – پست‌های پیش‌نویس فقط برای نویسنده قابل مشاهده است.

امتیاز: حفاظت از رابط کاربری با بررسی کننده مجوز

بیایید یک چالش دیگر به مشکل اضافه کنیم: کاربران برنامه ما دو نقش خواهند داشت:

  • خواننده: فقط می تواند پست های منتشر شده را بخواند
  • نویسنده: می تواند پست های جدید ایجاد کند

طرح ما باید بر این اساس به روز شود:

model User {
  ...

  role          String    @default('Reader')
}

model Post {
  ...

  // allow read for all signin users
  @@allow('read', auth() != null && status == 'published')

  // allow "Writer" users to create
  @@allow('create', auth().role == 'Writer')

  // full access by author
  @@allow('read,update,delete', author == auth())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون، اگر بخواهید با یک حساب “Reader” یک پست جدید ایجاد کنید، با خطای زیر روبرو خواهید شد:

دسترسی رد شد

عملیات طبق قوانین به درستی رد می شود. با این حال، این یک تجربه کاملا کاربر پسند نیست. خوب است که از ظاهر شدن دکمه “ایجاد” در وهله اول جلوگیری کنید. این را می توان با ترکیب دو ویژگی اضافی Refine و ZenStack به دست آورد:

  • Refine به شما این امکان را می‌دهد که یک «ارائه‌دهنده کنترل دسترسی» را پیاده‌سازی کنید تا تشخیص دهید آیا کاربر فعلی مجوز انجام یک عمل را دارد یا خیر.
  • PrismaClient پیشرفته ZenStack یک ویژگی اضافی دارد check API برای استنباط مجوز بر اساس قوانین خط مشی. این check API همچنین در CRUD API خودکار موجود است.

ZenStack check API پایگاه داده را پرس و جو نمی کند. این بر اساس استنتاج منطقی از قوانین سیاست است. جزئیات بیشتر را اینجا ببینید.

بیایید ببینیم این دو قطعه چگونه در کنار هم قرار می گیرند. ابتدا یک را پیاده سازی کنید AccessControlProvider:

// src/providers/access-control-provider/index.ts

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action }: CanParams): Promise<CanReturnType> => {
    if (action === 'create') {
      // make a request to "/api/model/:resource/check?q={operation:'create'}"
      let url = `/api/model/${resource}/check`;
      url +=
        '?q=' +
        encodeURIComponent(
            JSON.stringify({
                operation: 'create',
            })
        );
      const resp = await fetch(url);
      if (!resp.ok) {
        return { can: false };
      } else {
        const { data } = await resp.json();
        return { can: data };
      }
    }

    return { can: true };
  },

  options: {
    buttons: {
        enableAccessControl: true,
        hideIfUnauthorized: false,
    },
    queryOptions: {},
  },
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس، ارائه دهنده را در سطح بالا ثبت کنید Refine جزء:

// src/app/layout.tsx

<Refine 
  accessControlProvider={ accessControlProvider }
  ...
/>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بلافاصله متوجه این تفاوت خواهید شد که با یک کاربر “Reader”، دکمه “Create” خاکستری و غیرفعال می شود.

دکمه ایجاد غیرفعال است

با این حال، همچنان می‌توانید مستقیماً به URL “/blog-post/create” بروید تا به فرم ایجاد دسترسی داشته باشید. ما می توانیم با استفاده از Refine از آن جلوگیری کنیم CanAccess جزء برای محافظت از آن:

// src/app/blog-post/create/page.tsx

<CanAccess
    resource="post"
    action="create"
    fallback={<div>Not Alloweddiv>}
>
    <Create ... />
CanAccess>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ماموریت انجام شد! ما همچنین این کار را به زیبایی و بدون کدنویسی سخت، هیچ منطق مجوزی در رابط کاربری انجام داده‌ایم. همه چیز در مورد کنترل دسترسی هنوز در طرح ZModel متمرکز است.

نتیجه

Refine.dev یک ابزار عالی برای ایجاد رابط کاربری پیچیده بدون نوشتن کد پیچیده است. در ترکیب با ابرقدرت‌های Prisma و ZenStack، اکنون راه‌حلی کامل و کم‌کد با انعطاف‌پذیری عالی دریافت کرده‌ایم.

نمونه پروژه تکمیل شده اینجاست: https://github.com/ymc9/refine-nextjs-zenstack.


ما در حال ساخت ZenStack هستیم، جعبه ابزاری که Prisma ORM را با یک لایه کنترل دسترسی قدرتمند، سوپرشارژ می کند و پتانسیل کامل آن را برای توسعه تمام پشته آشکار می کند. اگر از خواندن لذت می برید و احساس می کنید پروژه جالب است، لطفاً به ستاره آن کمک کنید تا افراد بیشتری بتوانند آن را پیدا کنند!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا