برنامه نویسی

جایگزینی Google Firebase – یک پشته جایگزین منبع باز

در یک ماه گذشته یا بیشتر، گوگل موفق شد دو گروه متمایز از مردم را عصبانی کند. اولین گروهی که نام دامنه‌ها را مدیریت می‌کنند، چه برای کارفرمایان یا خودشان، با حمله غافلگیرکننده‌ای از طرف Google که سرویس دامنه خود را به SquareSpace فروخته است، مواجه می‌شوند. گروه دیگر بازاریابان دیجیتالی هستند که مجبور به تغییر از Universal Analytics به Google Analytics V4 هستند، که گفته می شود استفاده از آن “به طرز باورنکردنی سختی” است.

از آنجایی که گوگل سنت خوبی در کشتن محصولات دارد، نمی‌توانیم فکر نکنیم نفر بعدی کیست. آیا آن… Firebase 🤔؟

این پست پیش‌بینی زمان ورود Firebase به Google Graveyard است. در واقع، Firebase در آغاز راهگشا بود و به جهان نشان داد که Backend-as-a-Service چیست و هنوز هم ابزاری عالی است. یک دهه بعد، ما یکسری جایگزین داریم. افرادی که با دامنه آشنا هستند احتمالاً قبلاً Supabase را می‌شناختند – BaaS مبتنی بر Postgres که در ابتدا به عنوان جایگزین Firebase قرار گرفت. امروز اجازه دهید جایگزین دیگری را بررسی کنیم که شامل ترکیبی از چندین پروژه OSS محبوب است:

  • Next.js – یک چارچوب تمام پشته برای ساخت برنامه های وب با استفاده از React.js

  • NextAuth – چارچوب احراز هویت منبع باز

  • Prisma – نسل بعدی Node.js/Typescript ORM

  • ZenStack – پریسما را با یک لایه کنترل دسترسی قدرتمند شارژ می کند

برای تسهیل مقایسه بین این دو راه حل، من قصد دارم از یک برنامه وبلاگ نویسی ساده به عنوان مرجع استفاده کنم:

برنامه وبلاگ

الزامات کسب و کار عبارتند از:

  1. ورود/ثبت نام مبتنی بر ایمیل/رمز عبور.
  2. کاربران می توانند برای خود پست ایجاد کنند.
  3. صاحبان پست می توانند پست های خود را به روز کنند/انتشار/لغو انتشار/حذف کنند.
  4. کاربران نمی توانند در پست هایی که به آنها تعلق ندارد تغییراتی ایجاد کنند.
  5. همه کاربرانی که وارد سیستم شده اند می توانند پست های منتشر شده را مشاهده کنند.

بررسی بر روی مقایسه جنبه های زیر متمرکز خواهد بود:

  • 🆔 احراز هویت
  • ✍🏻 مدل سازی داده ها
  • 🔐 کنترل دسترسی
  • 👩🏻‍💻 پرس و جو داده های Frontend

بیا شروع کنیم.

احراز هویت

Firebase یک سرویس احراز هویت را ارائه می دهد که از مجموعه ای غنی از ارائه دهندگان هویت پشتیبانی می کند. احراز هویت مبتنی بر ایمیل/گذرواژه نسبتاً ساده است زیرا خدمات پشتیبان Google بیشتر کارها را برای ما انجام می دهند:

import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';

const auth = getAuth();

async function onSignin(email: string, password: string) {
  try {
    await signInWithEmailAndPassword(auth, email, password);
    Router.push('/');
  } catch (err) {
    alert('Unable to sign in: ' + (err as Error).message);
  }
}
وارد حالت تمام صفحه شوید

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

استفاده از Next.js + NextAuth برای احراز هویت نیاز به کمی کار بیشتری دارد، عمدتاً به این دلیل که برای تداوم حساب‌های کاربری نیاز به راه‌اندازی پشتیبان ذخیره‌سازی دارید. ما از PostgreSQL + Prisma برای لایه داده استفاده می کنیم، بنابراین ابتدا باید طرحواره های داده مرتبط را تعریف کنیم. برای اختصار، من فقط مدل «کاربر» را در زیر نشان دادم و از مدل‌های «حساب» و «جلسه» صرفنظر کردم.

