From 1b8fa4322f0e9068ebb0d0588b42e5b972d41887 Mon Sep 17 00:00:00 2001 From: KatherinaSl <106989490+KatherinaSl@users.noreply.github.com> Date: Thu, 22 May 2025 14:40:56 +0500 Subject: [PATCH 1/5] feat: implement graphql queries --- .env.example | 1 - src/routes/graphql/index.ts | 12 +- src/routes/graphql/schemas.ts | 185 ++++++++++++++++++++++++ src/routes/graphql/types/memberType.ts | 30 ++++ src/routes/graphql/types/model.ts | 36 +++++ src/routes/graphql/types/postType.ts | 17 +++ src/routes/graphql/types/profileType.ts | 29 ++++ src/routes/graphql/types/userType.ts | 71 +++++++++ 8 files changed, 378 insertions(+), 3 deletions(-) delete mode 100644 .env.example create mode 100644 src/routes/graphql/types/memberType.ts create mode 100644 src/routes/graphql/types/model.ts create mode 100644 src/routes/graphql/types/postType.ts create mode 100644 src/routes/graphql/types/profileType.ts create mode 100644 src/routes/graphql/types/userType.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index 8d87289d7..000000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -FASTIFY_PORT=8000 \ No newline at end of file diff --git a/src/routes/graphql/index.ts b/src/routes/graphql/index.ts index bb974d9c8..338c1313b 100644 --- a/src/routes/graphql/index.ts +++ b/src/routes/graphql/index.ts @@ -1,6 +1,7 @@ import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'; -import { createGqlResponseSchema, gqlResponseSchema } from './schemas.js'; +import { createGqlResponseSchema, gqlResponseSchema, gqlSchema } from './schemas.js'; import { graphql } from 'graphql'; +import { Context } from './types/model.js'; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { const { prisma } = fastify; @@ -15,7 +16,14 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }, }, async handler(req) { - // return graphql(); + return graphql({ + schema: gqlSchema, + source: req.body.query, + variableValues: req.body.variables, + contextValue: { + prisma, + } as Context, + }); }, }); }; diff --git a/src/routes/graphql/schemas.ts b/src/routes/graphql/schemas.ts index 56772d6e7..e4174e624 100644 --- a/src/routes/graphql/schemas.ts +++ b/src/routes/graphql/schemas.ts @@ -1,4 +1,22 @@ import { Type } from '@fastify/type-provider-typebox'; +import { + GraphQLBoolean, + GraphQLFloat, + GraphQLInputObjectType, + GraphQLInputType, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +} from 'graphql'; +import { UUIDType } from './types/uuid.js'; +import { memberType, memberTypeIdEnum } from './types/memberType.js'; +import { Args, Context } from './types/model.js'; +import { userType } from './types/userType.js'; +import { postType } from './types/postType.js'; +import { profileType } from './types/profileType.js'; export const gqlResponseSchema = Type.Partial( Type.Object({ @@ -18,3 +36,170 @@ export const createGqlResponseSchema = { }, ), }; + +export const createUserInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'CreateUserInput', + fields: () => ({ + name: { + type: new GraphQLNonNull(GraphQLString), + }, + balance: { + type: new GraphQLNonNull(GraphQLFloat), + }, + }), +}); + +export const createPostInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'CreatePostInput', + fields: () => ({ + title: { + type: new GraphQLNonNull(GraphQLString), + }, + content: { + type: new GraphQLNonNull(GraphQLString), + }, + authorId: { + type: new GraphQLNonNull(UUIDType), + }, + }), +}); + +export const createProfileInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'CreateProfileInput', + fields: () => ({ + isMale: { + type: new GraphQLNonNull(GraphQLBoolean), + }, + yearOfBirth: { + type: new GraphQLNonNull(GraphQLInt), + }, + userId: { + type: new GraphQLNonNull(UUIDType), + }, + memberTypeId: { + type: new GraphQLNonNull(memberTypeIdEnum), + }, + }), +}); + +const rootQueryType = new GraphQLObjectType({ + name: 'RootQueryType', + fields: { + users: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.user.findMany(); + }, + }, + user: { + type: userType, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.user.findUnique({ + where: { + id, + }, + }); + }, + }, + posts: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(postType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.post.findMany(); + }, + }, + post: { + type: postType, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.post.findUnique({ + where: { + id, + }, + }); + }, + }, + profiles: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(profileType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.profile.findMany(); + }, + }, + profile: { + type: profileType, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.profile.findUnique({ + where: { + id, + }, + }); + }, + }, + memberTypes: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(memberType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.memberType.findMany(); + }, + }, + memberType: { + type: memberType, + args: { + id: { type: new GraphQLNonNull(memberTypeIdEnum) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.memberType.findUnique({ + where: { + id, + }, + }); + }, + }, + }, +}); + +// const mutationType = new GraphQLObjectType({ +// name: 'Mutations', +// fields: { +// createUser: { +// type: new GraphQLNonNull(userType), +// args: { +// dto: { type: new GraphQLNonNull(createUserInput) }, +// }, +// resolve: (_, { dto }) => { +// console.log(dto); +// return createUser(dto); +// }, +// }, +// createProfile: { +// type: new GraphQLNonNull(profileType), +// args: { +// dto: { type: new GraphQLNonNull(createProfileInput) }, +// }, +// resolve: (_, { dto }) => { +// console.log(dto); +// }, +// }, +// createPost: { +// type: new GraphQLNonNull(postType), +// args: { +// dto: { type: new GraphQLNonNull(createPostInput) }, +// }, +// resolve: (_, { dto }) => { +// console.log(dto); +// }, +// }, +// }, +// }); + +export const gqlSchema: GraphQLSchema = new GraphQLSchema({ + types: [userType, createUserInput, createPostInput, createProfileInput], + // mutation: mutationType, + query: rootQueryType, +}); diff --git a/src/routes/graphql/types/memberType.ts b/src/routes/graphql/types/memberType.ts new file mode 100644 index 000000000..64a529014 --- /dev/null +++ b/src/routes/graphql/types/memberType.ts @@ -0,0 +1,30 @@ +import { + GraphQLEnumType, + GraphQLObjectType, + GraphQLNonNull, + GraphQLFloat, + GraphQLInt, +} from 'graphql'; + +export const memberTypeIdEnum = new GraphQLEnumType({ + name: 'MemberTypeId', + values: { + BASIC: { value: 'BASIC' }, + BUSINESS: { value: 'BUSINESS' }, + }, +}); + +export const memberType: GraphQLObjectType = new GraphQLObjectType({ + name: 'MemberType', + fields: () => ({ + id: { + type: new GraphQLNonNull(memberTypeIdEnum), + }, + discount: { + type: new GraphQLNonNull(GraphQLFloat), + }, + postsLimitPerMonth: { + type: new GraphQLNonNull(GraphQLInt), + }, + }), +}); diff --git a/src/routes/graphql/types/model.ts b/src/routes/graphql/types/model.ts new file mode 100644 index 000000000..81787bab3 --- /dev/null +++ b/src/routes/graphql/types/model.ts @@ -0,0 +1,36 @@ +import { PrismaClient } from "@prisma/client"; + +export interface User { + id: string; + name: string; + balance: number; +} + +export interface Post { + id: string; + title: string; + content: string; + authorId: string; +} + +export interface Profile { + id: string; + isMale: boolean; + yearOfBirth: number; + userId: string; + memberTypeId: string; +} + +export interface MemberType { + id: string; + discount: number; + postsLimitPerMonth: number; +} + +export interface Context { + prisma: PrismaClient; +} + +export interface Args { + id: string; +} diff --git a/src/routes/graphql/types/postType.ts b/src/routes/graphql/types/postType.ts new file mode 100644 index 000000000..ddf8e5c0f --- /dev/null +++ b/src/routes/graphql/types/postType.ts @@ -0,0 +1,17 @@ +import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; +import { UUIDType } from './uuid.js'; + +export const postType: GraphQLObjectType = new GraphQLObjectType({ + name: 'PostType', + fields: () => ({ + id: { + type: new GraphQLNonNull(UUIDType), + }, + title: { + type: new GraphQLNonNull(GraphQLString), + }, + content: { + type: new GraphQLNonNull(GraphQLString), + }, + }), +}); diff --git a/src/routes/graphql/types/profileType.ts b/src/routes/graphql/types/profileType.ts new file mode 100644 index 000000000..63a7b0d53 --- /dev/null +++ b/src/routes/graphql/types/profileType.ts @@ -0,0 +1,29 @@ +import { Profile } from '@prisma/client'; +import { GraphQLObjectType, GraphQLNonNull, GraphQLBoolean, GraphQLInt } from 'graphql'; +import { memberType } from './memberType.js'; +import { UUIDType } from './uuid.js'; +import { Context } from './model.js'; + +export const profileType: GraphQLObjectType = new GraphQLObjectType({ + name: 'Profile', + fields: () => ({ + id: { + type: new GraphQLNonNull(UUIDType), + }, + isMale: { + type: new GraphQLNonNull(GraphQLBoolean), + }, + yearOfBirth: { + type: new GraphQLNonNull(GraphQLInt), + }, + memberType: { + type: new GraphQLNonNull(memberType), + resolve: (profile: Profile, _, context: Context) => + context.prisma.memberType.findUnique({ + where: { + id: profile.memberTypeId, + }, + }), + }, + }), +}); diff --git a/src/routes/graphql/types/userType.ts b/src/routes/graphql/types/userType.ts new file mode 100644 index 000000000..eb547c464 --- /dev/null +++ b/src/routes/graphql/types/userType.ts @@ -0,0 +1,71 @@ +import { User } from '@prisma/client'; +import { + GraphQLObjectType, + GraphQLNonNull, + GraphQLString, + GraphQLFloat, + GraphQLList, +} from 'graphql'; +import { postType } from './postType.js'; +import { profileType } from './profileType.js'; +import { UUIDType } from './uuid.js'; +import { Context } from './model.js'; + +export const userType: GraphQLObjectType = new GraphQLObjectType({ + name: 'User', + fields: () => ({ + id: { + type: new GraphQLNonNull(UUIDType), + }, + name: { + type: new GraphQLNonNull(GraphQLString), + }, + balance: { + type: new GraphQLNonNull(GraphQLFloat), + }, + profile: { + type: profileType, + resolve: (user: User, _, context: Context) => + context.prisma.profile.findUnique({ + where: { + userId: user.id, + }, + }), + }, + posts: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(postType))), + resolve: (user: User, _, context: Context) => + context.prisma.post.findMany({ + where: { + authorId: user.id, + }, + }), + }, + userSubscribedTo: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), + resolve: (user: User, _, context: Context) => + context.prisma.user.findMany({ + where: { + subscribedToUser: { + some: { + subscriberId: user.id, + }, + }, + }, + }), + }, + subscribedToUser: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), + resolve: (user: User, _, context: Context) => + context.prisma.user.findMany({ + where: { + userSubscribedTo: { + some: { + authorId: user.id, + }, + }, + }, + }), + }, + }), +}); From 3c910d41852f8fd22bea3165449ba0eef189563e Mon Sep 17 00:00:00 2001 From: KatherinaSl <106989490+KatherinaSl@users.noreply.github.com> Date: Thu, 22 May 2025 17:49:59 +0500 Subject: [PATCH 2/5] feat: implement graphql mutations --- src/routes/graphql/mutation.ts | 186 ++++++++++++++++++++++++ src/routes/graphql/query.ts | 89 ++++++++++++ src/routes/graphql/schemas.ts | 185 +---------------------- src/routes/graphql/types/memberType.ts | 12 +- src/routes/graphql/types/postType.ts | 37 +++-- src/routes/graphql/types/profileType.ts | 44 ++++-- src/routes/graphql/types/userType.ts | 38 +++-- 7 files changed, 366 insertions(+), 225 deletions(-) create mode 100644 src/routes/graphql/mutation.ts create mode 100644 src/routes/graphql/query.ts diff --git a/src/routes/graphql/mutation.ts b/src/routes/graphql/mutation.ts new file mode 100644 index 000000000..fd8ff23d9 --- /dev/null +++ b/src/routes/graphql/mutation.ts @@ -0,0 +1,186 @@ +import { Prisma } from '@prisma/client'; +import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; +import { postType, createPostInput, changePostInput } from './types/postType.js'; +import { + profileType, + createProfileInput, + changeProfileInput, +} from './types/profileType.js'; +import { userType, createUserInput, changeUserInput } from './types/userType.js'; +import { UUIDType } from './types/uuid.js'; +import { Context } from './types/model.js'; + +export const mutationType = new GraphQLObjectType({ + name: 'Mutations', + fields: { + createUser: { + type: new GraphQLNonNull(userType), + args: { + dto: { type: new GraphQLNonNull(createUserInput) }, + }, + resolve: async (_, { dto }, context: Context) => { + return context.prisma.user.create({ + data: dto as Prisma.UserCreateInput, + }); + }, + }, + createProfile: { + type: new GraphQLNonNull(profileType), + args: { + dto: { type: new GraphQLNonNull(createProfileInput) }, + }, + resolve: async (_, { dto }, context: Context) => { + return context.prisma.profile.create({ + data: dto as Prisma.ProfileCreateInput, + }); + }, + }, + createPost: { + type: new GraphQLNonNull(postType), + args: { + dto: { type: new GraphQLNonNull(createPostInput) }, + }, + resolve: async (_, { dto }, context: Context) => { + return context.prisma.post.create({ + data: dto as Prisma.PostCreateInput, + }); + }, + }, + changePost: { + type: new GraphQLNonNull(postType), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(changePostInput) }, + }, + resolve: ( + _, + { id, dto }: { id: string; dto: Prisma.PostUpdateInput }, + context: Context, + ) => { + return context.prisma.post.update({ + where: { id }, + data: dto, + }); + }, + }, + changeProfile: { + type: new GraphQLNonNull(profileType), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(changeProfileInput) }, + }, + resolve: async ( + _, + { id, dto }: { id: string; dto: Prisma.ProfileUpdateInput }, + context: Context, + ) => { + return context.prisma.profile.update({ + where: { id }, + data: dto, + }); + }, + }, + changeUser: { + type: new GraphQLNonNull(userType), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(changeUserInput) }, + }, + resolve: async ( + _, + { id, dto }: { id: string; dto: Prisma.UserUpdateInput }, + context: Context, + ) => { + return context.prisma.user.update({ + where: { id }, + data: dto, + }); + }, + }, + deleteUser: { + type: new GraphQLNonNull(GraphQLString), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: { id: string }, context: Context) => { + return ( + await context.prisma.user.delete({ + where: { id }, + }) + ).id; + }, + }, + deletePost: { + type: new GraphQLNonNull(GraphQLString), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: { id: string }, context: Context) => { + return ( + await context.prisma.post.delete({ + where: { id }, + }) + ).id; + }, + }, + deleteProfile: { + type: new GraphQLNonNull(GraphQLString), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: { id: string }, context: Context) => { + return ( + await context.prisma.profile.delete({ + where: { id }, + }) + ).id; + }, + }, + subscribeTo: { + type: new GraphQLNonNull(GraphQLString), + args: { + userId: { type: new GraphQLNonNull(UUIDType) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async ( + _, + { userId, authorId }: { userId: string; authorId: string }, + context: Context, + ) => { + return ( + await context.prisma.subscribersOnAuthors.create({ + data: { + subscriberId: userId, + authorId, + }, + }) + ).subscriberId; + }, + }, + unsubscribeFrom: { + type: new GraphQLNonNull(GraphQLString), + args: { + userId: { type: new GraphQLNonNull(UUIDType) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async ( + _, + { userId, authorId }: { userId: string; authorId: string }, + context: Context, + ) => { + return ( + await context.prisma.subscribersOnAuthors.delete({ + where: { + subscriberId: userId, + authorId: authorId, + subscriberId_authorId: { + subscriberId: userId, + authorId, + }, + }, + }) + ).authorId; + }, + }, + }, +}); diff --git a/src/routes/graphql/query.ts b/src/routes/graphql/query.ts new file mode 100644 index 000000000..a2ce3d24f --- /dev/null +++ b/src/routes/graphql/query.ts @@ -0,0 +1,89 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'; +import { Args, Context } from './types/model.js'; +import { memberType, memberTypeIdEnum } from './types/memberType.js'; +import { postType } from './types/postType.js'; +import { profileType } from './types/profileType.js'; +import { userType } from './types/userType.js'; +import { UUIDType } from './types/uuid.js'; + +export const rootQueryType = new GraphQLObjectType({ + name: 'RootQueryType', + fields: { + users: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.user.findMany(); + }, + }, + user: { + type: userType, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.user.findUnique({ + where: { + id, + }, + }); + }, + }, + posts: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(postType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.post.findMany(); + }, + }, + post: { + type: postType, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.post.findUnique({ + where: { + id, + }, + }); + }, + }, + profiles: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(profileType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.profile.findMany(); + }, + }, + profile: { + type: profileType, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.profile.findUnique({ + where: { + id, + }, + }); + }, + }, + memberTypes: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(memberType))), + resolve: async (parents, args, context: Context) => { + return context.prisma.memberType.findMany(); + }, + }, + memberType: { + type: memberType, + args: { + id: { type: new GraphQLNonNull(memberTypeIdEnum) }, + }, + resolve: async (_, { id }: Args, context: Context) => { + return context.prisma.memberType.findUnique({ + where: { + id, + }, + }); + }, + }, + }, +}); diff --git a/src/routes/graphql/schemas.ts b/src/routes/graphql/schemas.ts index e4174e624..61303f3e4 100644 --- a/src/routes/graphql/schemas.ts +++ b/src/routes/graphql/schemas.ts @@ -1,22 +1,7 @@ import { Type } from '@fastify/type-provider-typebox'; -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLInputObjectType, - GraphQLInputType, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLSchema, - GraphQLString, -} from 'graphql'; -import { UUIDType } from './types/uuid.js'; -import { memberType, memberTypeIdEnum } from './types/memberType.js'; -import { Args, Context } from './types/model.js'; -import { userType } from './types/userType.js'; -import { postType } from './types/postType.js'; -import { profileType } from './types/profileType.js'; +import { GraphQLSchema } from 'graphql'; +import { mutationType } from './mutation.js'; +import { rootQueryType } from './query.js'; export const gqlResponseSchema = Type.Partial( Type.Object({ @@ -37,169 +22,7 @@ export const createGqlResponseSchema = { ), }; -export const createUserInput: GraphQLInputType = new GraphQLInputObjectType({ - name: 'CreateUserInput', - fields: () => ({ - name: { - type: new GraphQLNonNull(GraphQLString), - }, - balance: { - type: new GraphQLNonNull(GraphQLFloat), - }, - }), -}); - -export const createPostInput: GraphQLInputType = new GraphQLInputObjectType({ - name: 'CreatePostInput', - fields: () => ({ - title: { - type: new GraphQLNonNull(GraphQLString), - }, - content: { - type: new GraphQLNonNull(GraphQLString), - }, - authorId: { - type: new GraphQLNonNull(UUIDType), - }, - }), -}); - -export const createProfileInput: GraphQLInputType = new GraphQLInputObjectType({ - name: 'CreateProfileInput', - fields: () => ({ - isMale: { - type: new GraphQLNonNull(GraphQLBoolean), - }, - yearOfBirth: { - type: new GraphQLNonNull(GraphQLInt), - }, - userId: { - type: new GraphQLNonNull(UUIDType), - }, - memberTypeId: { - type: new GraphQLNonNull(memberTypeIdEnum), - }, - }), -}); - -const rootQueryType = new GraphQLObjectType({ - name: 'RootQueryType', - fields: { - users: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), - resolve: async (parents, args, context: Context) => { - return context.prisma.user.findMany(); - }, - }, - user: { - type: userType, - args: { - id: { type: new GraphQLNonNull(UUIDType) }, - }, - resolve: async (_, { id }: Args, context: Context) => { - return context.prisma.user.findUnique({ - where: { - id, - }, - }); - }, - }, - posts: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(postType))), - resolve: async (parents, args, context: Context) => { - return context.prisma.post.findMany(); - }, - }, - post: { - type: postType, - args: { - id: { type: new GraphQLNonNull(UUIDType) }, - }, - resolve: async (_, { id }: Args, context: Context) => { - return context.prisma.post.findUnique({ - where: { - id, - }, - }); - }, - }, - profiles: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(profileType))), - resolve: async (parents, args, context: Context) => { - return context.prisma.profile.findMany(); - }, - }, - profile: { - type: profileType, - args: { - id: { type: new GraphQLNonNull(UUIDType) }, - }, - resolve: async (_, { id }: Args, context: Context) => { - return context.prisma.profile.findUnique({ - where: { - id, - }, - }); - }, - }, - memberTypes: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(memberType))), - resolve: async (parents, args, context: Context) => { - return context.prisma.memberType.findMany(); - }, - }, - memberType: { - type: memberType, - args: { - id: { type: new GraphQLNonNull(memberTypeIdEnum) }, - }, - resolve: async (_, { id }: Args, context: Context) => { - return context.prisma.memberType.findUnique({ - where: { - id, - }, - }); - }, - }, - }, -}); - -// const mutationType = new GraphQLObjectType({ -// name: 'Mutations', -// fields: { -// createUser: { -// type: new GraphQLNonNull(userType), -// args: { -// dto: { type: new GraphQLNonNull(createUserInput) }, -// }, -// resolve: (_, { dto }) => { -// console.log(dto); -// return createUser(dto); -// }, -// }, -// createProfile: { -// type: new GraphQLNonNull(profileType), -// args: { -// dto: { type: new GraphQLNonNull(createProfileInput) }, -// }, -// resolve: (_, { dto }) => { -// console.log(dto); -// }, -// }, -// createPost: { -// type: new GraphQLNonNull(postType), -// args: { -// dto: { type: new GraphQLNonNull(createPostInput) }, -// }, -// resolve: (_, { dto }) => { -// console.log(dto); -// }, -// }, -// }, -// }); - export const gqlSchema: GraphQLSchema = new GraphQLSchema({ - types: [userType, createUserInput, createPostInput, createProfileInput], - // mutation: mutationType, + mutation: mutationType, query: rootQueryType, }); diff --git a/src/routes/graphql/types/memberType.ts b/src/routes/graphql/types/memberType.ts index 64a529014..9f82c57ef 100644 --- a/src/routes/graphql/types/memberType.ts +++ b/src/routes/graphql/types/memberType.ts @@ -17,14 +17,8 @@ export const memberTypeIdEnum = new GraphQLEnumType({ export const memberType: GraphQLObjectType = new GraphQLObjectType({ name: 'MemberType', fields: () => ({ - id: { - type: new GraphQLNonNull(memberTypeIdEnum), - }, - discount: { - type: new GraphQLNonNull(GraphQLFloat), - }, - postsLimitPerMonth: { - type: new GraphQLNonNull(GraphQLInt), - }, + id: { type: new GraphQLNonNull(memberTypeIdEnum) }, + discount: { type: new GraphQLNonNull(GraphQLFloat) }, + postsLimitPerMonth: { type: new GraphQLNonNull(GraphQLInt) }, }), }); diff --git a/src/routes/graphql/types/postType.ts b/src/routes/graphql/types/postType.ts index ddf8e5c0f..c70f7ab7b 100644 --- a/src/routes/graphql/types/postType.ts +++ b/src/routes/graphql/types/postType.ts @@ -1,17 +1,34 @@ -import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; +import { + GraphQLInputObjectType, + GraphQLInputType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, +} from 'graphql'; import { UUIDType } from './uuid.js'; export const postType: GraphQLObjectType = new GraphQLObjectType({ name: 'PostType', fields: () => ({ - id: { - type: new GraphQLNonNull(UUIDType), - }, - title: { - type: new GraphQLNonNull(GraphQLString), - }, - content: { - type: new GraphQLNonNull(GraphQLString), - }, + id: { type: new GraphQLNonNull(UUIDType) }, + title: { type: new GraphQLNonNull(GraphQLString) }, + content: { type: new GraphQLNonNull(GraphQLString) }, + }), +}); + +export const createPostInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'CreatePostInput', + fields: () => ({ + title: { type: new GraphQLNonNull(GraphQLString) }, + content: { type: new GraphQLNonNull(GraphQLString) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }), +}); + +export const changePostInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'ChangePostInput', + fields: () => ({ + title: { type: GraphQLString }, + content: { type: GraphQLString }, }), }); diff --git a/src/routes/graphql/types/profileType.ts b/src/routes/graphql/types/profileType.ts index 63a7b0d53..ea160097c 100644 --- a/src/routes/graphql/types/profileType.ts +++ b/src/routes/graphql/types/profileType.ts @@ -1,24 +1,25 @@ import { Profile } from '@prisma/client'; -import { GraphQLObjectType, GraphQLNonNull, GraphQLBoolean, GraphQLInt } from 'graphql'; -import { memberType } from './memberType.js'; +import { + GraphQLObjectType, + GraphQLNonNull, + GraphQLBoolean, + GraphQLInt, + GraphQLInputType, + GraphQLInputObjectType, +} from 'graphql'; +import { memberType, memberTypeIdEnum } from './memberType.js'; import { UUIDType } from './uuid.js'; import { Context } from './model.js'; export const profileType: GraphQLObjectType = new GraphQLObjectType({ name: 'Profile', fields: () => ({ - id: { - type: new GraphQLNonNull(UUIDType), - }, - isMale: { - type: new GraphQLNonNull(GraphQLBoolean), - }, - yearOfBirth: { - type: new GraphQLNonNull(GraphQLInt), - }, + id: { type: new GraphQLNonNull(UUIDType) }, + isMale: { type: new GraphQLNonNull(GraphQLBoolean) }, + yearOfBirth: { type: new GraphQLNonNull(GraphQLInt) }, memberType: { type: new GraphQLNonNull(memberType), - resolve: (profile: Profile, _, context: Context) => + resolve: async (profile: Profile, _, context: Context) => context.prisma.memberType.findUnique({ where: { id: profile.memberTypeId, @@ -27,3 +28,22 @@ export const profileType: GraphQLObjectType = new GraphQLObjectType({ }, }), }); + +export const createProfileInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'CreateProfileInput', + fields: () => ({ + isMale: { type: new GraphQLNonNull(GraphQLBoolean) }, + yearOfBirth: { type: new GraphQLNonNull(GraphQLInt) }, + userId: { type: new GraphQLNonNull(UUIDType) }, + memberTypeId: { type: new GraphQLNonNull(memberTypeIdEnum) }, + }), +}); + +export const changeProfileInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'ChangeProfileInput', + fields: () => ({ + isMale: { type: GraphQLBoolean }, + yearOfBirth: { type: GraphQLInt }, + memberTypeId: { type: memberTypeIdEnum }, + }), +}); diff --git a/src/routes/graphql/types/userType.ts b/src/routes/graphql/types/userType.ts index eb547c464..7c4c8b103 100644 --- a/src/routes/graphql/types/userType.ts +++ b/src/routes/graphql/types/userType.ts @@ -5,6 +5,8 @@ import { GraphQLString, GraphQLFloat, GraphQLList, + GraphQLInputObjectType, + GraphQLInputType, } from 'graphql'; import { postType } from './postType.js'; import { profileType } from './profileType.js'; @@ -14,18 +16,12 @@ import { Context } from './model.js'; export const userType: GraphQLObjectType = new GraphQLObjectType({ name: 'User', fields: () => ({ - id: { - type: new GraphQLNonNull(UUIDType), - }, - name: { - type: new GraphQLNonNull(GraphQLString), - }, - balance: { - type: new GraphQLNonNull(GraphQLFloat), - }, + id: { type: new GraphQLNonNull(UUIDType) }, + name: { type: new GraphQLNonNull(GraphQLString) }, + balance: { type: new GraphQLNonNull(GraphQLFloat) }, profile: { type: profileType, - resolve: (user: User, _, context: Context) => + resolve: async (user: User, _, context: Context) => context.prisma.profile.findUnique({ where: { userId: user.id, @@ -34,7 +30,7 @@ export const userType: GraphQLObjectType = new GraphQLObjectType({ }, posts: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(postType))), - resolve: (user: User, _, context: Context) => + resolve: async (user: User, _, context: Context) => context.prisma.post.findMany({ where: { authorId: user.id, @@ -43,7 +39,7 @@ export const userType: GraphQLObjectType = new GraphQLObjectType({ }, userSubscribedTo: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), - resolve: (user: User, _, context: Context) => + resolve: async (user: User, _, context: Context) => context.prisma.user.findMany({ where: { subscribedToUser: { @@ -56,7 +52,7 @@ export const userType: GraphQLObjectType = new GraphQLObjectType({ }, subscribedToUser: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), - resolve: (user: User, _, context: Context) => + resolve: async (user: User, _, context: Context) => context.prisma.user.findMany({ where: { userSubscribedTo: { @@ -69,3 +65,19 @@ export const userType: GraphQLObjectType = new GraphQLObjectType({ }, }), }); + +export const createUserInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'CreateUserInput', + fields: () => ({ + name: { type: new GraphQLNonNull(GraphQLString) }, + balance: { type: new GraphQLNonNull(GraphQLFloat) }, + }), +}); + +export const changeUserInput: GraphQLInputType = new GraphQLInputObjectType({ + name: 'ChangeUserInput', + fields: () => ({ + name: { type: GraphQLString }, + balance: { type: GraphQLFloat }, + }), +}); From fa387769018ca633b4e53f6319fe9247bc6a43b4 Mon Sep 17 00:00:00 2001 From: KatherinaSl <106989490+KatherinaSl@users.noreply.github.com> Date: Thu, 22 May 2025 17:57:47 +0500 Subject: [PATCH 3/5] feat: implement depth limit --- src/routes/graphql/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes/graphql/index.ts b/src/routes/graphql/index.ts index 338c1313b..2440f6e0f 100644 --- a/src/routes/graphql/index.ts +++ b/src/routes/graphql/index.ts @@ -1,7 +1,8 @@ import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'; import { createGqlResponseSchema, gqlResponseSchema, gqlSchema } from './schemas.js'; -import { graphql } from 'graphql'; +import { graphql, parse, validate } from 'graphql'; import { Context } from './types/model.js'; +import depthLimit from 'graphql-depth-limit'; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { const { prisma } = fastify; @@ -16,6 +17,9 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }, }, async handler(req) { + const errors = validate(gqlSchema, parse(req.body.query), [depthLimit(5)]); + if (errors.length) return { errors }; + return graphql({ schema: gqlSchema, source: req.body.query, From 51d33f6dc842b045395d4b34b7a6e4cd374483ae Mon Sep 17 00:00:00 2001 From: KatherinaSl <106989490+KatherinaSl@users.noreply.github.com> Date: Sat, 24 May 2025 18:40:36 +0500 Subject: [PATCH 4/5] feat: implement data loaders --- src/routes/graphql/dataloaders.ts | 82 +++++++++++++++++++++++++ src/routes/graphql/index.ts | 5 +- src/routes/graphql/types/model.ts | 11 +++- src/routes/graphql/types/profileType.ts | 6 +- src/routes/graphql/types/userType.ts | 35 ++--------- 5 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 src/routes/graphql/dataloaders.ts diff --git a/src/routes/graphql/dataloaders.ts b/src/routes/graphql/dataloaders.ts new file mode 100644 index 000000000..d3f2ebf40 --- /dev/null +++ b/src/routes/graphql/dataloaders.ts @@ -0,0 +1,82 @@ +import DataLoader from 'dataloader'; +import { PrismaClient } from '@prisma/client'; + +export function createContext(prisma: PrismaClient) { + return { + // userLoader: new DataLoader(async (userIds) => { + // const users = await prisma.user.findMany({ + // where: { + // id: { in: userIds as string[] }, + // }, + // }); + // return userIds.map((id) => users.find((user) => user.id === id)); + // }), + + postLoader: new DataLoader(async (userIds) => { + console.log('Post findMany called'); + const posts = await prisma.post.findMany({ + where: { + authorId: { in: userIds as string[] }, + }, + }); + return userIds.map((id) => posts.filter((post) => post.authorId === id)); + }), + + profileLoader: new DataLoader(async (userIds) => { + console.log('Profile findMany called'); + const profiles = await prisma.profile.findMany({ + where: { + userId: { in: userIds as string[] }, + }, + }); + return userIds.map((id) => profiles.find((profile) => profile.userId === id)); + }), + + memberTypeLoader: new DataLoader(async (memberTypeIds) => { + console.log('MemberType findMany called'); + + const memberTypes = await prisma.memberType.findMany({ + where: { + id: { in: memberTypeIds as string[] }, + }, + }); + return memberTypeIds.map((id) => + memberTypes.find((memberType) => memberType.id === id), + ); + }), + + userSubscribedToLoader: new DataLoader(async (userIds) => { + console.log('userSubscribedTo findMany called'); + + const userSubscribed = await prisma.subscribersOnAuthors.findMany({ + where: { + subscriberId: { in: userIds as string[] }, + }, + include: { + author: true, + }, + }); + return userIds.map((id) => + userSubscribed + .filter((user) => user.subscriberId === id) + .map((user) => user.author), + ); + }), + + subscribedToUserLoader: new DataLoader(async (userIds) => { + console.log('subscribedToUserLoader findMany called'); + + const subscribers = await prisma.subscribersOnAuthors.findMany({ + where: { + authorId: { in: userIds as string[] }, + }, + include: { + subscriber: true, + }, + }); + return userIds.map((id) => + subscribers.filter((user) => user.authorId === id).map((user) => user.subscriber), + ); + }), + }; +} diff --git a/src/routes/graphql/index.ts b/src/routes/graphql/index.ts index 2440f6e0f..4b7f1ab4d 100644 --- a/src/routes/graphql/index.ts +++ b/src/routes/graphql/index.ts @@ -1,8 +1,8 @@ import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'; import { createGqlResponseSchema, gqlResponseSchema, gqlSchema } from './schemas.js'; import { graphql, parse, validate } from 'graphql'; -import { Context } from './types/model.js'; import depthLimit from 'graphql-depth-limit'; +import { createContext } from './dataloaders.js'; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { const { prisma } = fastify; @@ -26,7 +26,8 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { variableValues: req.body.variables, contextValue: { prisma, - } as Context, + loaders: createContext(prisma), + }, }); }, }); diff --git a/src/routes/graphql/types/model.ts b/src/routes/graphql/types/model.ts index 81787bab3..4d842f216 100644 --- a/src/routes/graphql/types/model.ts +++ b/src/routes/graphql/types/model.ts @@ -1,4 +1,5 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient } from '@prisma/client'; +import DataLoader from 'dataloader'; export interface User { id: string; @@ -29,6 +30,14 @@ export interface MemberType { export interface Context { prisma: PrismaClient; + loaders: { + // userLoader: DataLoader; + postLoader: DataLoader; + profileLoader: DataLoader; + memberTypeLoader: DataLoader; + userSubscribedToLoader: DataLoader; + subscribedToUserLoader: DataLoader; + }; } export interface Args { diff --git a/src/routes/graphql/types/profileType.ts b/src/routes/graphql/types/profileType.ts index ea160097c..ab4713369 100644 --- a/src/routes/graphql/types/profileType.ts +++ b/src/routes/graphql/types/profileType.ts @@ -20,11 +20,7 @@ export const profileType: GraphQLObjectType = new GraphQLObjectType({ memberType: { type: new GraphQLNonNull(memberType), resolve: async (profile: Profile, _, context: Context) => - context.prisma.memberType.findUnique({ - where: { - id: profile.memberTypeId, - }, - }), + context.loaders.memberTypeLoader.load(profile.memberTypeId), }, }), }); diff --git a/src/routes/graphql/types/userType.ts b/src/routes/graphql/types/userType.ts index 7c4c8b103..5afbf99be 100644 --- a/src/routes/graphql/types/userType.ts +++ b/src/routes/graphql/types/userType.ts @@ -21,47 +21,24 @@ export const userType: GraphQLObjectType = new GraphQLObjectType({ balance: { type: new GraphQLNonNull(GraphQLFloat) }, profile: { type: profileType, - resolve: async (user: User, _, context: Context) => - context.prisma.profile.findUnique({ - where: { - userId: user.id, - }, - }), + resolve: async (user: User, _, context: Context) => { + return context.loaders.profileLoader.load(user.id); + }, }, posts: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(postType))), resolve: async (user: User, _, context: Context) => - context.prisma.post.findMany({ - where: { - authorId: user.id, - }, - }), + context.loaders.postLoader.load(user.id), }, userSubscribedTo: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), resolve: async (user: User, _, context: Context) => - context.prisma.user.findMany({ - where: { - subscribedToUser: { - some: { - subscriberId: user.id, - }, - }, - }, - }), + context.loaders.userSubscribedToLoader.load(user.id), }, subscribedToUser: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), resolve: async (user: User, _, context: Context) => - context.prisma.user.findMany({ - where: { - userSubscribedTo: { - some: { - authorId: user.id, - }, - }, - }, - }), + context.loaders.subscribedToUserLoader.load(user.id), }, }), }); From 932fe51c44461abfa2a446d93abac837a932f990 Mon Sep 17 00:00:00 2001 From: KatherinaSl <106989490+KatherinaSl@users.noreply.github.com> Date: Mon, 26 May 2025 13:31:44 +0500 Subject: [PATCH 5/5] feat: implement prime data loader --- src/routes/graphql/dataloaders.ts | 17 ------------- src/routes/graphql/query.ts | 41 ++++++++++++++++++++++++++++--- src/routes/graphql/types/model.ts | 5 ++-- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/routes/graphql/dataloaders.ts b/src/routes/graphql/dataloaders.ts index d3f2ebf40..deb70a102 100644 --- a/src/routes/graphql/dataloaders.ts +++ b/src/routes/graphql/dataloaders.ts @@ -3,17 +3,7 @@ import { PrismaClient } from '@prisma/client'; export function createContext(prisma: PrismaClient) { return { - // userLoader: new DataLoader(async (userIds) => { - // const users = await prisma.user.findMany({ - // where: { - // id: { in: userIds as string[] }, - // }, - // }); - // return userIds.map((id) => users.find((user) => user.id === id)); - // }), - postLoader: new DataLoader(async (userIds) => { - console.log('Post findMany called'); const posts = await prisma.post.findMany({ where: { authorId: { in: userIds as string[] }, @@ -23,7 +13,6 @@ export function createContext(prisma: PrismaClient) { }), profileLoader: new DataLoader(async (userIds) => { - console.log('Profile findMany called'); const profiles = await prisma.profile.findMany({ where: { userId: { in: userIds as string[] }, @@ -33,8 +22,6 @@ export function createContext(prisma: PrismaClient) { }), memberTypeLoader: new DataLoader(async (memberTypeIds) => { - console.log('MemberType findMany called'); - const memberTypes = await prisma.memberType.findMany({ where: { id: { in: memberTypeIds as string[] }, @@ -46,8 +33,6 @@ export function createContext(prisma: PrismaClient) { }), userSubscribedToLoader: new DataLoader(async (userIds) => { - console.log('userSubscribedTo findMany called'); - const userSubscribed = await prisma.subscribersOnAuthors.findMany({ where: { subscriberId: { in: userIds as string[] }, @@ -64,8 +49,6 @@ export function createContext(prisma: PrismaClient) { }), subscribedToUserLoader: new DataLoader(async (userIds) => { - console.log('subscribedToUserLoader findMany called'); - const subscribers = await prisma.subscribersOnAuthors.findMany({ where: { authorId: { in: userIds as string[] }, diff --git a/src/routes/graphql/query.ts b/src/routes/graphql/query.ts index a2ce3d24f..60984dc73 100644 --- a/src/routes/graphql/query.ts +++ b/src/routes/graphql/query.ts @@ -1,18 +1,53 @@ import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'; -import { Args, Context } from './types/model.js'; +import { Args, Context, User } from './types/model.js'; import { memberType, memberTypeIdEnum } from './types/memberType.js'; import { postType } from './types/postType.js'; import { profileType } from './types/profileType.js'; import { userType } from './types/userType.js'; import { UUIDType } from './types/uuid.js'; +import { + parseResolveInfo, + ResolveTree, + simplifyParsedResolveInfoFragmentWithType, +} from 'graphql-parse-resolve-info'; export const rootQueryType = new GraphQLObjectType({ name: 'RootQueryType', fields: { users: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(userType))), - resolve: async (parents, args, context: Context) => { - return context.prisma.user.findMany(); + resolve: async (parents, args, context: Context, resolveInfo) => { + const parsedResolveInfoFragment = parseResolveInfo(resolveInfo) as ResolveTree; + const { fields } = simplifyParsedResolveInfoFragmentWithType( + parsedResolveInfoFragment, + userType, + ); + const include = { + userSubscribedTo: 'userSubscribedTo' in fields, + subscribedToUser: 'subscribedToUser' in fields, + }; + + const users = await context.prisma.user.findMany({ + include, + }); + + users.forEach((user) => { + context.loaders.subscribedToUserLoader.prime( + user.id, + (user.subscribedToUser?.map((mapping) => + users.find((user) => mapping.subscriberId === user.id), + ) ?? []) as User[], + ); + + context.loaders.userSubscribedToLoader.prime( + user.id, + (user.subscribedToUser?.map((mapping) => + users.find((user) => mapping.authorId === user.id), + ) ?? []) as User[], + ); + }); + + return users; }, }, user: { diff --git a/src/routes/graphql/types/model.ts b/src/routes/graphql/types/model.ts index 4d842f216..14afcd1ea 100644 --- a/src/routes/graphql/types/model.ts +++ b/src/routes/graphql/types/model.ts @@ -31,12 +31,11 @@ export interface MemberType { export interface Context { prisma: PrismaClient; loaders: { - // userLoader: DataLoader; postLoader: DataLoader; profileLoader: DataLoader; memberTypeLoader: DataLoader; - userSubscribedToLoader: DataLoader; - subscribedToUserLoader: DataLoader; + userSubscribedToLoader: DataLoader; + subscribedToUserLoader: DataLoader; }; }