diff --git a/.eslintrc.js b/.eslintrc.js index 259de13..3a8c5c7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const { environments } = require('eslint-plugin-prettier'); + module.exports = { parser: '@typescript-eslint/parser', parserOptions: { @@ -14,6 +16,7 @@ module.exports = { env: { node: true, jest: true, + browser: true, }, ignorePatterns: ['.eslintrc.js'], rules: { diff --git a/Dockerfile b/Dockerfile index 4cdc2ed..066092d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,12 +26,6 @@ COPY --from=node_modules /app/node_modules ./node_modules # Expose the port EXPOSE 3000 - -# run migrations -RUN npm run prisma:migrate -# generate prisma client -RUN npm run prisma:generate - # Start the application CMD ["npm", "run", "start:dev"] \ No newline at end of file diff --git a/codegen.ts b/codegen.ts new file mode 100644 index 0000000..85f6a57 --- /dev/null +++ b/codegen.ts @@ -0,0 +1,14 @@ + +import type { CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + overwrite: true, + schema: "https://api.epess.org/graphql", + generates: { + "src/graphql/types/graphql.d.ts": { + plugins: ["typescript", "typescript-resolvers"] + } + } +}; + +export default config; diff --git a/epess-database b/epess-database index d64b153..7ef6a13 160000 --- a/epess-database +++ b/epess-database @@ -1 +1 @@ -Subproject commit d64b1537c8916da6ffa6a4bac72dc9c882e43510 +Subproject commit 7ef6a13b278413aa1ea2c4eecebefcad76d3fe23 diff --git a/package-lock.json b/package-lock.json index baa8c7d..1babb9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@apollo/server": "^4.11.0", "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/introspection": "^4.0.3", "@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript-operations": "^4.2.3", "@graphql-codegen/typescript-resolvers": "^4.2.1", @@ -27,8 +28,6 @@ "@pothos/plugin-prisma-utils": "^1.2.0", "@pothos/plugin-scope-auth": "^4.1.0", "@prisma/client": "^5.20.0", - "@smatch-corp/nestjs-pothos": "^0.3.0", - "@smatch-corp/nestjs-pothos-apollo-driver": "^0.1.0", "apollo-server-express": "^3.13.0", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", @@ -2360,6 +2359,26 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "license": "0BSD" }, + "node_modules/@graphql-codegen/introspection": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@graphql-codegen/introspection/-/introspection-4.0.3.tgz", + "integrity": "sha512-4cHRG15Zu4MXMF4wTQmywNf4+fkDYv5lTbzraVfliDnB8rJKcaurQpRBi11KVuQUe24YTq/Cfk4uwewfNikWoA==", + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/visitor-plugin-common": "^5.0.0", + "tslib": "~2.6.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/@graphql-codegen/introspection/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, "node_modules/@graphql-codegen/plugin-helpers": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.4.tgz", @@ -5440,35 +5459,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@smatch-corp/nestjs-pothos": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@smatch-corp/nestjs-pothos/-/nestjs-pothos-0.3.0.tgz", - "integrity": "sha512-MRWLe+jCKu3q2Nr5gc2o4vU4R2LDd0Fwk5Il6HOUgMAJ7yxUjvRDf7C0tCYLme2wESjEyCPTjMyoQr7yj2Ju1g==", - "license": "MIT", - "peerDependencies": { - "@nestjs/common": "^7 || ^8 || ^9", - "@nestjs/core": "^7 || ^8 || ^9", - "@nestjs/graphql": "*", - "@pothos/core": "^3", - "rxjs": "*" - } - }, - "node_modules/@smatch-corp/nestjs-pothos-apollo-driver": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@smatch-corp/nestjs-pothos-apollo-driver/-/nestjs-pothos-apollo-driver-0.1.0.tgz", - "integrity": "sha512-Xcqz6v4pOBMvJR5jm7L05EMHiOn+ZDmvKIRVfNmvK8Ttoo/fmzRIrCO3WHVJn0VdCk5nFvqgins2omHifzUwTg==", - "license": "MIT", - "dependencies": { - "@smatch-corp/nestjs-pothos": "^0.3.0" - }, - "peerDependencies": { - "@nestjs/apollo": "^10", - "@nestjs/common": "^7 || ^8 || ^9", - "@nestjs/core": "^7 || ^8 || ^9", - "@nestjs/graphql": "^10", - "rxjs": "*" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", diff --git a/package.json b/package.json index a2f8fd6..735b554 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,13 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "codegen": "graphql-codegen --config codegen.ts" }, "dependencies": { "@apollo/server": "^4.11.0", "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/introspection": "^4.0.3", "@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript-operations": "^4.2.3", "@graphql-codegen/typescript-resolvers": "^4.2.1", @@ -78,7 +80,10 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3", - "ws": "^8.18.0" + "ws": "^8.18.0", + "@graphql-codegen/typescript-resolvers": "4.2.1", + "@graphql-codegen/typescript": "4.0.9", + "@graphql-codegen/cli": "5.0.2" }, "jest": { "moduleFileExtensions": [ @@ -97,4 +102,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts new file mode 100644 index 0000000..d3234b8 --- /dev/null +++ b/src/generated/graphql.ts @@ -0,0 +1,324 @@ +import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } + DateTime: { input: any; output: any; } +}; + +export type Center = { + __typename?: 'Center'; + id?: Maybe; + userId?: Maybe; +}; + +export type CenterStaff = { + __typename?: 'CenterStaff'; + centerId?: Maybe; + serviceId?: Maybe; + staffId?: Maybe; +}; + +export type ChatRoom = { + __typename?: 'ChatRoom'; + id?: Maybe; +}; + +export type Order = { + __typename?: 'Order'; + id?: Maybe; + userId?: Maybe; +}; + +export type Query = { + __typename?: 'Query'; + users?: Maybe>; +}; + +export type Service = { + __typename?: 'Service'; + id?: Maybe; + userId?: Maybe; +}; + +export type ServiceFeedback = { + __typename?: 'ServiceFeedback'; + id?: Maybe; + userId?: Maybe; +}; + +export type User = { + __typename?: 'User'; + CenterStaff?: Maybe; + Service?: Maybe>; + WorkshopSubscription?: Maybe>; + center?: Maybe
; + centerStaffChatRoom?: Maybe>; + createdAt?: Maybe; + customerChatRoom?: Maybe>; + documents?: Maybe>; + email?: Maybe; + id?: Maybe; + name?: Maybe; + order?: Maybe>; + phoneNumber?: Maybe; + role?: Maybe; + sendingMessage?: Maybe>; + serviceFeedbacks?: Maybe>; + updatedAt?: Maybe; +}; + +export type Workshop = { + __typename?: 'Workshop'; + id?: Maybe; +}; + +export type WorkshopSubscription = { + __typename?: 'WorkshopSubscription'; + user?: Maybe; + userId?: Maybe; + workshop?: Maybe; + workshopId?: Maybe; +}; + +export type Documents = { + __typename?: 'documents'; + id?: Maybe; + userId?: Maybe; +}; + +export type SendingMessage = { + __typename?: 'sendingMessage'; + id?: Maybe; + userId?: Maybe; +}; + + + +export type ResolverTypeWrapper = Promise | T; + + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver = ResolverFn | ResolverWithResolve; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise | TResult; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterable | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe | Promise>; + +export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + + + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Boolean: ResolverTypeWrapper; + Center: ResolverTypeWrapper
; + CenterStaff: ResolverTypeWrapper; + ChatRoom: ResolverTypeWrapper; + DateTime: ResolverTypeWrapper; + Float: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + Int: ResolverTypeWrapper; + Order: ResolverTypeWrapper; + Query: ResolverTypeWrapper<{}>; + Service: ResolverTypeWrapper; + ServiceFeedback: ResolverTypeWrapper; + String: ResolverTypeWrapper; + User: ResolverTypeWrapper; + Workshop: ResolverTypeWrapper; + WorkshopSubscription: ResolverTypeWrapper; + documents: ResolverTypeWrapper; + sendingMessage: ResolverTypeWrapper; +}; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Boolean: Scalars['Boolean']['output']; + Center: Center; + CenterStaff: CenterStaff; + ChatRoom: ChatRoom; + DateTime: Scalars['DateTime']['output']; + Float: Scalars['Float']['output']; + ID: Scalars['ID']['output']; + Int: Scalars['Int']['output']; + Order: Order; + Query: {}; + Service: Service; + ServiceFeedback: ServiceFeedback; + String: Scalars['String']['output']; + User: User; + Workshop: Workshop; + WorkshopSubscription: WorkshopSubscription; + documents: Documents; + sendingMessage: SendingMessage; +}; + +export type CenterResolvers = { + id?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type CenterStaffResolvers = { + centerId?: Resolver, ParentType, ContextType>; + serviceId?: Resolver, ParentType, ContextType>; + staffId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ChatRoomResolvers = { + id?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig { + name: 'DateTime'; +} + +export type OrderResolvers = { + id?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type QueryResolvers = { + users?: Resolver>, ParentType, ContextType>; +}; + +export type ServiceResolvers = { + id?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ServiceFeedbackResolvers = { + id?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type UserResolvers = { + CenterStaff?: Resolver, ParentType, ContextType>; + Service?: Resolver>, ParentType, ContextType>; + WorkshopSubscription?: Resolver>, ParentType, ContextType>; + center?: Resolver, ParentType, ContextType>; + centerStaffChatRoom?: Resolver>, ParentType, ContextType>; + createdAt?: Resolver, ParentType, ContextType>; + customerChatRoom?: Resolver>, ParentType, ContextType>; + documents?: Resolver>, ParentType, ContextType>; + email?: Resolver, ParentType, ContextType>; + id?: Resolver, ParentType, ContextType>; + name?: Resolver, ParentType, ContextType>; + order?: Resolver>, ParentType, ContextType>; + phoneNumber?: Resolver, ParentType, ContextType>; + role?: Resolver, ParentType, ContextType>; + sendingMessage?: Resolver>, ParentType, ContextType>; + serviceFeedbacks?: Resolver>, ParentType, ContextType>; + updatedAt?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type WorkshopResolvers = { + id?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type WorkshopSubscriptionResolvers = { + user?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; + workshop?: Resolver, ParentType, ContextType>; + workshopId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type DocumentsResolvers = { + id?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type SendingMessageResolvers = { + id?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type Resolvers = { + Center?: CenterResolvers; + CenterStaff?: CenterStaffResolvers; + ChatRoom?: ChatRoomResolvers; + DateTime?: GraphQLScalarType; + Order?: OrderResolvers; + Query?: QueryResolvers; + Service?: ServiceResolvers; + ServiceFeedback?: ServiceFeedbackResolvers; + User?: UserResolvers; + Workshop?: WorkshopResolvers; + WorkshopSubscription?: WorkshopSubscriptionResolvers; + documents?: DocumentsResolvers; + sendingMessage?: SendingMessageResolvers; +}; + diff --git a/src/graphql/graphql.module.ts b/src/graphql/graphql.module.ts index 4a17d12..5d1f06a 100644 --- a/src/graphql/graphql.module.ts +++ b/src/graphql/graphql.module.ts @@ -1,8 +1,9 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { PrismaService } from '../prisma/prisma.service'; import { schema } from './schema'; +import { GraphQLValidationMiddleware } from 'src/middlewares/graphql.middleware'; @Module({ imports: [ GraphQLModule.forRoot({ @@ -15,4 +16,10 @@ import { schema } from './schema'; ], providers: [PrismaService], }) -export class GraphqlModule {} +export class GraphqlModule { + configure(consumer: MiddlewareConsumer) { + consumer + .apply(GraphQLValidationMiddleware) // Apply the custom middleware + .forRoutes('graphql'); // Ensure it only applies to the /graphql endpoint + } +} diff --git a/src/graphql/schema.ts b/src/graphql/schema.ts index 7bde063..3e6e77c 100644 --- a/src/graphql/schema.ts +++ b/src/graphql/schema.ts @@ -1,5 +1,4 @@ import { builder, prisma } from './graphql.builder'; -import type PrismaTypes from '@pothos/plugin-prisma/generated'; builder.prismaObject('User', { fields: (t) => ({ @@ -114,10 +113,103 @@ builder.queryType({ }); }, }), + user: t.prismaField({ + type: 'User', + args: { + id: t.arg.string(), + }, + resolve: (query, root, args, ctx, info) => { + return prisma.user.findUnique({ + where: { + id: args.id?.toString(), + }, + }); + }, + }), + orders: t.prismaField({ + type: ['Order'], + resolve: (query, root, args, ctx, info) => { + return prisma.order.findMany({ + ...query, + }); + }, + }), + serviceFeedbacks: t.prismaField({ + type: ['ServiceFeedback'], + resolve: (query, root, args, ctx, info) => { + return prisma.serviceFeedback.findMany({ + ...query, + }); + }, + }), + documents: t.prismaField({ + type: ['UploadedDocument'], + resolve: (query, root, args, ctx, info) => { + return prisma.uploadedDocument.findMany({ + ...query, + }); + }, + }), + messages: t.prismaField({ + type: ['Message'], + resolve: (query, root, args, ctx, info) => { + return prisma.message.findMany({ + ...query, + }); + }, + }), + services: t.prismaField({ + type: ['Service'], + resolve: (query, root, args, ctx, info) => { + return prisma.service.findMany({ + ...query, + }); + }, + }), + centers: t.prismaField({ + type: ['Center'], + resolve: (query, root, args, ctx, info) => { + return prisma.center.findMany({ + ...query, + }); + }, + }), + chatRooms: t.prismaField({ + type: ['ChatRoom'], + resolve: (query, root, args, ctx, info) => { + return prisma.chatRoom.findMany({ + ...query, + }); + }, + }), + centerStaffs: t.prismaField({ + type: ['CenterStaff'], + resolve: (query, root, args, ctx, info) => { + return prisma.centerStaff.findMany({ + ...query, + }); + }, + }), + workshopSubscriptions: t.prismaField({ + type: ['WorkshopSubscription'], + resolve: (query, root, args, ctx, info) => { + return prisma.workshopSubscription.findMany({ + ...query, + }); + }, + }), + workshops: t.prismaField({ + type: ['Workshop'], + resolve: (query, root, args, ctx, info) => { + return prisma.workshop.findMany({ + ...query, + }); + }, + }), }), }); -// // Mutation section +// Mutation section // builder.mutationType({ // fields: (t) => ({ // createUser: t.prismaField({ diff --git a/src/middlewares/graphql.middleware.ts b/src/middlewares/graphql.middleware.ts new file mode 100644 index 0000000..9370761 --- /dev/null +++ b/src/middlewares/graphql.middleware.ts @@ -0,0 +1,26 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class GraphQLValidationMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + // Only handle POST requests + if (req.method === 'POST' && req.headers['content-type'] === 'application/json') { + const { query, mutation, subscription } = req.body; + + // If none of these are present, return a custom error response + if (!query && !mutation && !subscription) { + return res.status(400).json({ + errors: [ + { + message: 'Must provide a valid GraphQL query, mutation, or subscription.', + }, + ], + }); + } + } + + // Continue to the next middleware or GraphQL handler + next(); + } +} diff --git a/src/middleware/prisma-context.middleware.ts b/src/middlewares/prisma-context.middleware.ts similarity index 100% rename from src/middleware/prisma-context.middleware.ts rename to src/middlewares/prisma-context.middleware.ts diff --git a/src/middlewares/prisma-redis-cache.middleware.ts b/src/middlewares/prisma-redis-cache.middleware.ts new file mode 100644 index 0000000..e69de29