Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/routes/graphql/id/id.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { GraphQLNonNull, GraphQLScalarType, Kind } from 'graphql';

const isUUID = (value: unknown): value is string =>
typeof value === 'string' &&
new RegExp('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$').test(
value,
);

export const UUIDType = new GraphQLScalarType({
name: 'UUID',
serialize(value) {
if (!isUUID(value)) {
throw new TypeError(`Invalid UUID.`);
}
return value;
},
parseValue(value) {
if (!isUUID(value)) {
throw new TypeError(`Invalid UUID.`);
}
return value;
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
if (isUUID(ast.value)) {
return ast.value;
}
}
return undefined;
},
});

export const IdField = {
type: new GraphQLNonNull(UUIDType),
} as const;
3 changes: 3 additions & 0 deletions src/routes/graphql/id/id.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IIdField {
id: string;
}
34 changes: 30 additions & 4 deletions src/routes/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
import { createGqlResponseSchema, gqlResponseSchema } from './schemas.js';
import { graphql } from 'graphql';
import { memberTypeLoader } from './member-type/member-type.loader.js';
import { postLoader } from './post/post.loader.js';
import { profileLoader } from './profile/profile.loader.js';
import { createGqlResponseSchema, gqlResponseSchema, MainSchema } from './schemas.js';
import { graphql, parse, validate } from 'graphql';
import depthLimit from 'graphql-depth-limit';
import { ILoaders } from './types/context.js';
import { subscriberLoader, subscriptionLoader } from './user/user.loader.js';

const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
const { prisma } = fastify;
Expand All @@ -14,8 +20,28 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
200: gqlResponseSchema,
},
},
async handler(req) {
// return graphql();
async handler({ body }) {
const query = parse(body.query);
const validationResult = validate(MainSchema, query, [depthLimit(5)]);

if (validationResult?.length) {
return { errors: validationResult };
}

const loaders: ILoaders = {
memberType: memberTypeLoader(prisma),
post: postLoader(prisma),
profile: profileLoader(prisma),
subscriber: subscriberLoader(prisma),
subscription: subscriptionLoader(prisma),
};

return graphql({
schema: MainSchema,
source: body.query,
variableValues: body.variables,
contextValue: { prisma, loaders },
});
},
});
};
Expand Down
18 changes: 18 additions & 0 deletions src/routes/graphql/member-type/member-type.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PrismaClient } from '@prisma/client';
import DataLoader from 'dataloader';

export const memberTypeLoader = (prisma: PrismaClient) => {
return new DataLoader(async (ids: readonly string[]) => {
const memberTypes = await prisma.memberType.findMany({
where: {
id: { in: [...ids] },
},
});

const profileById = new Map(
memberTypes.map((memberType) => [memberType.id, memberType]),
);

return ids.map((id) => profileById.get(id)!);
});
};
39 changes: 39 additions & 0 deletions src/routes/graphql/member-type/member-type.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
GraphQLEnumType,
GraphQLFloat,
GraphQLInt,
GraphQLNonNull,
GraphQLObjectType,
} from 'graphql/index.js';
import { IMemberType } from './member-type.types.js';

export enum MemberTypeEnum {
BASIC = 'BASIC',
BUSINESS = 'BUSINESS',
}

export const MemberTypeId = new GraphQLEnumType({
name: 'MemberTypeId',
values: Object.values(MemberTypeEnum).reduce(
(acc: Record<string, { value: MemberTypeEnum }>, value) => {
acc[value] = { value };
return acc;
},
{},
),
});

export const MemberType = new GraphQLObjectType<IMemberType>({
name: 'MemberType',
fields: {
id: {
type: new GraphQLNonNull(MemberTypeId),
},
discount: {
type: new GraphQLNonNull(GraphQLFloat),
},
postsLimitPerMonth: {
type: new GraphQLNonNull(GraphQLInt),
},
},
});
8 changes: 8 additions & 0 deletions src/routes/graphql/member-type/member-type.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MemberTypeEnum } from "./member-type.schema.js";


export interface IMemberType {
id: MemberTypeEnum;
discount: number;
postsLimitPerMonth: number;
}
133 changes: 133 additions & 0 deletions src/routes/graphql/mutations.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { GraphQLNonNull, GraphQLObjectType } from 'graphql';
import { IdField } from './id/id.schema.js';
import { IIdField } from './id/id.types.js';
import { ChangePostDto, CreatePostDto, Post } from './post/post.schema.js';
import { IChangePost, ICreatePost } from './post/post.types.js';
import { ChangeProfileDto, CreateProfileDto, Profile } from './profile/profile.schema.js';
import { IChangeProfile, ICreateProfile } from './profile/profile.types.js';
import { IContext } from './types/context.js';
import { ChangeUserDto, CreateUserDto, User } from './user/user.schema.js';
import { IChangeUser, ICreateUser, ISubscriptionChange } from './user/user.types.js';

