Merge branch 'master' of github.com:keen-studio/payload

This commit is contained in:
James
2020-10-21 17:58:49 -04:00
16 changed files with 190 additions and 80 deletions

View File

@@ -19,6 +19,10 @@ const base = [
label: 'is not in',
value: 'not_in',
},
{
label: 'exists',
value: 'exists',
},
];
const numeric = [

View File

@@ -9,7 +9,7 @@ async function me({ req }) {
delete user.collection;
const response = {
user: req.user,
user,
collection: req.user.collection,
};

View File

@@ -97,6 +97,79 @@ describe('GrahpQL Resolvers', () => {
expect(retrievedId).toStrictEqual(id);
});
it('should query exists - true', async () => {
const title = 'gql read';
const description = 'description';
const summary = 'summary';
// language=graphQL
const query = `mutation {
createLocalizedPost(data: {title: "${title}", description: "${description}", summary: "${summary}", priority: 10}) {
id
title
description
priority
createdAt
updatedAt
}
}`;
const response = await client.request(query);
const { id } = response.createLocalizedPost;
// language=graphQL
const readQuery = `query {
LocalizedPosts(where: { summary: { exists: true }}) {
docs {
id
description
summary
}
}
}`;
const readResponse = await client.request(readQuery);
const retrievedId = readResponse.LocalizedPosts.docs[0].id;
expect(readResponse.LocalizedPosts.docs).toHaveLength(1);
expect(retrievedId).toStrictEqual(id);
});
it('should query exists - false', async () => {
const title = 'gql read';
const description = 'description';
// language=graphQL
const query = `mutation {
createLocalizedPost(data: {title: "${title}", description: "${description}", priority: 10}) {
id
title
description
priority
createdAt
updatedAt
}
}`;
const response = await client.request(query);
const { id } = response.createLocalizedPost;
// language=graphQL
const readQuery = `query {
LocalizedPosts(where: { summary: { exists: false }}) {
docs {
id
summary
}
}
}`;
const readResponse = await client.request(readQuery);
const retrievedDoc = readResponse.LocalizedPosts.docs[0];
expect(readResponse.LocalizedPosts.docs.length).toBeGreaterThan(0);
expect(retrievedDoc.id).toStrictEqual(id);
expect(retrievedDoc.summary).toBeNull();
});
});
describe('Update', () => {

View File

@@ -5,11 +5,12 @@ const httpStatus = require('http-status');
* @extends Error
*/
class ExtendableError extends Error {
constructor(message, status, isPublic) {
constructor(message, status, data, isPublic) {
super(message);
this.name = this.constructor.name;
this.message = message;
this.status = status;
this.data = data;
this.isPublic = isPublic;
this.isOperational = true; // This is required since bluebird 4 doesn't append it anymore.
Error.captureStackTrace(this, this.constructor.name);
@@ -25,10 +26,11 @@ class APIError extends ExtendableError {
* Creates an API error.
* @param {string} message - Error message.
* @param {number} status - HTTP status code of error.
* @param {object} data - response data to be returned.
* @param {boolean} isPublic - Whether the message should be visible to user or not.
*/
constructor(message, status = httpStatus.INTERNAL_SERVER_ERROR, isPublic = false) {
super(message, status, isPublic);
constructor(message, status = httpStatus.INTERNAL_SERVER_ERROR, data, isPublic = false) {
super(message, status, data, isPublic);
}
}

View File

@@ -3,7 +3,7 @@ const APIError = require('./APIError');
class ValidationError extends APIError {
constructor(results) {
super(results, httpStatus.BAD_REQUEST);
super(`Bad request with ${results.length} errors`, httpStatus.BAD_REQUEST, results);
}
}

View File

@@ -1,22 +1,28 @@
const errorHandler = async (info, debug, afterErrorHook) => {
return Promise.all(info.result.errors.map(async (err) => {
// TODO: use payload logging
console.error(err.stack);
/**
*
* @param info
* @param debug
* @param afterErrorHook
* @returns {Promise<unknown[]>}
*/
const errorHandler = async (info, debug, afterErrorHook) => Promise.all(info.result.errors.map(async (err) => {
// TODO: use payload logging
console.error(err.stack);
let response = {
...err,
};
let response = {
message: err.message,
data: err?.originalError?.data,
};
if (afterErrorHook) {
({ response } = await afterErrorHook(err, response) || { response });
}
if (afterErrorHook) {
({ response } = await afterErrorHook(err, response) || { response });
}
if (debug && debug === true) {
response.stack = err.stack;
}
if (debug && debug === true) {
response.stack = err.stack;
}
return response;
}));
};
return response;
}));
module.exports = errorHandler;

View File

@@ -20,6 +20,7 @@ const initCollections = require('../collections/graphql/init');
const initGlobals = require('../globals/graphql/init');
const buildWhereInputType = require('./schema/buildWhereInputType');
const access = require('../auth/graphql/resolvers/access');
const errorHandler = require('./errorHandler');
class InitializeGraphQL {
constructor(init) {
@@ -93,31 +94,23 @@ class InitializeGraphQL {
this.schema = new GraphQLSchema(schema);
// this.errorExtensions = [];
// this.errorExtensionIteration = 0;
// this.extensions = async (info) => {
// const { result } = info;
// if (result.errors) {
// const afterErrorHook = typeof this.config.hooks.afterError === 'function' ? this.config.hooks.afterError : null;
// this.errorExtensions = await errorHandler(info, this.config.debug, afterErrorHook);
// }
// return null;
// };
this.extensions = async (info) => {
const { result } = info;
if (result.errors) {
const afterErrorHook = typeof this.config.hooks.afterError === 'function' ? this.config.hooks.afterError : null;
this.errorResponse = await errorHandler(info, this.config.debug, afterErrorHook);
}
return null;
};
}
init(req, res) {
this.errorResponse = null;
return graphQLHTTP(
async (request, response, { variables }) => ({
schema: this.schema,
// customFormatErrorFn: () => {
// const response = {
// ...this.errorExtensions[this.errorExtensionIteration],
// };
// this.errorExtensionIteration += 1;
// return response;
// },
// extensions: this.extensions,
customFormatErrorFn: () => (this.errorResponse),
extensions: this.extensions,
context: { req, res },
validationRules: [
queryComplexity({

View File

@@ -0,0 +1,14 @@
const graphQLPlayground = require('graphql-playground-middleware-express').default;
function initPlayground() {
if ((!this.config.graphQL.disablePlaygroundInProduction && process.env.NODE_ENV === 'production') || process.env.NODE_ENV !== 'production') {
this.router.get(this.config.routes.graphQLPlayground, graphQLPlayground({
endpoint: `${this.config.routes.api}${this.config.routes.graphQL}`,
settings: {
'request.credentials': 'include',
},
}));
}
}
module.exports = initPlayground;

View File

@@ -62,15 +62,21 @@ const buildWhereInputType = (name, fields, parentName) => {
return nestedPaths;
};
const operators = {
equality: ['equals', 'not_equals'],
contains: ['in', 'not_in', 'all'],
comparison: ['greater_than_equal', 'greater_than', 'less_than_equal', 'less_than'],
};
const fieldToSchemaMap = {
number: (field) => {
const type = GraphQLFloat;
return {
type: withOperators(
field.name,
field,
type,
parentName,
['equals', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals'],
[...operators.equality, ...operators.comparison],
),
};
},
@@ -78,10 +84,11 @@ const buildWhereInputType = (name, fields, parentName) => {
const type = GraphQLString;
return {
type: withOperators(
field.name,
field,
type,
parentName,
['equals', 'like', 'not_equals'],
[...operators.equality, 'like'],
),
};
},
@@ -89,10 +96,10 @@ const buildWhereInputType = (name, fields, parentName) => {
const type = EmailAddressResolver;
return {
type: withOperators(
field.name,
field,
type,
parentName,
['equals', 'like', 'not_equals'],
[...operators.equality, 'like'],
),
};
},
@@ -100,10 +107,10 @@ const buildWhereInputType = (name, fields, parentName) => {
const type = GraphQLString;
return {
type: withOperators(
field.name,
field,
type,
parentName,
['equals', 'like', 'not_equals'],
[...operators.equality, 'like'],
),
};
},
@@ -111,10 +118,10 @@ const buildWhereInputType = (name, fields, parentName) => {
const type = GraphQLJSON;
return {
type: withOperators(
field.name,
field,
type,
parentName,
['equals', 'like', 'not_equals'],
[...operators.equality, 'like'],
),
};
},
@@ -122,16 +129,16 @@ const buildWhereInputType = (name, fields, parentName) => {
const type = GraphQLString;
return {
type: withOperators(
field.name,
field,
type,
parentName,
['equals', 'like', 'not_equals'],
[...operators.equality, 'like'],
),
};
},
radio: (field) => ({
type: withOperators(
field.name,
field,
new GraphQLEnumType({
name: `${combineParentName(parentName, field.name)}_Input`,
values: field.options.reduce((values, option) => ({
@@ -142,26 +149,26 @@ const buildWhereInputType = (name, fields, parentName) => {
}), {}),
}),
parentName,
['like', 'equals', 'not_equals'],
[...operators.equality, 'like'],
),
}),
date: (field) => {
const type = DateTimeResolver;
return {
type: withOperators(
field.name,
field,
type,
parentName,
['equals', 'like', 'not_equals', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than'],
[...operators.equality, ...operators.comparison, 'like'],
),
};
},
relationship: (field) => {
let type = withOperators(
field.name,
field,
GraphQLString,
parentName,
['in', 'not_in', 'all', 'equals', 'not_equals'],
[...operators.equality, ...operators.contains],
);
if (Array.isArray(field.relationTo)) {
@@ -194,23 +201,23 @@ const buildWhereInputType = (name, fields, parentName) => {
},
upload: (field) => ({
type: withOperators(
field.name,
field,
GraphQLString,
parentName,
['equals', 'not_equals'],
[...operators.equality],
),
}),
checkbox: (field) => ({
type: withOperators(
field.name,
field,
GraphQLBoolean,
parentName,
['equals', 'not_equals'],
[...operators.equality],
),
}),
select: (field) => ({
type: withOperators(
field.name,
field,
new GraphQLEnumType({
name: `${combineParentName(parentName, field.name)}_Input`,
values: field.options.reduce((values, option) => {
@@ -236,7 +243,7 @@ const buildWhereInputType = (name, fields, parentName) => {
}, {}),
}),
parentName,
['in', 'not_in', 'all', 'equals', 'not_equals'],
[...operators.equality, ...operators.contains],
),
}),
array: (field) => recursivelyBuildNestedPaths(field),
@@ -296,10 +303,10 @@ const buildWhereInputType = (name, fields, parentName) => {
fieldTypes.id = {
type: withOperators(
'id',
{ name: 'id' },
GraphQLString,
parentName,
['equals', 'not_equals', 'in', 'not_in'],
[...operators.equality, ...operators.contains],
),
};

View File

@@ -1,17 +1,27 @@
const { GraphQLList, GraphQLInputObjectType } = require('graphql');
const { GraphQLList, GraphQLInputObjectType, GraphQLBoolean } = require('graphql');
const combineParentName = require('../utilities/combineParentName');
const withOperators = (fieldName, type, parent, operators) => {
const name = `${combineParentName(parent, fieldName)}_operator`;
const withOperators = (field, type, parent, operators) => {
const name = `${combineParentName(parent, field.name)}_operator`;
const listOperators = ['in', 'not_in', 'all'];
if (!field.required) operators.push('exists');
return new GraphQLInputObjectType({
name,
fields: operators.reduce((fields, operator) => {
let gqlType;
if (listOperators.indexOf(operator) > -1) {
gqlType = new GraphQLList(type);
} else if (operator === 'exists') {
gqlType = GraphQLBoolean;
} else {
gqlType = type;
}
return {
...fields,
[operator]: {
type: listOperators.indexOf(operator) > -1 ? new GraphQLList(type) : type,
type: gqlType,
},
};
}, {}),

View File

@@ -2,7 +2,6 @@ require('es6-promise').polyfill();
require('isomorphic-fetch');
const express = require('express');
const graphQLPlayground = require('graphql-playground-middleware-express').default;
const logger = require('./utilities/logger')();
const bindOperations = require('./init/bindOperations');
const bindRequestHandlers = require('./init/bindRequestHandlers');
@@ -15,6 +14,7 @@ const initAdmin = require('./express/admin');
const initAuth = require('./auth/init');
const initCollections = require('./collections/init');
const initGlobals = require('./globals/init');
const initGraphQLPlayground = require('./graphql/initPlayground');
const initStatic = require('./express/static');
const GraphQL = require('./graphql');
const sanitizeConfig = require('./utilities/sanitizeConfig');
@@ -60,6 +60,7 @@ class Payload {
this.initAuth = initAuth.bind(this);
this.initCollections = initCollections.bind(this);
this.initGlobals = initGlobals.bind(this);
this.initGraphQLPlayground = initGraphQLPlayground.bind(this);
this.buildEmail = buildEmail.bind(this);
this.sendEmail = this.sendEmail.bind(this);
this.getMockEmailCredentials = this.getMockEmailCredentials.bind(this);
@@ -113,12 +114,7 @@ class Payload {
(req, res) => graphQLHandler.init(req, res)(req, res),
);
this.router.get(this.config.routes.graphQLPlayground, graphQLPlayground({
endpoint: `${this.config.routes.api}${this.config.routes.graphQL}`,
settings: {
'request.credentials': 'include',
},
}));
this.initGraphQLPlayground();
// Bind router to API
this.express.use(this.config.routes.api, this.router);

View File

@@ -2,7 +2,7 @@
/* eslint-disable no-restricted-syntax */
const mongoose = require('mongoose');
const validOperators = ['like', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals'];
const validOperators = ['like', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists'];
function addSearchParam(key, value, searchParams) {
return {
@@ -216,6 +216,10 @@ class ParamParser {
break;
case 'exists':
formattedValue = { $exists: (val === 'true' || val === true) };
break;
default:
formattedValue = val;
break;

View File

@@ -40,6 +40,7 @@ const sanitizeConfig = (config) => {
sanitizedConfig.graphQL = config.graphQL || {};
sanitizedConfig.graphQL.maxComplexity = (sanitizedConfig.graphQL && sanitizedConfig.graphQL.maxComplexity) ? sanitizedConfig.graphQL.maxComplexity : 1000;
sanitizedConfig.graphQL.disablePlaygroundInProduction = (sanitizedConfig.graphQL && sanitizedConfig.graphQL.disablePlaygroundInProduction !== undefined) ? sanitizedConfig.graphQL.disablePlaygroundInProduction : true;
sanitizedConfig.routes = {
admin: (config.routes && config.routes.admin) ? config.routes.admin : '/admin',