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

در این مقاله به بررسی ایجاد یک سرور GraphQL در سال 2025 با استفاده از یک پشته مدرن و ایمن می پردازیم. ما گزینه های ابزار ، راه اندازی پس زمینه را با Pothos و Prisma و ادغام Frontend با استفاده از رله پوشش خواهیم داد و مزایای آن را برای تجربه توسعه دهنده و استحکام کاربردی برجسته می کنیم.
اگر GFM Markdown را ترجیح می دهید ، آن را در GitHub بخوانید
بازپرداخت کد
بخش 1: تنظیم مرحله – انتخاب ابزارهای مناسب برای نوع ایمنی
در این بخش الزامات پروژه ، پشته فناوری خاص انتخاب شده و فرایند تصمیم گیری مهم برای انتخاب کتابخانه هایی که از امنیت نوع پایان به پایان اطمینان دارند ، تشریح شده است ، در نهایت منجر به پوتوس و رله می شود.
هنگامی که وظیفه ساخت سرور جدید GraphQL را در سال 2025 انجام داد ، نیازهای اصلی استفاده از:
- node.js
- بیان کردن
- گرافیک
- پریسما
- پس از
- شرح
چالش اصلی شناسایی ابزارهایی بود که یکپارچه ادغام می شوند و ضمانت ایمنی از نوع پایان به پایان را ارائه می دهند. در یک سناریوی ایده آل ، انواع GraphQL مستقیماً از طرح پایگاه داده ای که توسط PRISMA اداره می شود ، به حداقل می رسد و تلاش های هماهنگ سازی دستی را به حداقل می رساند.
پس از ارزیابی چندین گزینه ، انتخاب به دو مدعی اصلی برای ساختن لایه طرحواره GraphQL در بالای Prisma کاهش یافته است:
- Nexus: در حالی که در گذشته یک انتخاب محبوب ، به نظر می رسید که در زمان ارزیابی از آخرین نسخه های PRISMA پشتیبانی نمی کند و آن را مناسب تر می کند.
- typegraphql: استفاده
TypeGraphQL
قبلاً باTypeORM
، من می دانستم که در آن اکوسیستم خوب کار می کند. با این حال ، رویکرد اول طرحواره Prisma با مدل مبتنی بر موجودیت Typeorm تفاوت معنی داری دارد. من مطمئن نبودم که تعریف طرحواره Prisma چگونه با رویکرد سنگین و مبتنی بر طبقاتی محوریت داردTypeGraphQL
بشر - 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 بخوانید
بازپرداخت کد