// schema.prisma

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

model Account { ... }

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

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

مرحله بعدی نصب باطن احراز هویت به عنوان یک مسیر API در Next.js است:

// pages/api/auth/[...nextauth].ts

import NextAuth, { type NextAuthOptions } from 'next-auth';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import CredentialsProvider from 'next-auth/providers/credentials';
import { prisma } from '../../../server/db';
import type { PrismaClient } from '@prisma/client';

export const authOptions: NextAuthOptions = {
  ...
  adapter: PrismaAdapter(prisma),
  providers: [
    CredentialsProvider({
      credentials: {
        email: { type: 'email'},
        password: { type: 'password' },
      },
      authorize: authorize(prisma),
    }),
  ],
};

function authorize(prisma: PrismaClient) {
  return async (
    credentials: Record<'email' | 'password', string> | undefined
  ) => {
    // verify email password against database
    ..
  };
}

export default NextAuth(authOptions);
وارد حالت تمام صفحه شوید

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

با قرار دادن اینها، قسمت جلویی بسیار ساده است:

import { signIn } from "next-auth/react";

async function onSignin(email: string, password: string) {
  const result = await signIn("credentials", {
    redirect: false,
    email,
    password,
  });

  if (result?.ok) {
    Router.push("/");
  } else {
    alert("Sign in failed");
  }
}
وارد حالت تمام صفحه شوید

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

مقایسه

احراز هویت Firebase در ظاهر آسان به نظر می رسد، اما بدون محدودیت نیست. مشکل اصلی این است که فضای ذخیره سازی کاربر آن جدا از حافظه اصلی برنامه – Firestore است. در نتیجه، شما فقط می‌توانید تعداد کمی از ویژگی‌های ثابت را روی یک کاربر تنظیم کنید، و نمایه یک کاربر فقط توسط خودش قابل دسترسی است. برای رهایی از محدودیت، باید از Cloud Functions برای گوش دادن به رویدادهای ثبت نام کاربر و ایجاد اسناد کاربر جداگانه در Firestore استفاده کنید، و پیچیدگی های زیادی در حال بازگشت هستند.

مدل سازی داده ها

خب این بحث برانگیزترین موضوع است. طرحواره یا بدون طرحواره؟ SQL یا NoSQL؟ انتخاب های سخت

Firebase یک پایگاه داده NoSQL بدون طرح واره است، که به طور موثر به این معنی است که هیچ مدل سازی رسمی داده وجود ندارد. با این حال، این بدان معنا نیست که در مورد داده های رابطه ای تصمیمات سختی برای گرفتن وجود ندارد:

  • آیا آنها را به عنوان اسناد تو در تو مدل می کنید؟
  • یا زیر مجموعه ها؟
  • یا مجموعه های سطح بالای خواهر و برادر؟
  • هنگامی که به ناچار نیاز به پیوستن به داده ها دارید، آیا به غیرعادی سازی تکیه می کنید یا چندین واکشی و مونتاژ داده را در توابع ابری انجام می دهید؟

هر انتخاب برخی از عملیات ها را آسان تر می کند، اما برخی دیگر را بسیار سخت تر می کند، عمدتاً به دلیل نداشتن ویژگی “پیوستن” بومی.

به برنامه وبلاگ خود بازگردیم، در Firestore، User و Post را با دو مجموعه سطح بالای جداگانه با شکل‌های زیر (با استفاده از رویکرد غیرعادی‌سازی) مدل‌سازی می‌کنیم:

// just a mental model in your head since Firestore is schema-less

// "users" collection
type User {
  id: string;    // references uid on the Firebase auth side
  email: string; // duplicated from Firebase auth
  createdAt: Date;
  updatedAt: Date;
}

// "posts" collection
type Post {
  id: string;          // auto id
  authorId: string;    // post author's uid
  authorEmail: string  // author email denormalized
  title: string;
  published: boolean;
  createdAt: Date;
  updatedAt: Date;
}
وارد حالت تمام صفحه شوید

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

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

// schema.prisma

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  posts Post[]
  ...
}

