feat: replace deprecated express-graphql dependency (#2484)
This commit is contained in:
@@ -106,7 +106,6 @@
|
||||
"dotenv": "^8.6.0",
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "1.4.0",
|
||||
"express-graphql": "0.12.0",
|
||||
"express-rate-limit": "^5.5.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"file-type": "16.5.4",
|
||||
@@ -115,6 +114,7 @@
|
||||
"fs-extra": "^10.1.0",
|
||||
"get-tsconfig": "^4.4.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-http": "^1.17.1",
|
||||
"graphql-playground-middleware-express": "^1.7.23",
|
||||
"graphql-query-complexity": "^0.12.0",
|
||||
"graphql-scalars": "^1.20.1",
|
||||
@@ -186,7 +186,7 @@
|
||||
"url-loader": "^4.1.1",
|
||||
"use-context-selector": "^1.4.1",
|
||||
"uuid": "^8.3.2",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack": "^5.78.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-middleware": "6.0.1",
|
||||
@@ -278,7 +278,7 @@
|
||||
"node-fetch": "2",
|
||||
"nodemon": "^2.0.20",
|
||||
"passport-strategy": "^1.0.0",
|
||||
"release-it": "^15.6.0",
|
||||
"release-it": "^15.10.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"serve-static": "^1.15.0",
|
||||
"shelljs": "^0.8.5",
|
||||
|
||||
@@ -2,19 +2,12 @@ import { GraphQLFormattedError } from 'graphql';
|
||||
import { AfterErrorHook } from '../collections/config/types';
|
||||
import { Payload } from '../payload';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param info
|
||||
* @param debug
|
||||
* @param afterErrorHook
|
||||
* @returns {Promise<unknown[]>}
|
||||
*/
|
||||
const errorHandler = async (
|
||||
payload: Payload,
|
||||
info: any,
|
||||
err: any,
|
||||
debug: boolean,
|
||||
afterErrorHook: AfterErrorHook,
|
||||
): Promise<GraphQLFormattedError[]> => Promise.all(info.result.errors.map(async (err) => {
|
||||
): Promise<GraphQLFormattedError> => {
|
||||
payload.logger.error(err.stack);
|
||||
|
||||
let response: GraphQLFormattedError = {
|
||||
@@ -33,6 +26,6 @@ const errorHandler = async (
|
||||
}
|
||||
|
||||
return response;
|
||||
}));
|
||||
};
|
||||
|
||||
export default errorHandler;
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import { graphqlHTTP } from 'express-graphql';
|
||||
import { Response } from 'express';
|
||||
import { createHandler } from 'graphql-http/lib/use/express';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import errorHandler from './errorHandler';
|
||||
|
||||
const graphQLHandler = (req: PayloadRequest, res: Response) => {
|
||||
const { payload } = req;
|
||||
|
||||
payload.errorResponses = null;
|
||||
const afterErrorHook = typeof payload.config.hooks.afterError === 'function' ? payload.config.hooks.afterError : null;
|
||||
|
||||
return graphqlHTTP(
|
||||
async (request, response, { variables }) => ({
|
||||
return createHandler(
|
||||
{
|
||||
schema: payload.schema,
|
||||
customFormatErrorFn: payload.customFormatErrorFn,
|
||||
extensions: payload.extensions,
|
||||
onOperation: async (request, args, result) => {
|
||||
const response = typeof payload.extensions === 'function' ? await payload.extensions({
|
||||
req: request,
|
||||
args,
|
||||
result,
|
||||
}) : result;
|
||||
if (response.errors) {
|
||||
const errors = await Promise.all(result.errors.map((error) => {
|
||||
return errorHandler(payload, error, payload.config.debug, afterErrorHook);
|
||||
})) as GraphQLError[];
|
||||
// errors type should be FormattedGraphQLError[] but onOperation has a return type of ExecutionResult instead of FormattedExecutionResult
|
||||
return { ...response, errors };
|
||||
}
|
||||
return response;
|
||||
},
|
||||
context: { req, res },
|
||||
validationRules: payload.validationRules(variables),
|
||||
}),
|
||||
validationRules: (request, args, defaultRules) => defaultRules.concat(payload.validationRules(args)),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import initGlobals from '../globals/graphql/init';
|
||||
import initPreferences from '../preferences/graphql/init';
|
||||
import buildPoliciesType from './schema/buildPoliciesType';
|
||||
import accessResolver from '../auth/graphql/resolvers/access';
|
||||
import errorHandler from './errorHandler';
|
||||
|
||||
export default function registerSchema(payload: Payload): void {
|
||||
payload.types = {
|
||||
@@ -74,26 +73,6 @@ export default function registerSchema(payload: Payload): void {
|
||||
|
||||
payload.schema = new GraphQLSchema(schema);
|
||||
|
||||
payload.extensions = async (info) => {
|
||||
const { result } = info;
|
||||
if (result.errors) {
|
||||
payload.errorIndex = 0;
|
||||
const afterErrorHook = typeof payload.config.hooks.afterError === 'function' ? payload.config.hooks.afterError : null;
|
||||
payload.errorResponses = await errorHandler(payload, info, payload.config.debug, afterErrorHook);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
payload.customFormatErrorFn = (error) => {
|
||||
if (payload.errorResponses && payload.errorResponses[payload.errorIndex]) {
|
||||
const response = payload.errorResponses[payload.errorIndex];
|
||||
payload.errorIndex += 1;
|
||||
return response;
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
payload.validationRules = (variables) => ([
|
||||
queryComplexity({
|
||||
estimators: [
|
||||
|
||||
@@ -18,7 +18,7 @@ import errorHandler from './express/middleware/errorHandler';
|
||||
import { PayloadRequest } from './express/types';
|
||||
import { getDataLoader } from './collections/dataloader';
|
||||
import mountEndpoints from './express/mountEndpoints';
|
||||
import { Payload, getPayload } from './payload';
|
||||
import { getPayload, Payload } from './payload';
|
||||
|
||||
export const initHTTP = async (options: InitOptions): Promise<Payload> => {
|
||||
if (typeof options.local === 'undefined') options.local = false;
|
||||
@@ -64,7 +64,7 @@ export const initHTTP = async (options: InitOptions): Promise<Payload> => {
|
||||
}
|
||||
},
|
||||
identifyAPI('GraphQL'),
|
||||
(req: PayloadRequest, res: Response) => graphQLHandler(req, res)(req, res),
|
||||
(req: PayloadRequest, res: Response, next) => graphQLHandler(req, res)(req, res, next),
|
||||
);
|
||||
initGraphQLPlayground(payload);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import pino from 'pino';
|
||||
import type { Express, Router } from 'express';
|
||||
import { GraphQLError, GraphQLFormattedError, GraphQLSchema } from 'graphql';
|
||||
import { ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql';
|
||||
import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
import mongoose from 'mongoose';
|
||||
import { Config as GeneratedTypes } from 'payload/generated-types';
|
||||
import { OperationArgs, Request as graphQLRequest } from 'graphql-http/lib/handler';
|
||||
import { BulkOperationResult, Collection, CollectionModel } from './collections/config/types';
|
||||
import { EmailOptions, InitOptions, SanitizedConfig } from './config/types';
|
||||
import { TypeWithVersion } from './versions/types';
|
||||
@@ -122,15 +123,13 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
|
||||
schema: GraphQLSchema;
|
||||
|
||||
extensions: (info: any) => Promise<any>;
|
||||
extensions: (args: {
|
||||
req: graphQLRequest<unknown, unknown>,
|
||||
args: OperationArgs<any>,
|
||||
result: ExecutionResult
|
||||
}) => Promise<any>;
|
||||
|
||||
customFormatErrorFn: (error: GraphQLError) => GraphQLFormattedError;
|
||||
|
||||
validationRules: any;
|
||||
|
||||
errorResponses: GraphQLFormattedError[] = [];
|
||||
|
||||
errorIndex: number;
|
||||
validationRules: (args: OperationArgs<any>) => ValidationRule[];
|
||||
|
||||
getAdminURL = (): string => `${this.config.serverURL}${this.config.routes.admin}`;
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ export const slug = 'posts';
|
||||
export const relationSlug = 'relation';
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
access: openAccess,
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug,
|
||||
access: openAccess,
|
||||
@@ -47,6 +53,11 @@ export default buildConfig({
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
name: 'min',
|
||||
type: 'number',
|
||||
min: 10,
|
||||
},
|
||||
// Relationship
|
||||
{
|
||||
name: 'relationField',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { GraphQLClient } from 'graphql-request';
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import configPromise from './config';
|
||||
import configPromise, { slug } from './config';
|
||||
import payload from '../../src';
|
||||
import type { Post } from './payload-types';
|
||||
|
||||
let slug = '';
|
||||
const title = 'title';
|
||||
|
||||
let client: GraphQLClient;
|
||||
@@ -14,7 +13,6 @@ describe('collections-graphql', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
const config = await configPromise;
|
||||
slug = config.collections[0]?.slug;
|
||||
const url = `${serverURL}${config.routes.api}${config.routes.graphQL}`;
|
||||
client = new GraphQLClient(url);
|
||||
});
|
||||
@@ -353,6 +351,80 @@ describe('collections-graphql', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handler', () => {
|
||||
it('should return have an array of errors when making a bad request', async () => {
|
||||
let error;
|
||||
|
||||
// language=graphQL
|
||||
const query = `query {
|
||||
Posts(where: { title: { exists: true }}) {
|
||||
docs {
|
||||
badFieldName
|
||||
}
|
||||
}
|
||||
}`;
|
||||
await client.request(query).catch((err) => {
|
||||
error = err;
|
||||
});
|
||||
expect(Array.isArray(error.response.errors)).toBe(true);
|
||||
expect(typeof error.response.errors[0].message).toBe('string');
|
||||
});
|
||||
|
||||
it('should return have an array of errors when failing to pass validation', async () => {
|
||||
let error;
|
||||
// language=graphQL
|
||||
const query = `mutation {
|
||||
createPost(data: {min: 1}) {
|
||||
id
|
||||
min
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}`;
|
||||
|
||||
await client.request(query).catch((err) => {
|
||||
error = err;
|
||||
});
|
||||
expect(Array.isArray(error.response.errors)).toBe(true);
|
||||
expect(error.response.errors[0].message).toEqual('The following field is invalid: min');
|
||||
expect(typeof error.response.errors[0].locations).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return have an array of errors when failing multiple mutations', async () => {
|
||||
let error;
|
||||
// language=graphQL
|
||||
const query = `mutation createTest {
|
||||
test1:createUser(data: { email: "test@test.com", password: "test" }) {
|
||||
email
|
||||
}
|
||||
|
||||
test2:createUser(data: { email: "test2@test.com", password: "" }) {
|
||||
email
|
||||
}
|
||||
|
||||
test3:createUser(data: { email: "test@test.com", password: "test" }) {
|
||||
email
|
||||
}
|
||||
}`;
|
||||
|
||||
await client.request(query).catch((err) => {
|
||||
error = err;
|
||||
});
|
||||
|
||||
expect(Array.isArray(error.response.errors)).toBe(true);
|
||||
expect(error.response.errors[0].message).toEqual('No password was given');
|
||||
expect(Array.isArray(error.response.errors[0].locations)).toEqual(true);
|
||||
expect(error.response.errors[0].path[0]).toEqual('test2');
|
||||
expect(error.response.errors[0].extensions.name).toEqual('MissingPasswordError');
|
||||
|
||||
expect(error.response.errors[1].message).toEqual('The following field is invalid: email');
|
||||
expect(error.response.errors[1].path[0]).toEqual('test3');
|
||||
expect(error.response.errors[1].extensions.name).toEqual('ValidationError');
|
||||
expect(error.response.errors[1].extensions.data[0].message).toEqual('A user with the given username is already registered');
|
||||
expect(error.response.errors[1].extensions.data[0].field).toEqual('email');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createPost(overrides?: Partial<Post>) {
|
||||
|
||||
@@ -5,16 +5,21 @@
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Config {
|
||||
collections: {
|
||||
posts: Post;
|
||||
relation: Relation;
|
||||
dummy: Dummy;
|
||||
users: User;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
number?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
relationField?: string | Relation;
|
||||
relationHasManyField?: string[] | Relation[];
|
||||
relationMultiRelationTo?:
|
||||
@@ -50,30 +55,18 @@ export interface Post {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relation".
|
||||
*/
|
||||
export interface Relation {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "dummy".
|
||||
*/
|
||||
export interface Dummy {
|
||||
id: string;
|
||||
name?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
@@ -83,4 +76,5 @@ export interface User {
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
@@ -719,9 +719,9 @@ type User {
|
||||
}
|
||||
|
||||
"""
|
||||
A field whose value conforms to the standard internet email address format as specified in RFC822: https://www.w3.org/Protocols/rfc822/.
|
||||
A field whose value conforms to the standard internet email address format as specified in HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address.
|
||||
"""
|
||||
scalar EmailAddress @specifiedBy(url: "https://www.w3.org/Protocols/rfc822/")
|
||||
scalar EmailAddress @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address")
|
||||
|
||||
type Users {
|
||||
docs: [User]
|
||||
|
||||
Reference in New Issue
Block a user