From 3d39d2679f03f94d4f8a88b96cad3d8bdc2568cb Mon Sep 17 00:00:00 2001 From: Josh Daniel Date: Sun, 11 Aug 2024 20:39:23 +0800 Subject: [PATCH] feat: implement subgraphs for gql schema --- apps/social/src/app/api/graphql/route.ts | 4 ++-- apps/social/src/app/components/feed.tsx | 2 +- apps/social/src/app/components/post-card.tsx | 2 +- apps/www/src/app/api/graphql/route.ts | 4 ++-- packages/gql/package.json | 1 + packages/gql/src/builder.ts | 8 +++++++- packages/gql/src/index.ts | 17 ++++++++++++++--- packages/gql/src/models/message/resolvers.ts | 5 +++++ packages/gql/src/models/message/types.ts | 8 +++++++- packages/gql/src/models/note/resolvers.ts | 5 +++++ packages/gql/src/models/note/types.ts | 4 ++++ packages/gql/src/models/user/resolvers.ts | 5 +++++ packages/gql/src/models/user/types.ts | 4 ++++ pnpm-lock.yaml | 14 ++++++++++++++ 14 files changed, 72 insertions(+), 11 deletions(-) diff --git a/apps/social/src/app/api/graphql/route.ts b/apps/social/src/app/api/graphql/route.ts index 9612093b..a8e3fb31 100644 --- a/apps/social/src/app/api/graphql/route.ts +++ b/apps/social/src/app/api/graphql/route.ts @@ -1,13 +1,13 @@ import { cookies } from "next/headers"; import { createYoga } from "graphql-yoga"; import { getSession, lucia } from "@/lib/auth"; -import { gqlSchema, initContextCache } from "@umamin/gql"; +import { social_schema, initContextCache } from "@umamin/gql"; import { useResponseCache } from "@graphql-yoga/plugin-response-cache"; import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention"; import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection"; const { handleRequest } = createYoga({ - schema: gqlSchema, + schema: social_schema, context: async () => { const { session } = await getSession(); diff --git a/apps/social/src/app/components/feed.tsx b/apps/social/src/app/components/feed.tsx index 83dbd6e5..1a26abd0 100644 --- a/apps/social/src/app/components/feed.tsx +++ b/apps/social/src/app/components/feed.tsx @@ -32,7 +32,7 @@ const data = [ export function Feed() { return (
-
+
{data.map((props) => ( ))} diff --git a/apps/social/src/app/components/post-card.tsx b/apps/social/src/app/components/post-card.tsx index 51902ae7..7e837a17 100644 --- a/apps/social/src/app/components/post-card.tsx +++ b/apps/social/src/app/components/post-card.tsx @@ -22,7 +22,7 @@ type Props = { export function PostCard(props: Props) { return ( -
+
diff --git a/apps/www/src/app/api/graphql/route.ts b/apps/www/src/app/api/graphql/route.ts index 6efa7f35..aaf456a4 100644 --- a/apps/www/src/app/api/graphql/route.ts +++ b/apps/www/src/app/api/graphql/route.ts @@ -1,7 +1,7 @@ import { cookies } from "next/headers"; import { createYoga } from "graphql-yoga"; import { getSession, lucia } from "@/lib/auth"; -import { gqlSchema, initContextCache } from "@umamin/gql"; +import { www_schema, initContextCache } from "@umamin/gql"; import persistedOperations from "@/persisted-operations.json"; import { useResponseCache } from "@graphql-yoga/plugin-response-cache"; import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention"; @@ -9,7 +9,7 @@ import { usePersistedOperations } from "@graphql-yoga/plugin-persisted-operation import { useDisableIntrospection } from "@graphql-yoga/plugin-disable-introspection"; const { handleRequest } = createYoga({ - schema: gqlSchema, + schema: www_schema, context: async () => { const { session } = await getSession(); diff --git a/packages/gql/package.json b/packages/gql/package.json index 7d44f907..4503fd02 100644 --- a/packages/gql/package.json +++ b/packages/gql/package.json @@ -19,6 +19,7 @@ "@pothos/core": "^4.0.2", "@pothos/plugin-directives": "^4.0.1", "@pothos/plugin-scope-auth": "^4.0.2", + "@pothos/plugin-sub-graph": "^4.1.0", "@pothos/plugin-zod": "^4.0.2", "@umamin/aes": "workspace:*", "@umamin/db": "workspace:*", diff --git a/packages/gql/src/builder.ts b/packages/gql/src/builder.ts index ad29a920..d7d2eb9e 100644 --- a/packages/gql/src/builder.ts +++ b/packages/gql/src/builder.ts @@ -1,5 +1,6 @@ import SchemaBuilder from "@pothos/core"; import ValidationPlugin from "@pothos/plugin-zod"; +import SubGraphPlugin from "@pothos/plugin-sub-graph"; import ScopeAuthPlugin from "@pothos/plugin-scope-auth"; import DirectivePlugin from "@pothos/plugin-directives"; import { DateResolver, JSONResolver } from "graphql-scalars"; @@ -24,6 +25,7 @@ const builder = new SchemaBuilder<{ AuthScopes: { authenticated: boolean; }; + SubGraphs: "www" | "social"; Objects: { User: SelectUser & { accounts?: SelectAccount[] | null; @@ -57,7 +59,11 @@ const builder = new SchemaBuilder<{ }; }; }>({ - plugins: [ScopeAuthPlugin, DirectivePlugin, ValidationPlugin], + plugins: [ScopeAuthPlugin, DirectivePlugin, ValidationPlugin, SubGraphPlugin], + subGraphs: { + defaultForTypes: [], + fieldsInheritFromTypes: true, + }, scopeAuth: { authScopes: async (ctx) => ({ authenticated: !!ctx.userId, diff --git a/packages/gql/src/index.ts b/packages/gql/src/index.ts index 57ba2336..efd2ac82 100644 --- a/packages/gql/src/index.ts +++ b/packages/gql/src/index.ts @@ -1,8 +1,13 @@ import builder from "./builder"; import { rateLimitDirective } from "graphql-rate-limit-directive"; -builder.queryType({}); -builder.mutationType({}); +builder.queryType({ + subGraphs: ["www", "social"], +}); + +builder.mutationType({ + subGraphs: ["www", "social"], +}); import "./models/user"; import "./models/note"; @@ -11,4 +16,10 @@ import "./models/message"; export { initContextCache } from "@pothos/core"; const { rateLimitDirectiveTransformer } = rateLimitDirective(); -export const gqlSchema = rateLimitDirectiveTransformer(builder.toSchema()); + +export const www_schema = rateLimitDirectiveTransformer( + builder.toSchema({ subGraph: "www" }) +); +export const social_schema = rateLimitDirectiveTransformer( + builder.toSchema({ subGraph: "social" }) +); diff --git a/packages/gql/src/models/message/resolvers.ts b/packages/gql/src/models/message/resolvers.ts index 0fd7544c..9a739d8e 100644 --- a/packages/gql/src/models/message/resolvers.ts +++ b/packages/gql/src/models/message/resolvers.ts @@ -10,6 +10,7 @@ import { CreateMessageInput, MessagesFromCursorInput } from "./types"; builder.queryFields((t) => ({ messages: t.field({ type: ["Message"], + subGraphs: ["www"], nullable: false, authScopes: { authenticated: true }, directives: { @@ -71,6 +72,7 @@ builder.queryFields((t) => ({ messagesFromCursor: t.field({ type: "MessagesWithCursor", + subGraphs: ["www"], nullable: false, authScopes: { authenticated: true }, directives: { @@ -155,6 +157,7 @@ builder.queryFields((t) => ({ builder.mutationFields((t) => ({ createMessage: t.field({ type: "Message", + subGraphs: ["www"], directives: { rateLimit: { limit: 3, duration: 20 }, }, @@ -182,6 +185,7 @@ builder.mutationFields((t) => ({ createReply: t.field({ type: "String", + subGraphs: ["www"], authScopes: { authenticated: true }, directives: { rateLimit: { limit: 3, duration: 20 }, @@ -214,6 +218,7 @@ builder.mutationFields((t) => ({ deleteMessage: t.field({ type: "String", + subGraphs: ["www"], authScopes: { authenticated: true }, directives: { rateLimit: { limit: 3, duration: 20 }, diff --git a/packages/gql/src/models/message/types.ts b/packages/gql/src/models/message/types.ts index 46d98daf..875e9be9 100644 --- a/packages/gql/src/models/message/types.ts +++ b/packages/gql/src/models/message/types.ts @@ -1,6 +1,7 @@ import builder from "../../builder"; builder.objectType("Message", { + subGraphs: ["www"], fields: (t) => ({ id: t.exposeID("id", { nullable: false }), question: t.exposeString("question", { nullable: false }), @@ -16,6 +17,7 @@ builder.objectType("Message", { }); builder.objectType("MessageCursor", { + subGraphs: ["www"], fields: (t) => ({ id: t.exposeString("id"), createdAt: t.exposeInt("createdAt"), @@ -23,6 +25,7 @@ builder.objectType("MessageCursor", { }); builder.objectType("MessagesWithCursor", { + subGraphs: ["www"], fields: (t) => ({ hasMore: t.exposeBoolean("hasMore", { nullable: false }), cursor: t.expose("cursor", { type: "MessageCursor" }), @@ -31,6 +34,7 @@ builder.objectType("MessagesWithCursor", { }); export const CreateMessageInput = builder.inputType("CreateMessageInput", { + subGraphs: ["www"], fields: (t) => ({ question: t.string({ required: true, @@ -46,6 +50,7 @@ export const CreateMessageInput = builder.inputType("CreateMessageInput", { }); const CursorInput = builder.inputType("CursorInput", { + subGraphs: ["www"], fields: (t) => ({ id: t.string(), createdAt: t.int(), @@ -55,9 +60,10 @@ const CursorInput = builder.inputType("CursorInput", { export const MessagesFromCursorInput = builder.inputType( "MessagesFromCursorInput", { + subGraphs: ["www"], fields: (t) => ({ type: t.string({ required: true }), cursor: t.field({ type: CursorInput }), }), - }, + } ); diff --git a/packages/gql/src/models/note/resolvers.ts b/packages/gql/src/models/note/resolvers.ts index 809bc59f..518bef60 100644 --- a/packages/gql/src/models/note/resolvers.ts +++ b/packages/gql/src/models/note/resolvers.ts @@ -9,6 +9,7 @@ import { NotesFromCursorInput } from "./types"; builder.queryFields((t) => ({ note: t.field({ type: "Note", + subGraphs: ["www"], authScopes: { authenticated: true }, directives: { rateLimit: { limit: 5, duration: 20 }, @@ -33,6 +34,7 @@ builder.queryFields((t) => ({ notes: t.field({ type: ["Note"], + subGraphs: ["www"], nullable: false, directives: { rateLimit: { limit: 5, duration: 20 }, @@ -65,6 +67,7 @@ builder.queryFields((t) => ({ notesFromCursor: t.field({ type: "NotesWithCursor", + subGraphs: ["www"], nullable: false, directives: { rateLimit: { limit: 5, duration: 20 }, @@ -124,6 +127,7 @@ builder.queryFields((t) => ({ builder.mutationFields((t) => ({ updateNote: t.field({ type: "Note", + subGraphs: ["www"], authScopes: { authenticated: true, }, @@ -172,6 +176,7 @@ builder.mutationFields((t) => ({ deleteNote: t.field({ type: "String", + subGraphs: ["www"], authScopes: { authenticated: true, }, diff --git a/packages/gql/src/models/note/types.ts b/packages/gql/src/models/note/types.ts index 49480f87..cc32ecd2 100644 --- a/packages/gql/src/models/note/types.ts +++ b/packages/gql/src/models/note/types.ts @@ -1,6 +1,7 @@ import builder from "../../builder"; builder.objectType("Note", { + subGraphs: ["www"], fields: (t) => ({ id: t.exposeID("id", { nullable: false }), content: t.exposeString("content", { nullable: false }), @@ -13,6 +14,7 @@ builder.objectType("Note", { }); builder.objectType("NoteCursor", { + subGraphs: ["www"], fields: (t) => ({ id: t.exposeString("id"), updatedAt: t.exposeInt("updatedAt"), @@ -20,6 +22,7 @@ builder.objectType("NoteCursor", { }); builder.objectType("NotesWithCursor", { + subGraphs: ["www"], fields: (t) => ({ hasMore: t.exposeBoolean("hasMore", { nullable: false }), cursor: t.expose("cursor", { type: "NoteCursor" }), @@ -28,6 +31,7 @@ builder.objectType("NotesWithCursor", { }); export const NotesFromCursorInput = builder.inputType("NotesFromCursorInput", { + subGraphs: ["www"], fields: (t) => ({ id: t.string(), updatedAt: t.int(), diff --git a/packages/gql/src/models/user/resolvers.ts b/packages/gql/src/models/user/resolvers.ts index eeb86dbc..b2d4b455 100644 --- a/packages/gql/src/models/user/resolvers.ts +++ b/packages/gql/src/models/user/resolvers.ts @@ -8,6 +8,7 @@ import { UpdateUserInput } from "./types"; builder.queryFields((t) => ({ user: t.field({ type: "User", + subGraphs: ["www", "social"], authScopes: { authenticated: true, }, @@ -37,6 +38,7 @@ builder.queryFields((t) => ({ userByUsername: t.field({ type: "PublicUser", + subGraphs: ["www", "social"], directives: { rateLimit: { limit: 5, duration: 20 }, }, @@ -61,6 +63,7 @@ builder.queryFields((t) => ({ builder.mutationFields((t) => ({ updateUser: t.field({ type: "String", + subGraphs: ["www", "social"], authScopes: { authenticated: true, }, @@ -94,6 +97,7 @@ builder.mutationFields((t) => ({ updatePicture: t.field({ type: "String", + subGraphs: ["www", "social"], authScopes: { authenticated: true, }, @@ -124,6 +128,7 @@ builder.mutationFields((t) => ({ updateQuietMode: t.field({ type: "String", + subGraphs: ["www", "social"], authScopes: { authenticated: true, }, diff --git a/packages/gql/src/models/user/types.ts b/packages/gql/src/models/user/types.ts index 40ff841d..0cfac798 100644 --- a/packages/gql/src/models/user/types.ts +++ b/packages/gql/src/models/user/types.ts @@ -1,6 +1,7 @@ import builder from "../../builder"; builder.objectType("Account", { + subGraphs: ["www", "social"], fields: (t) => ({ id: t.exposeID("providerUserId", { nullable: false }), email: t.exposeString("email", { nullable: false }), @@ -10,6 +11,7 @@ builder.objectType("Account", { }); builder.objectType("User", { + subGraphs: ["www", "social"], fields: (t) => ({ id: t.exposeID("id", { nullable: false }), displayName: t.exposeString("displayName"), @@ -26,6 +28,7 @@ builder.objectType("User", { }); builder.objectType("PublicUser", { + subGraphs: ["www", "social"], fields: (t) => ({ id: t.exposeID("id", { nullable: false }), displayName: t.exposeString("displayName"), @@ -39,6 +42,7 @@ builder.objectType("PublicUser", { }); export const UpdateUserInput = builder.inputType("UpdateUserInput", { + subGraphs: ["www", "social"], fields: (t) => ({ username: t.string({ required: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b84d1fb1..093369b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -443,6 +443,9 @@ importers: '@pothos/plugin-scope-auth': specifier: ^4.0.2 version: 4.0.2(@pothos/core@4.0.2(graphql@16.9.0))(graphql@16.9.0) + '@pothos/plugin-sub-graph': + specifier: ^4.1.0 + version: 4.1.0(@pothos/core@4.0.2(graphql@16.9.0))(graphql@16.9.0) '@pothos/plugin-zod': specifier: ^4.0.2 version: 4.0.2(@pothos/core@4.0.2(graphql@16.9.0))(graphql@16.9.0)(zod@3.23.8) @@ -1984,6 +1987,12 @@ packages: '@pothos/core': 4.0.2 graphql: '>=16.6.0' + '@pothos/plugin-sub-graph@4.1.0': + resolution: {integrity: sha512-ZOty3q8t1REyD2VGFXzDoqvBEN8sbjrjTScPayU9hDQ28riIcXhAlNQGv86CJfexm68wRL7RRUw+FgAiQdp9dw==} + peerDependencies: + '@pothos/core': '*' + graphql: '>=16.6.0' + '@pothos/plugin-zod@4.0.2': resolution: {integrity: sha512-1+kx6pIjRkX/CE59ZEFbhvAtK3Ktv0evB8JxmHTc7mB6Ptyhxgm4JHRfc/73MdXzKJJSfRweYE78N/vWIWO4FA==} peerDependencies: @@ -7184,6 +7193,11 @@ snapshots: '@pothos/core': 4.0.2(graphql@16.9.0) graphql: 16.9.0 + '@pothos/plugin-sub-graph@4.1.0(@pothos/core@4.0.2(graphql@16.9.0))(graphql@16.9.0)': + dependencies: + '@pothos/core': 4.0.2(graphql@16.9.0) + graphql: 16.9.0 + '@pothos/plugin-zod@4.0.2(@pothos/core@4.0.2(graphql@16.9.0))(graphql@16.9.0)(zod@3.23.8)': dependencies: '@pothos/core': 4.0.2(graphql@16.9.0)