export const Mutations = new GraphQLObjectType<unknown, IContext>({
name: 'Mutations',
fields: {
createUser: {
type: new GraphQLNonNull(User),
args: { dto: { type: new GraphQLNonNull(CreateUserDto) } },
resolve: async (_, { dto }: ICreateUser, { prisma }) => {
return prisma.user.create({ data: dto });
},
},
createPost: {
type: new GraphQLNonNull(Post),
args: { dto: { type: new GraphQLNonNull(CreatePostDto) } },
resolve: async (_, { dto }: ICreatePost, { prisma }) => {
return prisma.post.create({ data: dto });
},
},
createProfile: {
type: new GraphQLNonNull(Profile),
args: { dto: { type: new GraphQLNonNull(CreateProfileDto) } },
resolve: async (_, { dto }: ICreateProfile, { prisma }) => {
return prisma.profile.create({ data: dto });
},
},
changePost: {
type: new GraphQLNonNull(Post),
args: {
id: IdField,
dto: { type: new GraphQLNonNull(ChangePostDto) },
},
resolve: async (_, { id, dto }: IChangePost, { prisma }) => {
return prisma.post.update({ where: { id }, data: dto });
},
},
changeProfile: {
type: new GraphQLNonNull(Profile),
args: {
id: IdField,
dto: { type: new GraphQLNonNull(ChangeProfileDto) },
},
resolve: async (_, { id, dto }: IChangeProfile, { prisma }) => {
return prisma.profile.update({ where: { id }, data: dto });
},
},
changeUser: {
type: new GraphQLNonNull(User),
args: {
id: IdField,
dto: { type: new GraphQLNonNull(ChangeUserDto) },
},
resolve: async (_, { id, dto }: IChangeUser, { prisma }) => {
return prisma.user.update({ where: { id }, data: dto });
},
},
deleteUser: {
...IdField,
args: { id: IdField },
resolve: async (_, { id }: IIdField, { prisma }) => {
const result = await prisma.user.delete({ where: { id }, select: { id: true } });
return result.id;
},
},
deleteProfile: {
...IdField,
args: { id: IdField },
resolve: async (_, { id }: IIdField, { prisma }) => {
const result = await prisma.profile.delete({
where: { id },
select: { id: true },
});

return result.id;
},
},
deletePost: {
...IdField,
args: { id: IdField },
resolve: async (_, { id }: IIdField, { prisma }) => {
const result = await prisma.post.delete({ where: { id }, select: { id: true } });
return result.id;
},
},
subscribeTo: {
...IdField,
args: {
userId: IdField,
authorId: IdField,
},
resolve: async (_, { userId, authorId }: ISubscriptionChange, { prisma }) => {
const result = await prisma.subscribersOnAuthors.create({
data: {
subscriberId: userId,
authorId,
},
select: { subscriberId: true },
});

return result.subscriberId;
},
},
unsubscribeFrom: {
...IdField,
args: {
userId: IdField,
authorId: IdField,
},
resolve: async (_, { userId, authorId }: ISubscriptionChange, { prisma }) => {
const result = await prisma.subscribersOnAuthors.delete({
where: {
subscriberId_authorId: {
subscriberId: userId,
authorId,
},
},
select: { subscriberId: true },
});

return result.subscriberId;
},
},
},
});
24 changes: 24 additions & 0 deletions src/routes/graphql/post/post.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { PrismaClient } from '@prisma/client';
import DataLoader from 'dataloader';
import { IPost } from './post.types.js';

export const postLoader = (prisma: PrismaClient) => {
return new DataLoader(async (authorIds: readonly string[]) => {
const posts = await prisma.post.findMany({
where: {
authorId: { in: [...authorIds] },
},
});

const postsByAuthor = new Map(
posts.reduce((acc: Map<string, IPost[]>, post) => {
const posts = acc.get(post.authorId) ?? [];
posts.push(post);
acc.set(post.authorId, posts);
return acc;
}, new Map()),
);

return authorIds.map((authorId) => postsByAuthor.get(authorId) ?? []);
});
};
42 changes: 42 additions & 0 deletions src/routes/graphql/post/post.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
GraphQLInputObjectType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLString,
} from 'graphql/index.js';
import { IdField } from '../id/id.schema.js';
import { IPost } from './post.types.js';

const fields = {
id: IdField,
title: {
type: new GraphQLNonNull(GraphQLString),
},
content: {
type: new GraphQLNonNull(GraphQLString),
},
authorId: IdField,
} as const;

export const Post = new GraphQLObjectType<IPost>({ name: 'Post', fields });

export const CreatePostDto = new GraphQLInputObjectType({
name: 'CreatePostInput',
fields: {
title: fields.title,
content: fields.content,
authorId: fields.authorId,
},
});

export const ChangePostDto = new GraphQLInputObjectType({
name: 'ChangePostInput',
fields: {
title: {
type: GraphQLString,
},
content: {
type: GraphQLString,
},
},
});
15 changes: 15 additions & 0 deletions src/routes/graphql/post/post.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IIdField } from '../id/id.types.js';

export interface IPost extends IIdField {
title: string;
content: string;
authorId: string;
}

export interface ICreatePost {
dto: Omit<IPost, 'id'>;
}

export interface IChangePost extends IIdField {
dto: Partial<Pick<IPost, 'title' | 'content'>>;
}
16 changes: 16 additions & 0 deletions src/routes/graphql/profile/profile.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PrismaClient } from '@prisma/client';
import DataLoader from 'dataloader';
import { IProfile } from './profile.types.js';

export const profileLoader = (prisma: PrismaClient) => {
return new DataLoader(async (userIds: readonly string[]) => {
const profiles = (await prisma.profile.findMany({
where: {
userId: { in: [...userIds] },
},
})) as IProfile[];

const profileByUser = new Map(profiles.map((profile) => [profile.userId, profile]));
return userIds.map((userId) => profileByUser.get(userId));
});
};
Loading