راه حل کم کد 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 کاملاً کارآمد داریم.
میتوانید برای دو حساب ثبتنام کنید و بررسی کنید که قوانین کنترل دسترسی طبق انتظار کار میکنند – پستهای پیشنویس فقط برای نویسنده قابل مشاهده است.
امتیاز: حفاظت از رابط کاربری با بررسی کننده مجوز
بیایید یک چالش دیگر به مشکل اضافه کنیم: کاربران برنامه ما دو نقش خواهند داشت:
- خواننده: فقط می تواند پست های منتشر شده را بخواند
- نویسنده: می تواند پست های جدید ایجاد کند
طرح ما باید بر این اساس به روز شود:
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 را با یک لایه کنترل دسترسی قدرتمند، سوپرشارژ می کند و پتانسیل کامل آن را برای توسعه تمام پشته آشکار می کند. اگر از خواندن لذت می برید و احساس می کنید پروژه جالب است، لطفاً به ستاره آن کمک کنید تا افراد بیشتری بتوانند آن را پیدا کنند!