model Post {
  id String @id() @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt()
  title String
  published Boolean @default(false)
  author User @relation(fields: [authorId], references: [id])
  authorId String
}
وارد حالت تمام صفحه شوید

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

مقایسه

مزیت استفاده از پایگاه داده بدون طرحواره آشکار است – شروع سریع است. شما هر داده ای را که می خواهید از کد خود می نویسید و فروشگاه داده با خوشحالی آنها را بدون نیاز به تعریف یک مدل از قبل می پذیرد. با این حال، نیاز به مدل‌سازی داده‌های رابطه‌ای وجود دارد، صرف نظر از اینکه شما آن را به صورت رسمی انجام دهید یا نه. هنگام استفاده از یک فروشگاه داده مانند Firestore، طرح واره در ذهن شما قرار دارد و این مشکلات بسیاری را به دنبال دارد:

  1. همانطور که برنامه شما تکامل می‌یابد، طرحواره تغییر می‌کند و شما در نهایت نسخه‌های زیادی از همان داده‌ها را در فروشگاه خواهید داشت و باید با حل تفاوت‌های کد مقابله کنید.
  2. از آنجایی که هیچ رابطه رسمی وجود ندارد، هیچ بررسی یکپارچگی وجود ندارد، و داشتن نشانگرهای آویزان در مجموعه داده شما آسان است. باز هم، باید از آنها اجتناب کنید یا آنها را در کد به دقت مدیریت کنید.
  3. پیوستن غیرطبیعی است اما اجتناب ناپذیر است، و متأسفانه با رشد برنامه خود به پیوستن بیشتر و بیشتری نیاز خواهید داشت.

بازگشت به انتخاب مهم SQL در مقابل NoSQL. این برداشت من است:

  • یک طرحواره بدون ابهام برای موفقیت هر برنامه غیر پیش پا افتاده ضروری است. شما یا به فروشگاه داده اجازه می دهید آن را اجرا کند یا به نوعی خودتان آن را مدیریت کنید.
  • پایگاه داده های رابطه ای از نظر مقیاس پذیری بسیار قوی تر از سال های پیش هستند. احتمالاً هرگز به اندازه پایگاه‌های داده NoSQL «مقیاس وب» نمی‌شوند، اما باید از خود بپرسید که آیا مشکل «مقیاس وب» دارید و مطمئناً می‌خواهید بهای آن را بپردازید.
  • انعطاف‌پذیری کدنویسی ارائه شده توسط پایگاه‌های داده بدون طرح‌واره دروغ است. این ناکارآمدی بسیار بیشتر از حل می کند.

افراد بیشتر و بیشتری همین عقیده را دارند، و به همین دلیل است که ما یک رنسانس پایگاه داده رابطه ای داریم.

کنترل دسترسی

Firebase در ادغام کنترل دسترسی به دیتا استور و قرار دادن مستقیم آن در اینترنت پیشگام بود. این کار را با موفقیت انجام داد. شما از قوانین امنیتی برای بیان مجوزهای CRUD اسناد استفاده می کنید. بخش امنیتی دارای یکپارچگی دقیق با احراز هویت Firebase است تا بتوانید هویت کاربر فعلی را در قوانین ارجاع دهید.
الزامات تجاری برنامه وبلاگ ما را می توان به صورت زیر مدل کرد:

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /posts/{post} {
      // published posts are readable to all login users
      allow read: if request.auth != null && resource.data.published == true;

      // all posts are readable to their author
      allow read: if request.auth != null && request.auth.uid == resource.data.authorId;

            // login users can create posts for themselves
      allow create: if request.auth != null && request.auth.uid == request.resource.data.authorId;

      // login users can update posts but cannot change the author of a post
      allow update: if request.auth != null
        && request.auth.uid == resource.data.authorId
        && request.resource.data.authorId == resource.data.authorId;

            // login users can delete their own posts
      allow delete: if request.auth != null
        && request.auth.uid == resource.data.authorId;
    }
  }
}
وارد حالت تمام صفحه شوید

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

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

// schema.zmodel

