From 6edf9b71cd3e6e32e533c5a98d343004f50d8364 Mon Sep 17 00:00:00 2001 From: Yulia Demir Date: Sat, 24 May 2025 11:22:39 +0300 Subject: [PATCH 1/5] feat: add schemas --- src/routes/graphql/schemas.ts | 153 ++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/src/routes/graphql/schemas.ts b/src/routes/graphql/schemas.ts index 56772d6e7..7baf06935 100644 --- a/src/routes/graphql/schemas.ts +++ b/src/routes/graphql/schemas.ts @@ -1,4 +1,7 @@ import { Type } from '@fastify/type-provider-typebox'; +import { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLInt, GraphQLBoolean, GraphQLList, GraphQLNonNull, GraphQLFloat } from 'graphql'; +import { memberTypeFields, MemberTypeId } from '../member-types/schemas.js'; +import { profile } from 'console'; export const gqlResponseSchema = Type.Partial( Type.Object({ @@ -18,3 +21,153 @@ export const createGqlResponseSchema = { }, ), }; + +export function createSchema(prisma: any) { + const MemberType = new GraphQLObjectType({ + name: 'MemberType', + fields: () => ({ + id: { type: new GraphQLNonNull(GraphQLString) }, + discount: { type: new GraphQLNonNull(GraphQLFloat) }, + postsLimitPerMonth: { type: new GraphQLNonNull(GraphQLInt) }, + profiles: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Profile)))} + }), + }); + + const User = new GraphQLObjectType({ + name: 'User', + fields:() => ({ + id: { type: new GraphQLNonNull(GraphQLString) }, + name: { type: new GraphQLNonNull(GraphQLString) }, + balance: { type: new GraphQLNonNull(GraphQLFloat) }, + profile: { + type: Profile, + resolve: (parent, args, context) => { + return context.prisma.profile + .findUnique({ where: { id: parent.id} }) + .profile(); + }, + }, + + posts: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Post))), + resolve: (parent, args, context) => { + return context.prisma.user + .findUnique({ where: { id: parent.id } }) + .posts(); + }, + }, + + userSubscribedTo: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SubscribersOnAuthors))), + resolve: (parent, args, context) => { + return context.prisma.user + .findUnique({ where: { id: parent.id } }) + .userSubscribedTo(); + }, + }, + + subscribedToUser: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SubscribersOnAuthors))), + resolve: (parent, args, context) => { + return context.prisma.user + .findUnique({ where: { id: parent.id } }) + .subscribedToUser(); + }, + }, + }), + }); + + const SubscribersOnAuthors = new GraphQLObjectType({ + name: 'SubscribersOnAuthors', + fields: { + subscriber: { + type: User, + resolve: (parent, args, context) => { + return context.prisma.profile + .findUnique({ where: { id: parent.id} }) + .subscriber(); + }, + }, + + author: { + type: User, + resolve: (parent, args, context) => { + return context.prisma.profile + .findUnique({ where: { id: parent.id} }) + .author(); + }, + }, + }, + }); + + const Post = new GraphQLObjectType({ + name: 'Post', + fields: { + id: { type: new GraphQLNonNull(GraphQLString) }, + title: { type: new GraphQLNonNull(GraphQLString) }, + content: { type: new GraphQLNonNull(GraphQLString) }, + + author: { + type: User, + resolve: (parent, args, context) => { + return context.prisma.profile + .findUnique({ where: { id: parent.id} }) + .author(); + }, + } + }, + }); + + const Profile = new GraphQLObjectType({ + name: 'Profile', + fields: { + id: { type: new GraphQLNonNull(GraphQLString) }, + isMale: { type: new GraphQLNonNull(GraphQLBoolean) }, + yearOfBirth: { type: new GraphQLNonNull(GraphQLInt) }, + userId: { type: new GraphQLNonNull(GraphQLString) }, + memberTypeId: { type: new GraphQLNonNull(GraphQLString) }, + + user: { + type: User, + resolve: (parent, args, context) => { + return context.prisma.profile + .findUnique({ where: { id: parent.id} }) + .user(); + }, + }, + + memberType: { + type: MemberType, + resolve: (parent, args, context) => { + return context.prisma.profile + .findUnique({ where: { id: parent.id} }) + .memberType(); + }, + }, + }, + }); + + const Query = new GraphQLObjectType({ + name: 'Query', + fields: { + memberTypes: { + type: new GraphQLList(MemberType) , + resolve: () => prisma.memberType.findMany(), + }, + posts: { + type: new GraphQLList(Post) , + resolve: () => prisma.post.findMany(), + }, + users: { + type: new GraphQLList(User) , + resolve: () => prisma.user.findMany(), + }, + profiles: { + type: new GraphQLList(Profile) , + resolve: () => prisma.profile.findMany(), + }, + }, + }); + + return new GraphQLSchema({ query: Query }); +}; \ No newline at end of file From 15384713aa53623b6c8b1258385cb6e85d2a0de5 Mon Sep 17 00:00:00 2001 From: Yulia Demir Date: Sat, 24 May 2025 11:34:29 +0300 Subject: [PATCH 2/5] feat: implement graphql method --- src/routes/graphql/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/routes/graphql/index.ts b/src/routes/graphql/index.ts index bb974d9c8..b12d32446 100644 --- a/src/routes/graphql/index.ts +++ b/src/routes/graphql/index.ts @@ -1,9 +1,10 @@ import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'; -import { createGqlResponseSchema, gqlResponseSchema } from './schemas.js'; +import { createGqlResponseSchema, createSchema, gqlResponseSchema } from './schemas.js'; import { graphql } from 'graphql'; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { const { prisma } = fastify; + const schema = createSchema(prisma); fastify.route({ url: '/', @@ -15,7 +16,16 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }, }, async handler(req) { - // return graphql(); + const { query, variables } = req.body; + + const result = await graphql({ + schema, + source: query, + variableValues: variables, + contextValue: { prisma }, + }); + + return result; }, }); }; From 417fe5632ddb8d8c68ebee41cafb790093cdeb2e Mon Sep 17 00:00:00 2001 From: Yulia Demir Date: Sun, 25 May 2025 15:07:13 +0300 Subject: [PATCH 3/5] fix: get subscribers --- src/routes/graphql/schemas.ts | 138 +++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 45 deletions(-) diff --git a/src/routes/graphql/schemas.ts b/src/routes/graphql/schemas.ts index 7baf06935..fedd50788 100644 --- a/src/routes/graphql/schemas.ts +++ b/src/routes/graphql/schemas.ts @@ -1,7 +1,16 @@ import { Type } from '@fastify/type-provider-typebox'; -import { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLInt, GraphQLBoolean, GraphQLList, GraphQLNonNull, GraphQLFloat } from 'graphql'; -import { memberTypeFields, MemberTypeId } from '../member-types/schemas.js'; -import { profile } from 'console'; +import { + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + GraphQLInt, + GraphQLBoolean, + GraphQLList, + GraphQLNonNull, + GraphQLFloat, + GraphQLEnumType, + } from 'graphql'; +import { UUIDType } from './types/uuid.js'; export const gqlResponseSchema = Type.Partial( Type.Object({ @@ -25,8 +34,8 @@ export const createGqlResponseSchema = { export function createSchema(prisma: any) { const MemberType = new GraphQLObjectType({ name: 'MemberType', - fields: () => ({ - id: { type: new GraphQLNonNull(GraphQLString) }, + fields: () => ({ + id: { type: new GraphQLNonNull(MemberTypeIdEnum) }, discount: { type: new GraphQLNonNull(GraphQLFloat) }, postsLimitPerMonth: { type: new GraphQLNonNull(GraphQLInt) }, profiles: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Profile)))} @@ -36,42 +45,49 @@ export function createSchema(prisma: any) { const User = new GraphQLObjectType({ name: 'User', fields:() => ({ - id: { type: new GraphQLNonNull(GraphQLString) }, + id: { type: new GraphQLNonNull(UUIDType) }, name: { type: new GraphQLNonNull(GraphQLString) }, balance: { type: new GraphQLNonNull(GraphQLFloat) }, + profile: { type: Profile, resolve: (parent, args, context) => { return context.prisma.profile - .findUnique({ where: { id: parent.id} }) - .profile(); + .findUnique({ where: { userId: parent.id} }); }, }, posts: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Post))), resolve: (parent, args, context) => { - return context.prisma.user - .findUnique({ where: { id: parent.id } }) - .posts(); + return context.prisma.post + .findMany({ where: { authorId: parent.id } }); }, }, userSubscribedTo: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SubscribersOnAuthors))), - resolve: (parent, args, context) => { - return context.prisma.user - .findUnique({ where: { id: parent.id } }) - .userSubscribedTo(); + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))), + resolve: async (parent, args, context) => { + const userIsSubLinks = await context.prisma.subscribersOnAuthors + .findMany({ where: { subscriberId: parent.id } }); + + const authors = await context.prisma.user + .findMany({ where: { id: { in: userIsSubLinks.map((link) => link.authorId) } }}); + + return authors; }, }, subscribedToUser: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SubscribersOnAuthors))), - resolve: (parent, args, context) => { - return context.prisma.user - .findUnique({ where: { id: parent.id } }) - .subscribedToUser(); + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))), + resolve: async (parent, args, context) => { + const userIsAuthorLinks = await context.prisma.subscribersOnAuthors + .findMany({ where: { authorId: parent.id } }); + + const subscribers = await context.prisma.user + .findMany({ where: { id: { in: userIsAuthorLinks.map((link) => link.subscriberId) } }}); + + return subscribers; }, }, }), @@ -83,18 +99,16 @@ export function createSchema(prisma: any) { subscriber: { type: User, resolve: (parent, args, context) => { - return context.prisma.profile - .findUnique({ where: { id: parent.id} }) - .subscriber(); + return context.prisma.user + .findUnique({ where: { id: parent.subscriberId} }); }, }, author: { type: User, resolve: (parent, args, context) => { - return context.prisma.profile - .findUnique({ where: { id: parent.id} }) - .author(); + return context.prisma.user + .findUnique({ where: { id: parent.authorId} }); }, }, }, @@ -103,16 +117,15 @@ export function createSchema(prisma: any) { const Post = new GraphQLObjectType({ name: 'Post', fields: { - id: { type: new GraphQLNonNull(GraphQLString) }, + id: { type: new GraphQLNonNull(UUIDType) }, title: { type: new GraphQLNonNull(GraphQLString) }, content: { type: new GraphQLNonNull(GraphQLString) }, author: { type: User, resolve: (parent, args, context) => { - return context.prisma.profile - .findUnique({ where: { id: parent.id} }) - .author(); + return context.prisma.user + .findUnique({ where: { id: parent.authorId} }); }, } }, @@ -120,30 +133,36 @@ export function createSchema(prisma: any) { const Profile = new GraphQLObjectType({ name: 'Profile', - fields: { - id: { type: new GraphQLNonNull(GraphQLString) }, + fields: () => ({ + id: { type: new GraphQLNonNull(UUIDType) }, isMale: { type: new GraphQLNonNull(GraphQLBoolean) }, yearOfBirth: { type: new GraphQLNonNull(GraphQLInt) }, userId: { type: new GraphQLNonNull(GraphQLString) }, - memberTypeId: { type: new GraphQLNonNull(GraphQLString) }, + memberTypeId: { type: new GraphQLNonNull(MemberTypeIdEnum) }, user: { type: User, resolve: (parent, args, context) => { - return context.prisma.profile - .findUnique({ where: { id: parent.id} }) - .user(); + return context.prisma.user + .findUnique({ where: { id: parent.userId} }); }, }, memberType: { type: MemberType, resolve: (parent, args, context) => { - return context.prisma.profile - .findUnique({ where: { id: parent.id} }) - .memberType(); + return context.prisma.memberType + .findUnique({ where: { id: parent.memberTypeId} }); }, }, + }), + }); + + const MemberTypeIdEnum = new GraphQLEnumType({ + name: 'MemberTypeId', + values: { + BASIC: { value: 'BASIC' }, + BUSINESS: { value: 'BUSINESS'}, }, }); @@ -152,22 +171,51 @@ export function createSchema(prisma: any) { fields: { memberTypes: { type: new GraphQLList(MemberType) , - resolve: () => prisma.memberType.findMany(), + resolve: (_, __, context) => context.prisma.memberType.findMany(), + }, + memberType: { + type: MemberType, + args: { + id: { type: new GraphQLNonNull(MemberTypeIdEnum) }, + }, + resolve: (_, { id }, context) => context.prisma.memberType.findUnique({ where: { id } }), }, posts: { type: new GraphQLList(Post) , - resolve: () => prisma.post.findMany(), + resolve: (_, __, context) => context.prisma.post.findMany(), + }, + post: { + type: Post, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: (_, { id }, context) => context.prisma.post.findUnique({ where: { id } }), }, users: { type: new GraphQLList(User) , - resolve: () => prisma.user.findMany(), + resolve: (_, __, context) => context.prisma.user.findMany(), + }, + user: { + type: User, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: (_, { id }, context) => context.prisma.user.findUnique({ where: { id } }), }, profiles: { type: new GraphQLList(Profile) , - resolve: () => prisma.profile.findMany(), + resolve: (_, __, context) => context.prisma.profile.findMany(), + }, + profile: { + type: Profile, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: (_, { id }, context) => context.prisma.profile.findUnique({ where: { id } }), }, }, }); return new GraphQLSchema({ query: Query }); -}; \ No newline at end of file +}; + From 01e35b865e97706b2f5cbb4c1c296b80257e8f4b Mon Sep 17 00:00:00 2001 From: Yulia Demir Date: Sun, 25 May 2025 19:42:21 +0300 Subject: [PATCH 4/5] feat: add mutations --- src/routes/graphql/schemas.ts | 186 +++++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/src/routes/graphql/schemas.ts b/src/routes/graphql/schemas.ts index fedd50788..ce39f8b4f 100644 --- a/src/routes/graphql/schemas.ts +++ b/src/routes/graphql/schemas.ts @@ -9,8 +9,11 @@ import { GraphQLNonNull, GraphQLFloat, GraphQLEnumType, + GraphQLInputObjectType, } from 'graphql'; import { UUIDType } from './types/uuid.js'; +import { create } from 'domain'; +import { subscribe } from 'diagnostics_channel'; export const gqlResponseSchema = Type.Partial( Type.Object({ @@ -216,6 +219,187 @@ export function createSchema(prisma: any) { }, }); - return new GraphQLSchema({ query: Query }); + const CreateUserInput = new GraphQLInputObjectType({ + name: 'CreateUserInput', + fields: { + name: { type: new GraphQLNonNull(GraphQLString) }, + balance: { type: new GraphQLNonNull(GraphQLFloat) } + } + }); + + const CreatePostInput = new GraphQLInputObjectType({ + name: 'CreatePostInput', + fields: { + title: { type: new GraphQLNonNull(GraphQLString) }, + content: { type: new GraphQLNonNull(GraphQLString) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + } + }); + + const CreateProfileInput = 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 ChangePostInput = new GraphQLInputObjectType({ + name: 'ChangePostInput', + fields: { + title: { type: GraphQLString }, + content: { type: GraphQLString }, + } + }); + + const ChangeProfileInput = new GraphQLInputObjectType({ + name: 'ChangeProfileInput', + fields: { + isMale: { type: GraphQLBoolean }, + yearOfBirth: { type: GraphQLInt }, + memberTypeId: { type: MemberTypeIdEnum }, + } + }); + + const ChangeUserInput = new GraphQLInputObjectType({ + name: 'ChangeUserInput', + fields: { + name: { type: GraphQLString }, + balance: { type: GraphQLFloat }, + } + }); + + const Mutation = new GraphQLObjectType({ + name: 'Mutation', + fields: { + createUser: { + type: new GraphQLNonNull(User), + args: { + dto: { type: new GraphQLNonNull(CreateUserInput) }, + }, + resolve: (_, { dto }, context) => + context.prisma.user.create({ data: dto }), + }, + + createProfile: { + type: new GraphQLNonNull(Profile), + args: { + dto: { type: new GraphQLNonNull(CreateProfileInput) }, + }, + resolve: (_, { dto }, context) => + context.prisma.profile.create({ data: dto }), + }, + + createPost: { + type: new GraphQLNonNull(Post), + args: { + dto: { type: new GraphQLNonNull(CreatePostInput) }, + }, + resolve: (_, { dto }, context) => + context.prisma.post.create({ data: dto }), + }, + + changePost: { + type: new GraphQLNonNull(Post), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(ChangePostInput) }, + }, + resolve: (_, { id, dto }, context) => + context.prisma.post.update({ where: { id }, data: dto }), + }, + + changeProfile: { + type: new GraphQLNonNull(Profile), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(ChangeProfileInput) }, + }, + resolve: (_, { id, dto }, context) => + context.prisma.profile.update({ where: { id }, data: dto }), + }, + + changeUser: { + type: new GraphQLNonNull(User), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(ChangeUserInput) }, + }, + resolve: (_, { id, dto }, context) => + context.prisma.user.update({ where: { id }, data: dto }), + }, + + deleteUser: { + type: new GraphQLNonNull(GraphQLString), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (__dirname, { id }, context) => { + await context.prisma.user.delete({ where: { id }}); + return 'User deleted'; + }, + }, + + deletePost: { + type: new GraphQLNonNull(GraphQLString), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }, context) => { + await context.prisma.post.delete({ where: { id }}); + return 'Post deleted'; + }, + }, + + deleteProfile: { + type: new GraphQLNonNull(GraphQLString), + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { id }, context) => { + await context.prisma.profile.delete({ where: { id }}); + return 'Profile deleted'; + }, + }, + + subscribeTo: { + type: new GraphQLNonNull(GraphQLString), + args: { + userId: { type: new GraphQLNonNull(UUIDType) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { userId, authorId }, context) => { + await context.prisma.subscribersOnAuthors.create({ + data: { subscriberId: userId, authorId }, + }); + return 'User subscribed'; + }, + }, + + unsubscribeFrom: { + type: new GraphQLNonNull(GraphQLString), + args: { + userId: { type: new GraphQLNonNull(UUIDType) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async (_, { userId, authorId }, context) => { + await context.prisma.subscribersOnAuthors.delete({ + where: { + subscriberId_authorId: { + subscriberId: userId, + authorId: authorId, + } + }, + }); + return 'User unsubscribed'; + }, + }, + + }, + }); + + return new GraphQLSchema({ query: Query, mutation: Mutation }); }; From 62d9ab7234c46d48d8d43cc02cebefe75792de93 Mon Sep 17 00:00:00 2001 From: Yulia Demir Date: Mon, 26 May 2025 08:06:14 +0300 Subject: [PATCH 5/5] feat: add depth limit --- src/routes/graphql/index.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/routes/graphql/index.ts b/src/routes/graphql/index.ts index b12d32446..64162439b 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, createSchema, gqlResponseSchema } from './schemas.js'; -import { graphql } from 'graphql'; +import { graphql, validate, parse } from 'graphql'; +import depthLimit from 'graphql-depth-limit'; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { const { prisma } = fastify; @@ -15,9 +16,20 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { 200: gqlResponseSchema, }, }, - async handler(req) { + async handler(req, res) { const { query, variables } = req.body; + const validation = validate( + schema, + parse(query), + [depthLimit(5)] + ); + + if (validation.length > 0) { + res.code(400); + return { errors: validation }; + }; + const result = await graphql({ schema, source: query,