From e0c4b6d429f4178c4bb8eeb5010b009abc735ccd Mon Sep 17 00:00:00 2001 From: AlexandrOgarkov Date: Mon, 4 Nov 2024 23:26:29 +0300 Subject: [PATCH 1/3] Add types --- src/routes/graphql/types/models.ts | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/routes/graphql/types/models.ts diff --git a/src/routes/graphql/types/models.ts b/src/routes/graphql/types/models.ts new file mode 100644 index 000000000..9d41d898d --- /dev/null +++ b/src/routes/graphql/types/models.ts @@ -0,0 +1,54 @@ +import type { PrismaClient } from '@prisma/client'; +import { MemberTypeId } from '../../member-types/schemas.js'; + +export interface Context { + prisma: PrismaClient; +} + +export type UserModel = { + id: string; + name: string; + balance: number; + profile?: ProfileModel; + posts?: PostModel[]; +}; + +export type ProfileModel = { + id: string; + isMale: boolean; + yearOfBirth: number; + userId: string; + memberTypeId: string; + memberType?: MemberTypeModel; +}; + +export type PostModel = { + id: string; + title: string; + content: string; + authorId: string; +}; + +export type MemberTypeModel = { + id: string; + discount: number; + postsLimitPerMonth: number; +}; + +export type PostDto = { + title: string; + content: string; + authorId: string; +}; + +export type UserDto = { + name: string; + balance: number; +}; + +export type ProfileDto = { + isMale: boolean; + yearOfBirth: number; + memberTypeId: MemberTypeId.BASIC | MemberTypeId.BUSINESS; + userId: string; +}; From a84ae9d04768425f79d717c0c5aff1063ce07af2 Mon Sep 17 00:00:00 2001 From: AlexandrOgarkov Date: Mon, 4 Nov 2024 23:27:08 +0300 Subject: [PATCH 2/3] Add queries and mutations --- src/routes/graphql/index.ts | 16 ++- src/routes/graphql/mutation.ts | 225 +++++++++++++++++++++++++++++++++ src/routes/graphql/query.ts | 168 ++++++++++++++++++++++++ src/routes/graphql/schemas.ts | 8 ++ 4 files changed, 414 insertions(+), 3 deletions(-) create mode 100644 src/routes/graphql/mutation.ts create mode 100644 src/routes/graphql/query.ts diff --git a/src/routes/graphql/index.ts b/src/routes/graphql/index.ts index bb974d9c8..68b1d0a2e 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 { graphql } from 'graphql'; +import { createGqlResponseSchema, gqlResponseSchema, schema } from './schemas.js'; +import { graphql, parse, validate } from 'graphql'; +import depthLimit from 'graphql-depth-limit'; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { const { prisma } = fastify; @@ -15,7 +16,16 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }, }, async handler(req) { - // return graphql(); + const { query, variables } = req.body; + const errors = validate(schema, parse(query), [depthLimit(5)]); + return errors.length + ? { errors } + : await graphql({ + schema, + source: query, + variableValues: variables, + contextValue: { prisma }, + }); }, }); }; diff --git a/src/routes/graphql/mutation.ts b/src/routes/graphql/mutation.ts new file mode 100644 index 000000000..cc9fd4053 --- /dev/null +++ b/src/routes/graphql/mutation.ts @@ -0,0 +1,225 @@ +import { + GraphQLBoolean, + GraphQLFloat, + GraphQLInputObjectType, + GraphQLInt, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, +} from 'graphql'; + +import { Post, User, Profile, GraphQLMemberTypeId } from './query.js'; +import { UUIDType } from './types/uuid.js'; +import { + PostDto, + PostModel, + ProfileDto, + ProfileModel, + UserDto, + UserModel, + Context, +} from './types/models.js'; + +// Input Types +const CreatePostInput = new GraphQLInputObjectType({ + name: 'CreatePostInput', + fields: { + title: { type: new GraphQLNonNull(GraphQLString) }, + content: { type: new GraphQLNonNull(GraphQLString) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }, +}); + +const ChangePostInput = new GraphQLInputObjectType({ + name: 'ChangePostInput', + fields: { + title: { type: GraphQLString }, + content: { type: GraphQLString }, + }, +}); + +const CreateUserInput = new GraphQLInputObjectType({ + name: 'CreateUserInput', + fields: { + name: { type: new GraphQLNonNull(GraphQLString) }, + balance: { type: new GraphQLNonNull(GraphQLFloat) }, + }, +}); + +const ChangeUserInput = new GraphQLInputObjectType({ + name: 'ChangeUserInput', + fields: { + name: { type: GraphQLString }, + balance: { type: GraphQLFloat }, + }, +}); + +const CreateProfileInput = new GraphQLInputObjectType({ + name: 'CreateProfileInput', + fields: { + isMale: { type: new GraphQLNonNull(GraphQLBoolean) }, + yearOfBirth: { type: new GraphQLNonNull(GraphQLInt) }, + memberTypeId: { type: new GraphQLNonNull(GraphQLMemberTypeId) }, + userId: { type: new GraphQLNonNull(UUIDType) }, + }, +}); + +const ChangeProfileInput = new GraphQLInputObjectType({ + name: 'ChangeProfileInput', + fields: { + isMale: { type: GraphQLBoolean }, + yearOfBirth: { type: GraphQLInt }, + memberTypeId: { type: GraphQLMemberTypeId }, + }, +}); + +// Mutations +export const Mutation = new GraphQLObjectType({ + name: 'Mutation', + fields: { + createPost: { + type: Post, + args: { dto: { type: new GraphQLNonNull(CreatePostInput) } }, + resolve: async ( + root, + args: { dto: PostDto }, + context: Context, + ): Promise => await context.prisma.post.create({ data: args.dto }), + }, + deletePost: { + type: new GraphQLNonNull(UUIDType), + args: { id: { type: new GraphQLNonNull(UUIDType) } }, + resolve: async (root, args: { id: string }, context: Context) => { + await context.prisma.post.delete({ where: { id: args.id } }); + return args.id; + }, + }, + changePost: { + type: Post, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(ChangePostInput) }, + }, + resolve: async ( + root, + args: { id: string; dto: PostDto }, + context: Context, + ): Promise => + await context.prisma.post.update({ + where: { id: args.id }, + data: args.dto, + }), + }, + + createUser: { + type: User, + args: { dto: { type: new GraphQLNonNull(CreateUserInput) } }, + resolve: async ( + root, + args: { dto: UserDto }, + context: Context, + ): Promise => await context.prisma.user.create({ data: args.dto }), + }, + deleteUser: { + type: new GraphQLNonNull(UUIDType), + args: { id: { type: new GraphQLNonNull(UUIDType) } }, + resolve: async (root, args: { id: string }, context: Context) => { + await context.prisma.user.delete({ where: { id: args.id } }); + return args.id; + }, + }, + changeUser: { + type: User, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(ChangeUserInput) }, + }, + resolve: async ( + root, + args: { id: string; dto: UserDto }, + context: Context, + ): Promise => + await context.prisma.user.update({ + where: { id: args.id }, + data: args.dto, + }), + }, + + createProfile: { + type: Profile, + args: { dto: { type: new GraphQLNonNull(CreateProfileInput) } }, + resolve: async ( + root, + args: { dto: ProfileDto }, + context: Context, + ): Promise => await context.prisma.profile.create({ data: args.dto }), + }, + deleteProfile: { + type: new GraphQLNonNull(UUIDType), + args: { id: { type: new GraphQLNonNull(UUIDType) } }, + resolve: async (root, args: { id: string }, context: Context) => { + await context.prisma.profile.delete({ where: { id: args.id } }); + return args.id; + }, + }, + changeProfile: { + type: Profile, + args: { + id: { type: new GraphQLNonNull(UUIDType) }, + dto: { type: new GraphQLNonNull(ChangeProfileInput) }, + }, + resolve: async ( + root, + args: { id: string; dto: ProfileDto }, + context: Context, + ): Promise => + await context.prisma.profile.update({ + where: { id: args.id }, + data: args.dto, + }), + }, + + subscribeTo: { + type: User, + args: { + userId: { type: new GraphQLNonNull(UUIDType) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async ( + root, + args: { userId: string; authorId: string }, + context: Context, + ): Promise => + await context.prisma.user.update({ + where: { id: args.userId }, + data: { + userSubscribedTo: { + create: { authorId: args.authorId }, + }, + }, + }), + }, + unsubscribeFrom: { + type: new GraphQLNonNull(UUIDType), + args: { + userId: { type: new GraphQLNonNull(UUIDType) }, + authorId: { type: new GraphQLNonNull(UUIDType) }, + }, + resolve: async ( + root, + args: { userId: string; authorId: string }, + context: Context, + ): Promise => { + await context.prisma.subscribersOnAuthors.delete({ + where: { + subscriberId_authorId: { + subscriberId: args.userId, + authorId: args.authorId, + }, + }, + }); + return args.userId; + }, + }, + }, +}); diff --git a/src/routes/graphql/query.ts b/src/routes/graphql/query.ts new file mode 100644 index 000000000..5dae35b6e --- /dev/null +++ b/src/routes/graphql/query.ts @@ -0,0 +1,168 @@ +import { + GraphQLBoolean, + GraphQLEnumType, + GraphQLFloat, + GraphQLInt, + GraphQLList, + GraphQLObjectType, + GraphQLString, +} from 'graphql'; +import { UUIDType } from './types/uuid.js'; +import { UUID } from 'node:crypto'; +import { MemberTypeId } from '../member-types/schemas.js'; +import { UserModel, ProfileModel, Context } from './types/models.js'; + +export const GraphQLMemberTypeId = new GraphQLEnumType({ + name: 'MemberTypeId', + values: { + BASIC: { value: MemberTypeId.BASIC }, + BUSINESS: { value: MemberTypeId.BUSINESS }, + }, +}); + +export const MemberType = new GraphQLObjectType({ + name: 'MemberType', + fields: () => ({ + id: { type: GraphQLMemberTypeId }, + discount: { type: GraphQLFloat }, + postsLimitPerMonth: { type: GraphQLInt }, + }), +}); + +export const Post = new GraphQLObjectType({ + name: 'Post', + fields: () => ({ + id: { type: UUIDType }, + title: { type: GraphQLString }, + content: { type: GraphQLString }, + authorId: { type: UUIDType }, + }), +}); + +export const User = new GraphQLObjectType({ + name: 'User', + fields: () => ({ + id: { type: UUIDType }, + name: { type: GraphQLString }, + balance: { type: GraphQLFloat }, + profile: { + type: Profile, + resolve: async (root: UserModel, args, context: Context) => + await context.prisma.profile.findUnique({ + where: { userId: root.id }, + }), + }, + posts: { + type: new GraphQLList(Post), + resolve: async (root: UserModel, args, context: Context) => + await context.prisma.post.findMany({ + where: { authorId: root.id }, + }), + }, + userSubscribedTo: { + type: new GraphQLList(User), + resolve: async (root: UserModel, args, context: Context) => + await context.prisma.user.findMany({ + where: { + subscribedToUser: { + some: { + subscriberId: root.id, + }, + }, + }, + }), + }, + subscribedToUser: { + type: new GraphQLList(User), + resolve: async (root: UserModel, args, context: Context) => + await context.prisma.user.findMany({ + where: { + userSubscribedTo: { + some: { + authorId: root.id, + }, + }, + }, + }), + }, + }), +}); + +export const Profile = new GraphQLObjectType({ + name: 'Profile', + fields: () => ({ + id: { type: UUIDType }, + isMale: { type: GraphQLBoolean }, + yearOfBirth: { type: GraphQLInt }, + userId: { type: UUIDType }, + memberTypeId: { type: GraphQLMemberTypeId }, + memberType: { + type: MemberType, + resolve: async (root: ProfileModel, args, context: Context) => + await context.prisma.memberType.findUnique({ + where: { id: root.memberTypeId }, + }), + }, + }), +}); + +export const Query = new GraphQLObjectType({ + name: 'Query', + fields: () => ({ + memberTypes: { + type: new GraphQLList(MemberType), + resolve: async (root, args, context: Context) => + await context.prisma.memberType.findMany(), + }, + memberType: { + type: MemberType, + args: { id: { type: GraphQLMemberTypeId } }, + resolve: async (root, args: { id: MemberTypeId }, context: Context) => + await context.prisma.memberType.findUnique({ + where: { id: args.id }, + }), + }, + + posts: { + type: new GraphQLList(Post), + resolve: async (root, args, context: Context) => + await context.prisma.post.findMany(), + }, + post: { + type: Post, + args: { id: { type: UUIDType } }, + resolve: async (root, args: { id: UUID }, context: Context) => + await context.prisma.post.findUnique({ + where: { id: args.id }, + }), + }, + + users: { + type: new GraphQLList(User), + resolve: async (root, args, context: Context) => + await context.prisma.user.findMany(), + }, + user: { + type: User, + args: { id: { type: UUIDType } }, + resolve: async (root, args: { id: UUID }, context: Context) => + await context.prisma.user.findUnique({ + where: { id: args.id }, + }), + }, + + profiles: { + type: new GraphQLList(Profile), + resolve: async (root, args, context: Context) => + await context.prisma.profile.findMany(), + }, + profile: { + args: { id: { type: UUIDType } }, + type: Profile, + resolve: async (root, args: { id: UUID }, context: Context) => + await context.prisma.profile.findUnique({ + where: { id: args.id }, + }), + }, + }), +}); diff --git a/src/routes/graphql/schemas.ts b/src/routes/graphql/schemas.ts index 56772d6e7..f24c30921 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 { GraphQLSchema } from 'graphql/type/index.js'; +import { Query } from './query.js'; +import { Mutation } from './mutation.js'; export const gqlResponseSchema = Type.Partial( Type.Object({ @@ -18,3 +21,8 @@ export const createGqlResponseSchema = { }, ), }; + +export const schema = new GraphQLSchema({ + query: Query, + mutation: Mutation, +}); From 20f59eff3425157ad1bb7adde6ad135fc8867e09 Mon Sep 17 00:00:00 2001 From: AlexandrOgarkov Date: Tue, 5 Nov 2024 02:20:25 +0300 Subject: [PATCH 3/3] Fix subscribbeTo and unsubscribeFrom mutations --- src/routes/graphql/mutation.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/routes/graphql/mutation.ts b/src/routes/graphql/mutation.ts index cc9fd4053..a8e27c384 100644 --- a/src/routes/graphql/mutation.ts +++ b/src/routes/graphql/mutation.ts @@ -180,7 +180,7 @@ export const Mutation = new GraphQLObjectType({ }, subscribeTo: { - type: User, + type: new GraphQLNonNull(GraphQLString), args: { userId: { type: new GraphQLNonNull(UUIDType) }, authorId: { type: new GraphQLNonNull(UUIDType) }, @@ -189,18 +189,19 @@ export const Mutation = new GraphQLObjectType({ root, args: { userId: string; authorId: string }, context: Context, - ): Promise => - await context.prisma.user.update({ - where: { id: args.userId }, + ): Promise => { + await context.prisma.subscribersOnAuthors.create({ data: { - userSubscribedTo: { - create: { authorId: args.authorId }, - }, + subscriberId: args.userId, + authorId: args.authorId, }, - }), + }); + + return args.authorId; + } }, unsubscribeFrom: { - type: new GraphQLNonNull(UUIDType), + type: new GraphQLNonNull(GraphQLString), args: { userId: { type: new GraphQLNonNull(UUIDType) }, authorId: { type: new GraphQLNonNull(UUIDType) },