model Post {
  id String @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title String
  published Boolean @default(false)
  author User @relation(fields: [authorId], references: [id])
  authorId String

  // published posts are readable to all login users
  @@allow('read', auth() != null && published)

  // all posts are readable to their author
  @@allow('read', auth() == author)

  // users can create posts for themselves
  @@allow('create', auth() == author)

  // author can update posts but can't change a post's author
  @@allow('update', auth() == author && auth() == future().author)
}
وارد حالت تمام صفحه شوید

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

سپس می توانید یک سرویس گیرنده Prisma “افزایش یافته” ایجاد کنید که این قوانین را در زمان اجرا اجرا می کند.

مقایسه

در ظاهر، این دو رویکرد شبیه به هم به نظر می رسند، و در واقع، قوانین سیاست ZenStack تا حد زیادی از Firebase الهام گرفته شده است. با این حال، دو تفاوت اساسی وجود دارد:

  1. قوانین امنیتی Firebase فیلترهای ضمنی نیستند، در حالی که ZenStack هستند.

    اگر کل مجموعه «پست‌ها» را در Firestore ساده لوحانه جویا شوید مانند:

    const posts = await getDocs(query(collection(db, 'posts')));
    

    شما یک رد دریافت خواهید کرد زیرا Firebase تشخیص می دهد که مجموعه نتایج قوانین “خواندن” را نقض می کند. شما مسئول اضافه کردن فیلترها در سمت کلاینت هستید تا اطمینان حاصل کنید که پرس و جو کاملاً با قوانین مطابقت دارد. شما اساساً قوانین را تکرار می کنید، و احتمالاً در بسیاری از مکان ها.

    در حالی که خط‌مشی‌های ZenStack فیلترهای خواندن خودکار هستند و پرس و جوی زیر پست‌هایی را برمی‌گرداند که باید برای کاربر فعلی قابل خواندن باشند:

    const posts = await db.Post.findMany();
    
  2. قوانین امنیتی Firestore «متعلق» به Firestore است، در حالی که سیاست‌های دسترسی ZenStack «متعلق» به کد منبع است.

    مردم اغلب قوانین امنیتی Firestore خود را در کنسول مدیریت تغییر می دهند. آنهایی که فرآیندهای بهتری دارند قوانین را در کد منبع نگه می دارند و آنها را در Firebase در طول CI مستقر می کنند. با این حال، حتی با وجود آن، انتشار قوانین مدتی طول می کشد و بلافاصله اعمال نمی شود. آنها بیشتر احساس می کنند که متعلق به سمت سرویس Firebase هستند، نه برنامه شما.

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

پرس و جو داده های Frontend

یکی از بزرگترین مزایای استفاده از Firebase این است که به لطف قوانین امنیتی، می‌توانید پایگاه داده را مستقیماً از سمت کلاینت دستکاری کنید و نیاز به داشتن یک سرویس پشتیبان را که عملیات CRUD را پوشش می‌دهد، کاهش دهید. پرس و جوها و جهش ها بسیار ساده هستند:

const posts = await getDocs(
  query(collection(db, 'posts'),
    or(
      where("published", "==", true),
      where("authorId", "==", user.uid)))
);
وارد حالت تمام صفحه شوید

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

با این حال، در یک frontend مدرن، معمولاً می خواهید از یک کتابخانه پرس و جوی داده مانند SWR یا TanStack Query استفاده کنید تا به شما در مدیریت وضعیت، حافظه پنهان و عدم اعتبار کمک کند. ادغام آنها نیز دشوار نیست. در اینجا یک مثال با SWR آورده شده است:

export function Posts(user: User) {
  const fsQuery = query(
    collection(db, 'posts'),
      or(
        where("published", "==", true),
        where("authorId", "==", user.uid)));

  const { data: posts } = useSWR("posts", async () => {
    const snapshot = await getDocs(fsQuery);
    const data = snapshot.docs.map((doc) => ({
      id: doc.id,
      ...doc.data(),
    }));
    return data;
  });

  return <ul>{posts?.map((post) => (<Post key={post.id} data={post} />))}</ul>;
}
وارد حالت تمام صفحه شوید

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

