برنامه نویسی

تجدید نظر در GraphQL در سال 2025: یک پشته از نوع ایمن با Pothos و Relay

در این مقاله به بررسی ایجاد یک سرور GraphQL در سال 2025 با استفاده از یک پشته مدرن و ایمن می پردازیم. ما گزینه های ابزار ، راه اندازی پس زمینه را با Pothos و Prisma و ادغام Frontend با استفاده از رله پوشش خواهیم داد و مزایای آن را برای تجربه توسعه دهنده و استحکام کاربردی برجسته می کنیم.

اگر GFM Markdown را ترجیح می دهید ، آن را در GitHub بخوانید

بازپرداخت کد

بخش 1: تنظیم مرحله – انتخاب ابزارهای مناسب برای نوع ایمنی

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

هنگامی که وظیفه ساخت سرور جدید GraphQL را در سال 2025 انجام داد ، نیازهای اصلی استفاده از:

  • node.js
  • بیان کردن
  • گرافیک
  • پریسما
  • پس از
  • شرح

چالش اصلی شناسایی ابزارهایی بود که یکپارچه ادغام می شوند و ضمانت ایمنی از نوع پایان به پایان را ارائه می دهند. در یک سناریوی ایده آل ، انواع GraphQL مستقیماً از طرح پایگاه داده ای که توسط PRISMA اداره می شود ، به حداقل می رسد و تلاش های هماهنگ سازی دستی را به حداقل می رساند.

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

  1. Nexus: در حالی که در گذشته یک انتخاب محبوب ، به نظر می رسید که در زمان ارزیابی از آخرین نسخه های PRISMA پشتیبانی نمی کند و آن را مناسب تر می کند.
  2. typegraphql: استفاده TypeGraphQL قبلاً با TypeORM، من می دانستم که در آن اکوسیستم خوب کار می کند. با این حال ، رویکرد اول طرحواره Prisma با مدل مبتنی بر موجودیت Typeorm تفاوت معنی داری دارد. من مطمئن نبودم که تعریف طرحواره Prisma چگونه با رویکرد سنگین و مبتنی بر طبقاتی محوریت دارد TypeGraphQLبشر
  3. Pothos: این کتابخانه به دلیل افزونه اختصاصی Prisma ((prisma-pothos-types) ، به طور خاص برای تولید انواع GraphQL به طور مستقیم از طرح PRISMA طراحی شده است. این به نظر می رسید یک مناسب طبیعی برای اهداف پروژه است.

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

بخش 2: اجرای پس زمینه – Pothos ، Prisma و Express Integration

در اینجا ، ما به تنظیمات پشتی عملی می پردازیم. این شامل تعریف مدل های پایگاه داده با PRISMA ، پیکربندی سازنده Schema Pothos ، ادغام آن در یک برنامه اکسپرس با استفاده از GraphQL Yoga و ایجاد انواع GraphQL ، از جمله زمینه های مشتق شده است.

خود این پروژه به عنوان یک شبکه اجتماعی ساده پیش بینی شده بود. برای کوتاه بودن و تمرکز ، ما روی جنبه های خاص GraphQL ، به ویژه در اطراف آن تمرکز خواهیم کرد Post مدل ، حذف دیگ بخار عمومی اکسپرس و Typescript. (تنظیم کامل را می توان در اینجا یافت).

بیایید طرح اصلی Prisma را با تمرکز بر روی User وت Post مدل ها:

generator client {
  provider = "prisma-client-js"
  output   = "./generated/client"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Pothos generator to create types from Prisma models
generator pothos {
  provider = "prisma-pothos-types"
}

model User {
  id            String    @id
  name          String
  email         String    @unique
  emailVerified Boolean
  image         String?
  createdAt     DateTime  @default(now()) // Corrected: Added default
  updatedAt     DateTime  @updatedAt
  sessions      Session[]
  accounts      Account[]

  // Social aspects
  posts         Post[]
  likes         Like[]
  comments      Comment[]
  // Follow relationships
  followers     Follow[]  @relation("following")
  following     Follow[]  @relation("follower")
  role          String?
  banned        Boolean?
  banReason     String?
  banExpires    DateTime?

  apikeys       Apikey[]

  @@map("user")
}

model Post {
  id        String   @id @default(ulid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  content   String
  imageUrl  String?
  // Relations
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId  String
  likes     Like[]
  comments  Comment[]
}

model Like {
  id        String   @id @default(ulid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  // Relations
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId    String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    String

  // Ensure a user can only like a post once
  @@unique([userId, postId])
}
حالت تمام صفحه را وارد کنید

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

در مرحله بعد ، ما سازنده Schema Pothos را پیکربندی می کنیم ، مشتری Prisma را مشخص می کنیم و افزونه هایی مانند پشتیبانی رله را فعال می کنیم:

// Define the generic types for the Pothos builder, including Prisma types and Context
export type PothosBuilderGenericType = { // Corrected Typo: PothosBuilderGenericTYpe -> PothosBuilderGenericType
  PrismaTypes: PrismaTypes;
  Context: {
    currentUser?: Pick<User, "id" | "email" | "name">; // Context includes optional current user
  };
};

// Instantiate the builder with necessary plugins and configurations
export const builder = new SchemaBuilder<PothosBuilderGenericType>({ // Corrected Typo: PothosBuilderGenericTYpe -> PothosBuilderGenericType
  plugins: [PrismaPlugin, RelayPlugin], // Enable Prisma and Relay plugins
  relay: {}, // Basic Relay configuration
  prisma: {
    client: prisma, // Provide the Prisma client instance
    // Expose Prisma schema /// comments as GraphQL descriptions
    exposeDescriptions: true,
    // Use Prisma's filtering capabilities for Relay connection total counts
    filterConnectionTotalCount: true,
    // Warn about unused query parameters during development
    onUnusedQuery: process.env.NODE_ENV === "production" ? null : "warn",
  },
});
حالت تمام صفحه را وارد کنید

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

سپس این سازنده برای تولید طرحواره GraphQL اجرایی استفاده می شود ، که به سرور یوگا GraphQL یکپارچه با Express منتقل می شود:

// graphql/builder.ts
import { lexicographicSortSchema, printSchema } from "graphql";
// Generate the schema object from the Pothos builder
export const pothosSchema = builder.toSchema();

// Export the schema definition as a string (SDL)
export const pothosSchemaString = printSchema(lexicographicSortSchema(pothosSchema));


// index.ts - Server setup
import express from 'express'; // Added import for clarity
import { createYoga } from 'graphql-yoga'; // Added import for clarity
import { fromNodeHeaders } from '@whatwg-node/server'; // Added import for clarity
import { auth } from './auth'; // Assuming auth setup exists
import { prisma } from './prismaClient'; // Assuming prisma client export exists
import { PothosBuilderGenericType, builder, pothosSchema, pothosSchemaString } from './graphql/builder'; // Assuming builder exports exist
// import { PrismaClient, User } from '@prisma/client'; // Assuming Prisma types import

const app = express(); // Added instantiation
const port = process.env.PORT || 4000; // Added port definition

// Configure GraphQL Yoga server
const yoga = createYoga<{
  req: express.Request;
  res: express.Response;
}>({
  // Use Apollo Sandbox for the GraphiQL interface
  renderGraphiQL: () => {
    // HTML to embed Apollo Sandbox
    return `
        
        
          
          
          ${port}/graphql", // Dynamic port
          });
          
          
        `;
  },
  schema: pothosSchema, // Pass the generated Pothos schema
  // Define the context function to inject data (like authenticated user) into resolvers
  context: async (ctx): Promise<PothosBuilderGenericType['Context']> => { // Typed context return
    try {
      const session = await auth.api.getSession({ // Assuming auth setup provides getSession
        headers: fromNodeHeaders(ctx.req.headers),
      });
      if (!session?.user) { // Check specifically for user object in session
        return {
          currentUser: undefined, // Explicitly undefined if no user
        };
      }
      // Provide relevant user details to the context
      return {
        currentUser: {
          id: session.user.id,
          email: session.user.email ?? undefined, // Handle potentially null email
          name: session.user.name ?? undefined,   // Handle potentially null name
        },
      };
    } catch (error) {
      console.error("Error resolving context:", error); // Add error logging
      return { currentUser: undefined };
    }
  },
  graphiql: true, // Enable GraphiQL interface
  logging: true, // Enable logging
  cors: true, // Enable CORS
});

// Bind GraphQL Yoga to the /graphql endpoint
// @ts-expect-error - Yoga types might mismatch slightly with Express middleware types
app.use(yoga.graphqlEndpoint, yoga);

// Define a simple root query required by GraphQL
builder.queryType({
  fields: (t) => ({
    hello: t.string({
      resolve: () => "Hello world!",
    }),
    // Other root queries will be added here...
  }),
});

// Placeholder for other express routes/middleware
// app.get("https://dev.to/", (req, res) => res.send('Server is running!'));

app.listen(port, () => {
  console.log(`🚀 Server ready at http://localhost:${port}/graphql`);
  console.log(`🚀 GraphQL Playground available at http://localhost:${port}/graphql`); // Adjusted log message
});
حالت تمام صفحه را وارد کنید

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

با اجرای سرور اصلی ، می توانیم انواع GraphQL را بر اساس مدل های PRISMA خود تعریف کنیم. پوتوس این کار را ساده می کند. ما می توانیم یک نقشه برداری مستقیم 1 به 1 ایجاد کنیم:

// Example of a simple Post type mapping (Not the final version used)
/*
export const SimplePostType = builder.prismaNode("Post", {
  id: { field: "id" },
  fields: 
    postId: t.exposeString("id", { nullable: false }), // Expose Prisma 'id' as 'postId'
    content: t.exposeString("content"),
    imageUrl: t.exposeString("imageUrl"),
    createdAt: t.exposeString("createdAt", { // Directly expose createdAt as string
      type: "String", // Define the GraphQL type
    }),
    updatedAt: t.exposeString("updatedAt", { // Directly expose updatedAt as string
      type: "String",
    }),
    // Example resolver for the author relation
    postedBy: t.relation("author", {
       type: UserType // Assuming UserType is defined elsewhere
    })
  }),
});
*/
حالت تمام صفحه را وارد کنید

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

با این حال ، برای یک خوراک ، ما اغلب به داده های مشتق شده خاص برای کاربر مشاهده نیاز داریم (به عنوان مثال ، "آیا من این پست را دوست داشته ام؟"). Pothos اجازه می دهد تا تعریف شود انواع مختلف از انواع Prisma یا اضافه کردن زمینه های سفارشی به راحتی. اینجا است FeedPost نوع ترکیب likeCount وت likedByMe:

// Define the Fren (User) type first if not already defined
// Assuming a basic User type 'Fren' exists or is defined similarly
export const Fren = builder.prismaNode("User", { // Example Fren type definition
  id: { field: "id" },
  fields: (t) => ({
    frenId: t.exposeString("id"), // Expose 'id' as 'frenId'
    name: t.exposeString("name"),
    email: t.exposeString("email"),
    image: t.exposeString("image"),
    // Add other user fields as needed
  }),
});


// Define the enhanced FeedPost type using prismaNode and custom fields
export const FeedPost = builder.prismaNode("Post", {
  // Using a variant allows multiple GraphQL types based on the same Prisma model if needed
  // variant: "FeedPost", // Optional: Define a variant name
  id: { field: "id" }, // Map the 'id' field for Relay Node interface
  fields: (t) => ({
    postId: t.exposeString("id", { nullable: false }), // Expose DB 'id' as 'postId'
    content: t.exposeString("content"),
    imageUrl: t.exposeString("imageUrl", { nullable: true }), // Explicitly nullable
    // Custom resolver for ISO string date format
    createdAt: t.field({
      type: "String",
      resolve: (post) => post.createdAt.toISOString(),
    }),
    // Custom resolver for ISO string date format
    updatedAt: t.field({
      type: "String",
      resolve: (post) => post.updatedAt.toISOString(), // Corrected: use updatedAt
    }),
    // Field resolving the User who posted this
    postedBy: t.field({
      type: Fren, // Reference the 'Fren' (User) type
      nullable: false, // Author should always exist
      resolve: async (parent, args, context) => {
        // Fetch the author using the authorId from the parent Post
        const author = await prisma.user.findUnique({
          where: { id: parent.authorId },
        });
        if (!author) {
          // Handle case where author is somehow not found, though schema constraints should prevent this
          throw new Error(`Author not found for post ${parent.id}`);
        }
        return author;
      },
    }),
    // Custom field to calculate the number of likes
    likeCount: t.field({
      type: "Int",
      resolve: async (parent) => {
        // Count likes associated with the parent Post's id
        return prisma.like.count({
          where: { postId: parent.id },
        });
      },
    }),
    // Custom field to check if the current user liked this post
    likedByMe: t.field({
      type: "Boolean",
      resolve: async (parent, args, context) => {
        // If no user is logged in, they haven't liked it
        if (!context.currentUser?.id) return false;
        // Check if a Like record exists for this user and post
        const like = await prisma.like.findUnique({ // Use findUnique for efficiency
          where: {
            userId_postId: { // Use the @@unique constraint defined in Prisma
              userId: context.currentUser.id,
              postId: parent.id,
            }
          },
        });
        // Return true if a like exists, false otherwise
        return !!like;
      },
    }),
  }),
});
حالت تمام صفحه را وارد کنید

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

سرانجام ، ما با استفاده از یاور اتصال رله پوتوس ، یک پرس و جو برای واکشی پست ها اضافه می کنیم (prismaConnection) برای تنظیم خودکار صفحه بندی:

// Extend the root query type with a field to fetch feed posts
builder.queryType({
  fields: (t) => ({
    // ... existing fields like 'hello'
    hello: t.string({ // Keeping the hello query from before
      resolve: () => "Hello world!",
    }),
    // Define the feedPosts query using Relay connections
    feedPosts: t.prismaConnection({
      type: FeedPost, // The type of nodes in the connection
      cursor: "id", // Field used for cursor-based pagination
      resolve: (query, parent, args, context, info) => {
        // Resolve by fetching posts from Prisma, applying connection arguments (like 'first', 'after')
        return prisma.post.findMany({
          ...query, // Spreads Relay arguments (first, after, etc.) into Prisma query
          orderBy: {
            createdAt: "desc", // Order posts by creation date, newest first
          },
        });
      },
    }),
  }),
});
حالت تمام صفحه را وارد کنید

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

بخش 3: ادغام Frontend - مصرف API با قطعات رله

این بخش به جبهه منتقل می شود و نشان می دهد که چگونه می توان از معماری قطعه محور رله استفاده کرد. ما می توانیم از این طرح استفاده کنیم ، قطعات GraphQL را که با اجزای React مستقر شده اند ، تعریف کنیم و از قلاب های رله برای واکشی و نمایش داده ها استفاده کنیم.

پشتیبانی داخلی پوتوس برای اتصالات رله در اینجا بسیار مهم است. در t.prismaConnection Helper به طور خودکار انواع GraphQL لازم را برای صفحه بندی رله ایجاد می کند (مانند QueryFeedPostsConnection وت QueryFeedPostsConnectionEdge) ، صرفه جویی در دیگ قابل توجه.

# Auto-generated GraphQL types by Pothos prismaConnection
type QueryFeedPostsConnection {
  edges: [QueryFeedPostsConnectionEdge] # List of edges (cursor + node)
  pageInfo: PageInfo!                  # Information about the current page
}

type QueryFeedPostsConnectionEdge {
  cursor: String!                       # Opaque cursor for pagination
  node: FeedPost                        # The actual Post data
}

# Standard Relay PageInfo type
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
حالت تمام صفحه را وارد کنید

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

چرا این ساختار مهم است؟ رله به شدت به این مدل اتصال استاندارد برای صفحه بندی کارآمد و واکشی داده ها متکی است.

برای ادغام با Frontend (با فرض تنظیم React با رله پیکربندی شده) ، اولین قدم مشترک برای واکشی آخرین زبان تعریف طرحواره GraphQL (SDL) تولید شده توسط API با پس زمینه ما است. این به کامپایلر رله اجازه می دهد تا نمایش داده ها را تأیید کند و انواع ایجاد کند.

// Example API endpoint to serve the SDL
// (Add this within your Express setup in index.ts or a separate routes file)
app.get("/sdl", (req, res) => {
  res.type("application/graphql").send(pothosSchemaString); // Set content type
});

// Example script on the frontend (e.g., scripts/fetchSdl.ts)
import "dotenv/config"; // If using environment variables for API URL
import fs from "fs/promises";

export async function getSdl() {
  try {
    const apiUrl = process.env.VITE_API_URL || "http://localhost:4000"; // Default or from env
    console.log(`Workspaceing SDL from ${apiUrl}/sdl...`);
    const res = await fetch(`${apiUrl}/sdl`);
    if (!res.ok) {
      throw new Error(`Failed to fetch SDL: ${res.status} ${res.statusText}`);
    }
    const sdl = await res.text();
    await fs.writeFile("./schema.graphql", sdl); // Save to root or specified path
    console.log("✅ SDL fetched and saved to schema.graphql");
  } catch (error) {
    console.error("❌ Error fetching SDL: ", error); // Improved error logging
  }
}

// Run the script (e.g., via package.json script)
getSdl();
حالت تمام صفحه را وارد کنید

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

رله یک اصل "جابجایی" را تشویق می کند: الزامات داده (قطعات) در کنار مؤلفه هایی که از آنها استفاده می کنند تعریف می شوند. این با رویکردهای استراحت سنتی متفاوت است که در آن یک جزء والدین ممکن است تمام داده ها را واگذار کرده و آن را به پایین منتقل کند. در رله ، مؤلفه های برگ نیازهای داده خود را از طریق قطعات تعریف می کنند ، که به سمت بالا در قطعات والدین و در نهایت در یک پرس و جو صفحه واحد تشکیل شده اند.

در اینجا نحوه جستجوی قطعات به دنبال فید اجتماعی ما آمده است:

# src/components/FeedCard.tsx (or similar) - Fragment defining data needed by a single post card
# Naming Convention: ComponentName_propName
export const FeedCardFragment = graphql`
  fragment FeedCard_post on FeedPost {
    id # Global Relay ID
    postId # Our application-specific ID
    content
    imageUrl
    createdAt
    likeCount
    likedByMe
    updatedAt
    postedBy {
      # We can include fragments from other components here too if needed
      # Or specify the fields directly:
      frenId # User's ID (exposed as frenId in our Fren type)
      name
      email
      image
      # Assuming 'amFollowing' fields were added to the 'Fren' type on the backend
      # amFollowing
    }
  }
`;

# src/components/MainFeed.tsx - Fragment defining the list of posts needed by the feed container
export const MainFeedFragment = graphql`
  # Fragment on the Query type, defining arguments for pagination
  fragment MainFeed_feedPosts on Query @argumentDefinitions(
    first: { type: "Int", defaultValue: 10 }, # How many items to fetch
    after: { type: "String" } # Cursor for pagination
  ) {
    # Use the feedPosts connection field defined in our backend query
    feedPosts(first: $first, after: $after) {
      edges {
        node {
          id # Needed for mapping and keys
          ...FeedCard_post # Include the data requirements from the child component
        }
      }
      pageInfo { # Needed for pagination logic
        hasNextPage
        endCursor
      }
    }
  }
`;


# src/pages/FeedPage.tsx (or container component) - The main query for the page
export const MainFeedQuery = graphql`
  # This query includes the MainFeed fragment, passing arguments down
  query MainFeedContainerQuery($first: Int!, $after: String) {
    ...MainFeed_feedPosts @arguments(first: $first, after: $after)
  }
`;

حالت تمام صفحه را وارد کنید

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

توجه: کامپایلر رله به اینها نیاز دارد graphql برچسب های برچسب زده شده در همان پرونده ای که از قطعه یا پرس و جو مربوطه استفاده می کند ، در همان پرونده قرار می گیرند. پس از تعریف این موارد ، کامپایلر رله را اجرا کنید (relay-compiler) برای تولید انواع TypeScript و مصنوعات زمان اجرا.

در اینجا نحوه استفاده از این قطعات و نمایش داده ها در مؤلفه های React آورده شده است:

// src/pages/FeedPage.tsx - Root component for the feed view
import React from 'react';
import { useLazyLoadQuery } from 'react-relay';
import { MainFeedContainerQuery } from './__generated__/MainFeedContainerQuery.graphql'; // Import generated types
import { MainFeedQuery } from './MainFeed'; // Import the query definition
import { MainFeed } from '../components/MainFeed'; // Import the component using the fragment

export function FeedPage() { // Renamed component for clarity
  // Fetch the initial data for the page using the main query
  const queryData = useLazyLoadQuery<MainFeedContainerQuery>(MainFeedQuery, { first: 10 }); // Fetch initial 10 posts

  return (
    <div className="w-full py-4">
      {/* Pass the query data (specifically the part matching the fragment) to the MainFeed component */}
      <MainFeed queryRef={queryData} />
    div>
  );
}


// src/components/MainFeed.tsx - Component rendering the list of posts
import React from 'react';
import { useFragment } from 'react-relay';
import { MainFeed_feedPosts$key } from './__generated__/MainFeed_feedPosts.graphql'; // Import fragment type
import { MainFeedFragment } from './MainFeed'; // Import fragment definition (assuming it's here or imported)
import { PostCard } from "./FeedCard"; // Import the child component

interface FeedProps {
  queryRef: MainFeed_feedPosts$key; // Prop type expects the fragment $key
}

export function MainFeed({ queryRef }: FeedProps) { // Renamed component
  // Use the useFragment hook to read data defined by the MainFeedFragment
  const data = useFragment(
    MainFeedFragment,
    queryRef
  );

  // Extract post nodes safely
  const posts = data?.feedPosts?.edges?.map(edge => edge?.node) ?? []; // Use optional chaining and nullish coalescing

  // Render the list of posts, passing each post's data (as a fragment ref) to PostCard
  return (
    <div className="w-full max-w-2xl mx-auto">
      {posts.map((post) => post && (
        // Pass the individual post fragment reference to the PostCard
        <PostCard key={post.id} postRef={post} />
      ))}
      {/* Pagination controls will be added later */}
    div>
  );
}


// src/components/FeedCard.tsx - Component rendering a single post
import React from 'react';
import { useFragment } from 'react-relay';
import { FeedCard_post$key } from "./__generated__/FeedCard_post.graphql"; // Import fragment type
import { FeedCardFragment } from './FeedCard'; // Import fragment definition (assuming it's here or imported)
// Assuming Card components are imported from a UI library like ShadCN/UI
import { Card, CardContent /* ... other Card parts */ } from '@/components/ui/card';


interface PostCardProps {
  postRef: FeedCard_post$key; // Prop type expects the fragment $key for a single post
  // viewer?: BetterAuthViewer; // Example of passing other props if needed
}

export function PostCard({ postRef }: PostCardProps) { // Removed viewer prop for simplicity
  // Use useFragment to read the data defined by FeedCardFragment
  const postData = useFragment<FeedCard_post$key>(FeedCardFragment, postRef);

  // Early return if postData is somehow null/undefined (though Relay usually prevents this if ref is valid)
  if (!postData) {
    return null;
  }

  // Example: Using derived data or formatting
  // const postIdFirstChars = postData?.id.substring(0, 2).toUpperCase();

  return (
    <Card className="w-full mb-4 border-none bg-base-300">
      <CardContent className="pt-6">
        {/* Display post content, author info, like button, etc. using postData */}
        <p>{postData.content}p>
        {/* ... other card elements ... */}
        <span>Likes: {postData.likeCount}span>
        <span>{postData.likedByMe ? 'You liked this' : 'Like'}span>
      CardContent>
    Card>
  )
}
حالت تمام صفحه را وارد کنید

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

بخش 4: رله پیشرفته - صفحه بندی بدون دردسر و دست زدن به جهش

این بخش پایانی قابلیت های پیشرفته تر رله را که توسط Pothos و Design Relay تسهیل می شود ، پوشش می دهد. ما با استفاده از پیمایش/صفحه بندی بی نهایت پیاده سازی خواهیم کرد usePaginationFragment و نشان می دهد که چگونه رله با مدیریت حافظه نهان اتوماتیک و دستی ، جهش داده ها (به روزرسانی ، ایجاد ، حذف) را انجام می دهد.

در حالی که تنظیمات اولیه پست ها را به دست می آورد ، فیدهای دنیای واقعی نیاز به صفحه بندی دارند (به عنوان مثال ، پیمایش نامحدود یا "بار بیشتر"). رله در اینجا برتری دارد ، به خصوص هنگامی که با زمینه های اتصال پوتوس ترکیب می شود.

اول ، ما اصلاح می کنیم MainFeedFragment تا آن را برای صفحه بندی با استفاده از @refetchable وت @connection بخشنامه ها:

# src/components/MainFeed.tsx - Updated fragment for pagination
export const MainFeedFragment = graphql`
  fragment MainFeed_feedPosts on Query
  # Define arguments for pagination, Relay needs these defined here
  @argumentDefinitions(
    first: { type: "Int", defaultValue: 10 }, # Default items per page
    after: { type: "String" } # Cursor to fetch items after
  )
  # Make this fragment refetchable, generating a MainFeedPaginationQuery
  @refetchable(queryName: "MainFeedPaginationQuery") {
    # Specify the connection field
    feedPosts(first: $first, after: $after)
    # Identify this specific connection in the Relay store
    @connection(key: "MainFeed_feedPosts", filters: []) {
      edges {
        cursor # Needed for pagination
        node {
          id
          ...FeedCard_post # Include child fragment
        }
      }
      pageInfo {
        endCursor
        hasNextPage # Crucial for knowing if more data is available
        # Optional:
        # hasPreviousPage
        # startCursor
      }
    }
  }
`;
حالت تمام صفحه را وارد کنید

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

حالا ، MainFeed مؤلفه می تواند از usePaginationFragment قلاب ارائه شده توسط رله:

// src/components/MainFeed.tsx - Updated component using usePaginationFragment
import React from 'react';
// Import the specific pagination query type generated by @refetchable
import { MainFeedPaginationQuery } from "./__generated__/MainFeedPaginationQuery.graphql";
import { MainFeed_feedPosts$key } from './__generated__/MainFeed_feedPosts.graphql';
import { usePaginationFragment } from 'react-relay';
import { PostCard } from "./FeedCard";
import { MainFeedFragment } from './MainFeed'; // Import fragment definition
// Assuming Button and Loader components are imported
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';

interface FeedProps {
  queryRef: MainFeed_feedPosts$key;
}

export function MainFeed({ queryRef }: FeedProps) {
  // Use usePaginationFragment hook
  const {
    data,       // The accumulated data for the connection
    loadNext,   // Function to load the next page
    hasNext,    // Boolean indicating if more pages exist
    isLoadingNext // Boolean indicating if the next page is currently loading
  } = usePaginationFragment<MainFeedPaginationQuery, MainFeed_feedPosts$key>(
    MainFeedFragment, // The fragment definition
    queryRef         // The fragment reference passed from the parent
  );

  // Function to trigger loading more posts
  const loadMorePosts = () => {
    // Prevent multiple requests or loading if no more data
    if (isLoadingNext || !hasNext) return;
    loadNext(5); // Load the next 5 items (or adjust count as needed)
  };

  // Extract post nodes from the accumulated data
  const posts = data?.feedPosts?.edges?.map(edge => edge?.node) ?? [];

  return (
    <div className="w-full max-w-2xl mx-auto">
      {posts.map((post) => post && (
        <PostCard key={post.id} postRef={post} />
      ))}

      {/* Display a "Load More" button if there's a next page */}
      {hasNext && (
        <div className="flex justify-center my-4">
          <Button onClick={loadMorePosts} variant="outline" disabled={isLoadingNext}>
            {isLoadingNext ? (
              <>
                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                Loading more posts...
              >
            ) : (
              "Load More Posts"
            )}
          Button>
        div>
      )}
    div>
  );
}
حالت تمام صفحه را وارد کنید

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

این تنظیم با حداقل تلاش دستی ، صفحه بندی صاف را فراهم می کند. اگر با استفاده از منطق صفحه بندی با استفاده از کتابخانه هایی مانند Apollo Client ، کشتی گیری کرده اید ، سادگی در اینجا به ویژه قابل توجه است.

در آخر ، بیایید به جهش ها (ایجاد ، به روزرسانی ، حذف داده ها) نگاه کنیم. یکی از ویژگی های عالی رله این است که اگر یک جهش همان قطعه ای را که جهش یافته است بازگرداند (توسط جهانی مشخص شده است id) ، رله اغلب فروشگاه محلی را به طور خودکار به روز می کند.

به عنوان مثال ، یک جهش ویرایش:

# src/components/PostDialogs.tsx (or similar) - Edit Mutation
const editPostMutation = graphql`
  mutation PostDialogsEditMutation($id: ID!, $content: String, $imageUrl: String) { # Use global ID!
    # Assume backend mutation 'updatePost' takes global ID
    updatePost(input: {id: $id, content: $content, imageUrl: $imageUrl}) { # Example input object
      # Return the fragment for the updated post
      updatedPostEdge { # Assuming mutation returns an edge or node
         node {
           ...FeedCard_post # Spreading the fragment triggers automatic update if ID matches
         }
      }
    }
  }
`;
حالت تمام صفحه را وارد کنید

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

با این حال ، برای ایجاد شده موارد جدید یا حذف موارد موجود ، حافظه نهان به طور خودکار نمی داند مورد جدید باید در یک لیست (اتصال) قرار گیرد یا اینکه یک مورد حذف شود. ما باید یک updater عملکرد.

ایجاد یک پست:

# src/components/PostDialogs.tsx - Create Mutation
const createPostMutation = graphql`
  mutation PostDialogsCreateMutation($content: String!, $imageUrl: String) {
    # Assume backend mutation 'createPost' takes content/imageUrl
    createPost(input: { content: $content, imageUrl: $imageUrl }) {
      # Return the fragment for the newly created post, wrapped in an edge
      newPostEdge { # Standard Relay practice to return the new edge
         cursor
         node {
            ...FeedCard_post # Include the fragment data
         }
      }
    }
  }
`;
حالت تمام صفحه را وارد کنید

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

// src/components/PostDialogs.tsx - Usage of create mutation
import { useMutation, ConnectionHandler } from 'react-relay';
import { PostDialogsCreateMutation } from './__generated__/PostDialogsCreateMutation.graphql'; // Generated type

// Inside your component...
const [commitCreateMutation, isCreating] = useMutation<PostDialogsCreateMutation>(createPostMutation);
// Assuming 'setError' state hook exists
const setError = (e: Error | null) => { /* ... */ };
// Assuming PostFormData type exists
type PostFormData = { content: string; imageUrl?: string };

const handleCreateSubmit = (data: PostFormData) => {
  setError(null);
  commitCreateMutation({
    variables: {
      content: data.content,
      imageUrl: data.imageUrl || undefined, // Use undefined if optional
    },
    // Updater function to manually insert the new post into the connection
    updater: (store) => {
      // Get the newly created post edge from the mutation response payload
      const payload = store.getRootField("createPost"); // Matches mutation name
      const newEdge = payload?.getLinkedRecord("newPostEdge"); // Matches field in mutation response

      if (!newEdge) {
        console.error("Failed to get new edge from createPost mutation payload");
        return;
      }

      // Get the connection record from the store
      const root = store.getRoot();
      // Use the connection key defined in the @connection directive
      const connection = ConnectionHandler.getConnection(
        root,
        "MainFeed_feedPosts" // Must match the key in MainFeedFragment @connection
      );

      if (!connection) {
         console.error("Failed to find connection MainFeed_feedPosts in store");
        return;
      }

      // Insert the new edge at the beginning of the connection
      ConnectionHandler.insertEdgeBefore(connection, newEdge);
    },
    onError: (error) => {
       setError(error);
       console.error("Create post failed:", error);
    }
  });
};
حالت تمام صفحه را وارد کنید

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

حذف یک پست:

# src/components/PostDialogs.tsx - Delete Mutation
const deletePostMutation = graphql`
  mutation PostDialogsDeleteMutation($id: ID!) { # Use global ID!
    deletePost(input: { id: $id }) {
      deletedPostId # Return the ID of the deleted post
    }
  }
`;
حالت تمام صفحه را وارد کنید

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

// src/components/PostDialogs.tsx - Usage of delete mutation
import { useMutation, ConnectionHandler } from 'react-relay';
import { PostDialogsDeleteMutation } from './__generated__/PostDialogsDeleteMutation.graphql';

// Inside your component, assuming 'post' object with 'id' (global Relay ID) exists
// const post: { id: string, postId: string /* ... other fields */ };
const [commitDeleteMutation, isDeleting] =
  useMutation<PostDialogsDeleteMutation>(deletePostMutation);
const setError = (e: Error | null) => { /* ... */ };

const handleDeletePost = () => {
  setError(null);

  commitDeleteMutation({
    variables: {
      id: post.id, // Pass the global Relay ID
    },
    // Updater function to remove the node from the connection
    updater: (store) => {
      // Get the ID of the deleted post from the payload
      const payload = store.getRootField("deletePost");
      const deletedId = payload?.getValue("deletedPostId"); // Matches field in mutation

      if (typeof deletedId !== 'string') {
         console.error("Could not get deletedPostId from payload");
         return;
      }

      // Get the connection
      const root = store.getRoot();
      const connection = ConnectionHandler.getConnection(root, "MainFeed_feedPosts");

      if (!connection) {
         console.error("Failed to find connection MainFeed_feedPosts in store");
        return;
      }

      // Remove the node using its ID
      ConnectionHandler.deleteNode(connection, deletedId);
    },
    // Optimistic updater removes the item from the UI immediately
    optimisticUpdater: (store) => {
        const root = store.getRoot();
        const connection = ConnectionHandler.getConnection(root, "MainFeed_feedPosts");
        if (connection) {
          // Remove the node optimistically using its known ID
          ConnectionHandler.deleteNode(connection, post.id);
        }
    },
    onError: (error) => {
       setError(error);
       console.error("Delete post failed:", error);
    }
  });
};
حالت تمام صفحه را وارد کنید

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

با ترکیب Pothos در پس زمینه برای تولید آسان طرحواره و یکپارچه سازی رله با رله در قسمت جلوی داده های قدرتمند ، تکه تکه شدن و قابلیت مدیریت حافظه نهان ، ما به یک مجموعه GraphQL بسیار ایمن و کارآمد برای این پروژه 2025 رسیدیم. هم افزایی بین این ابزارها به طور قابل توجهی تجربه توسعه دهنده را هنگام برخورد با تعامل داده های پیچیده بهبود می بخشد.

اگر GFM Markdown را ترجیح می دهید ، آن را در GitHub بخوانید

بازپرداخت کد

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

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

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

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