Merge branch 'master' of github.com:keen-studio/payload
This commit is contained in:
@@ -19,6 +19,10 @@ const base = [
|
||||
label: 'is not in',
|
||||
value: 'not_in',
|
||||
},
|
||||
{
|
||||
label: 'exists',
|
||||
value: 'exists',
|
||||
},
|
||||
];
|
||||
|
||||
const numeric = [
|
||||
|
||||
@@ -9,7 +9,7 @@ async function me({ req }) {
|
||||
delete user.collection;
|
||||
|
||||
const response = {
|
||||
user: req.user,
|
||||
user,
|
||||
collection: req.user.collection,
|
||||
};
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
14
src/graphql/initPlayground.js
Normal file
14
src/graphql/initPlayground.js
Normal 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;
|
||||
@@ -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],
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}, {}),
|
||||
|
||||
10
src/index.js
10
src/index.js
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user