در راه حل جایگزین ما، می توانیم با استفاده از Next.js + ZenStack به نتیجه بهتری برسیم. ابتدا یک CRUD API خودکار ارائه شده توسط ZenStack به عنوان مسیر Next.js API نصب کنید:

// src/pages/api/model/[...path].ts

import { withPolicy } from "@zenstackhq/runtime";
import { NextRequestHandler } from "@zenstackhq/server/next";
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerAuthSession } from "../../../server/auth";
import { prisma } from "../../../server/db";

async function getPrisma(req: NextApiRequest, res: NextApiResponse) {
  const session = await getServerAuthSession({ req, res });
  // create a wrapper of Prisma client that enforces access policy
  return withPolicy(prisma, { user: session?.user });
}

export default NextRequestHandler({ getPrisma });
وارد حالت تمام صفحه شوید

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

سپس پلاگین SWR را در طرح داده فعال کنید تا قلاب‌های درخواست مشتری را برای مدل‌های ما ایجاد کند:

// schema.zmodel

plugin hooks {
  provider = '@zenstackhq/swr'
  output = "./src/lib/hooks"
}
وارد حالت تمام صفحه شوید

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

سپس از قلاب ها برای پرس و جو از سمت مشتری استفاده کنید:

import { useFindManyPost } from "../lib/hooks";

export function Posts(user: User) {
  // you can use the "include" clause to join the "User" table really easy
  const { data: posts } = useFindManyPost({ include: { author: true } });

  // posts is automatically typed as `Array<Post & { author: User }>`
  return <ul>{posts?.map((post) => (<Post key={post.id} data={post} />))}</ul>;
}
وارد حالت تمام صفحه شوید

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

مقایسه

هر دو راه حل اجازه می دهند تا داده ها را مستقیماً از فرانت اند دستکاری کنید، اما چند تفاوت مهم وجود دارد:

  • نیاز به فیلترینگ

    همانطور که در بخش قبل ذکر شد، از آنجایی که قوانین خط مشی ZenStack مانند فیلترهای ضمنی عمل می کنند، نیازی به کپی کردن قوانین در کد پرس و جو ندارید.

  • ایمنی نوع

    Firebase بدون طرح واره است، بنابراین هیچ راهی برای تولید یا استنباط انواع مدل وجود ندارد. شما یا اصلا تایپ نمی کنید یا به صورت دستی انواع را بر اساس درک خود از شکل داده ها اعلام می کنید. طرح واره Prisma به شدت تایپ شده است تا ZenStack بتواند انواع مدل های کاملاً ایمن و کد قلاب را تولید کند، حتی برای پرس و جوی رابطه ای شامل پویا (فیلد “نویسنده”).

  • پرس و جو رابطه ای

    از آنجایی که هیچ “پیوستن” در Firestore وجود ندارد، ما مجبور شدیم ایمیل نویسنده را در مجموعه “پست ها” از حالت عادی خارج کرده و آن را تکرار کنیم تا آن را ارائه دهیم. با این حال، برای Prisma و پایگاه‌های داده رابطه‌ای، پیوستن مانند یک غریزه اساسی است. شما می توانید آن را به طور طبیعی انجام دهید

نتیجه

Firebase یک نوآوری عالی بود و هنوز هم یک ابزار عالی است. با این حال، بسیاری از چیزها در دهه گذشته تغییر کرده است. زمانی که Firebase متولد شد، TypeScript هنوز در مراحل ابتدایی خود بود و ORM ها چیز کمیاب بودند. اکنون ما به ابزارهای بسیار بهتر و طرز فکرهای بسیار متفاوتی مجهز شده ایم. زمان خوبی برای امتحان کردن چیزهای متفاوت است.


امیدوارم از خواندن لذت برده باشید و مقاله برایتان جالب باشد. ما جعبه ابزار ZenStack را با این باور ساختیم که یک طرحواره قدرتمند می تواند مزایای زیادی را به همراه داشته باشد که ساخت یک برنامه تمام پشته را ساده می کند. اگر این ایده را دوست دارید، صفحه GitHub ما را برای جزئیات بیشتر بررسی کنید!

https://github.com/zenstackhq/zenstack

ستاره من در github

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

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

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

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