From a22dfe6c421933d9af7a89518e79c44a6f3b8d00 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Mon, 8 Jul 2019 23:13:59 +0100 Subject: [PATCH 01/26] add parse-graph-ql configuration for class schema customisation Not yet tested - essentially an RFC --- spec/ParseGraphQLServer.spec.js | 7 +- src/GraphQL/ParseGraphQLSchema.js | 76 +++++- src/GraphQL/ParseGraphQLServer.js | 3 +- src/GraphQL/loaders/parseClassMutations.js | 204 +++++++++------- src/GraphQL/loaders/parseClassQueries.js | 185 +++++++------- src/GraphQL/loaders/parseClassTypes.js | 266 +++++++++++++++++---- src/GraphQL/loaders/usersMutations.js | 3 + src/GraphQL/loaders/usersQueries.js | 3 + src/Options/index.js | 43 ++++ src/ParseServer.js | 1 + 10 files changed, 569 insertions(+), 222 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 82f9ca1e72..5b3839026f 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -685,12 +685,15 @@ describe('ParseGraphQLServer', () => { const expectedTypes = [ '_RoleClass', '_RoleConstraints', - '_RoleFields', + '_RoleCreateFields', + '_RoleUpdateFields', '_RoleFindResult', '_UserClass', '_UserConstraints', '_UserFindResult', - '_UserFields', + '_UserSignUpFields', + '_UserCreateFields', + '_UserUpdateFields', ]; expect( expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 7664eef525..76f9b85721 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -8,18 +8,23 @@ import * as parseClassQueries from './loaders/parseClassQueries'; import * as parseClassMutations from './loaders/parseClassMutations'; import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; +import { ParseGraphQLSchemaConfig } from '../Options/index'; class ParseGraphQLSchema { - constructor(databaseController, log) { + constructor( + databaseController, + log, + graphQLSchemaConfig: ?ParseGraphQLSchemaConfig + ) { this.databaseController = databaseController || requiredParameter('You must provide a databaseController instance!'); this.log = log || requiredParameter('You must provide a log instance!'); + this.graphQLSchemaConfig = graphQLSchemaConfig || {}; } async load() { - const schemaController = await this.databaseController.loadSchema(); - const parseClasses = await schemaController.getAllClasses(); + const parseClasses = await this._getClassesForSchema(); const parseClassesString = JSON.stringify(parseClasses); if (this.graphQLSchema) { @@ -47,13 +52,13 @@ class ParseGraphQLSchema { defaultGraphQLTypes.load(this); - parseClasses.forEach(parseClass => { - parseClassTypes.load(this, parseClass); - - parseClassQueries.load(this, parseClass); - - parseClassMutations.load(this, parseClass); - }); + this._getParseClassesWithConfig(parseClasses).forEach( + ([parseClass, parseClassConfig]) => { + parseClassTypes.load(this, parseClass, parseClassConfig); + parseClassQueries.load(this, parseClass, parseClassConfig); + parseClassMutations.load(this, parseClass, parseClassConfig); + } + ); defaultGraphQLQueries.load(this); @@ -112,6 +117,57 @@ class ParseGraphQLSchema { } throw new ApolloError(message, code); } + + /** + * Gets all classes found by the `schemaController` + * minus those filtered out by the app's configuration + */ + async _getClassesForSchema() { + const { enabledForClasses, disabledForClasses } = this.graphQLSchemaConfig; + const schemaController = await this.databaseController.loadSchema(); + + const allClasses = await schemaController.getAllClasses(); + if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) { + let includedClasses = allClasses; + if (enabledForClasses) { + includedClasses = allClasses.filter(clazz => { + return enabledForClasses.includes(clazz.className); + }); + } + if (disabledForClasses) { + // Classes included in `enabledForClasses` that + // are also present in `disabledForClasses` will + // still be filtered out + includedClasses = includedClasses.filter(clazz => { + return !disabledForClasses.includes(clazz.className); + }); + } + + this.isUsersClassDisabled = !includedClasses.some(clazz => { + return clazz.className === '_User'; + }); + + return includedClasses; + } else { + return allClasses; + } + } + + /** + * This method returns a list of tuples + * that provide the parseClass along with + * its parseClassConfig where provided. + */ + _getParseClassesWithConfig(parseClasses) { + const { parseClassConfigResolver } = this.graphQLSchemaConfig; + return parseClasses.map(parseClass => { + let parseClassConfig; + if (parseClassConfigResolver) { + parseClassConfig = parseClassConfigResolver(parseClass.className); + } + return [parseClass, parseClassConfig]; + }); + } } export { ParseGraphQLSchema }; diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 5cb4c1c747..c299d89e06 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -22,7 +22,8 @@ class ParseGraphQLServer { this.parseGraphQLSchema = new ParseGraphQLSchema( this.parseServer.config.databaseController, (this.parseServer.config && this.parseServer.config.loggerController) || - defaultLogger + defaultLogger, + config.graphQLSchemaConfig ); } diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 403faa4310..e6d910c4de 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,23 +1,55 @@ import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsMutations from './objectsMutations'; +import { ParseGraphQLClassConfig } from '../../Options/index'; -const load = (parseGraphQLSchema, parseClass) => { - const className = parseClass.className; +const getParseClassMutationConfig = ( + parseClassConfig: ?ParseGraphQLClassConfig +) => { + if (parseClassConfig) { + return parseClassConfig.mutation || {}; + } else { + return {}; + } +}; + +const load = ( + parseGraphQLSchema, + parseClass, + parseClassConfig: ?ParseGraphQLClassConfig +) => { + const { className } = parseClass; + const { + create: isCreateEnabled = true, + update: isUpdateEnabled = true, + delete: isDeleteEnabled = true, + } = getParseClassMutationConfig(parseClassConfig); + + const { + classGraphQLCreateType, + classGraphQLUpdateType, + } = parseGraphQLSchema.parseClassTypes[className]; - const classGraphQLInputType = - parseGraphQLSchema.parseClassTypes[className].classGraphQLInputType; - const fields = { - description: 'These are the fields of the object.', - type: classGraphQLInputType, + const createFields = { + description: 'These are the fields used to create the object.', + type: classGraphQLCreateType, }; - const classGraphQLInputTypeFields = classGraphQLInputType.getFields(); + const updateFields = { + description: 'These are the fields used to update the object.', + type: classGraphQLUpdateType, + }; + + const classGraphQLCreateTypeFields = classGraphQLCreateType.getFields(); + const classGraphQLUpdateTypeFields = classGraphQLUpdateType.getFields(); const transformTypes = fields => { if (fields) { Object.keys(fields).forEach(field => { - if (classGraphQLInputTypeFields[field]) { - switch (classGraphQLInputTypeFields[field].type) { + const inputTypeField = + classGraphQLCreateTypeFields[field] || + classGraphQLUpdateTypeFields[field]; + if (inputTypeField) { + switch (inputTypeField.type) { case defaultGraphQLTypes.GEO_POINT: fields[field].__type = 'GeoPoint'; break; @@ -36,86 +68,92 @@ const load = (parseGraphQLSchema, parseClass) => { } }; - const createGraphQLMutationName = `create${className}`; - parseGraphQLSchema.graphQLObjectsMutations[createGraphQLMutationName] = { - description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${className} class.`, - args: { - fields, - }, - type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT), - async resolve(_source, args, context) { - try { - const { fields } = args; - const { config, auth, info } = context; + if (isCreateEnabled) { + const createGraphQLMutationName = `create${className}`; + parseGraphQLSchema.graphQLObjectsMutations[createGraphQLMutationName] = { + description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${className} class.`, + args: { + fields: createFields, + }, + type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT), + async resolve(_source, args, context) { + try { + const { fields } = args; + const { config, auth, info } = context; - transformTypes(fields); + transformTypes(fields); - return await objectsMutations.createObject( - className, - fields, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, - }; + return await objectsMutations.createObject( + className, + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + } - const updateGraphQLMutationName = `update${className}`; - parseGraphQLSchema.graphQLObjectsMutations[updateGraphQLMutationName] = { - description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${className} class.`, - args: { - objectId: defaultGraphQLTypes.OBJECT_ID_ATT, - fields, - }, - type: defaultGraphQLTypes.UPDATE_RESULT, - async resolve(_source, args, context) { - try { - const { objectId, fields } = args; - const { config, auth, info } = context; + if (isUpdateEnabled) { + const updateGraphQLMutationName = `update${className}`; + parseGraphQLSchema.graphQLObjectsMutations[updateGraphQLMutationName] = { + description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${className} class.`, + args: { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + fields: updateFields, + }, + type: defaultGraphQLTypes.UPDATE_RESULT, + async resolve(_source, args, context) { + try { + const { objectId, fields } = args; + const { config, auth, info } = context; - transformTypes(fields); + transformTypes(fields); - return await objectsMutations.updateObject( - className, - objectId, - fields, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, - }; + return await objectsMutations.updateObject( + className, + objectId, + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + } - const deleteGraphQLMutationName = `delete${className}`; - parseGraphQLSchema.graphQLObjectsMutations[deleteGraphQLMutationName] = { - description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`, - args: { - objectId: defaultGraphQLTypes.OBJECT_ID_ATT, - }, - type: new GraphQLNonNull(GraphQLBoolean), - async resolve(_source, args, context) { - try { - const { objectId } = args; - const { config, auth, info } = context; + if (isDeleteEnabled) { + const deleteGraphQLMutationName = `delete${className}`; + parseGraphQLSchema.graphQLObjectsMutations[deleteGraphQLMutationName] = { + description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`, + args: { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + }, + type: new GraphQLNonNull(GraphQLBoolean), + async resolve(_source, args, context) { + try { + const { objectId } = args; + const { config, auth, info } = context; - return await objectsMutations.deleteObject( - className, - objectId, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, - }; + return await objectsMutations.deleteObject( + className, + objectId, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + } }; export { load }; diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 7c8a048467..db88ac48b8 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -3,9 +3,28 @@ import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; import * as parseClassTypes from './parseClassTypes'; +import { ParseGraphQLClassConfig } from '../../Options/index'; -const load = (parseGraphQLSchema, parseClass) => { - const className = parseClass.className; +const getParseClassQueryConfig = ( + parseClassConfig: ?ParseGraphQLClassConfig +) => { + if (parseClassConfig) { + return parseClassConfig.query || {}; + } else { + return {}; + } +}; + +const load = ( + parseGraphQLSchema, + parseClass, + parseClassConfig: ?ParseGraphQLClassConfig +) => { + const { className } = parseClass; + const { + get: isGetEnabled = true, + find: isFindEnabled = true, + } = getParseClassQueryConfig(parseClassConfig); const { classGraphQLOutputType, @@ -13,90 +32,94 @@ const load = (parseGraphQLSchema, parseClass) => { classGraphQLFindResultType, } = parseGraphQLSchema.parseClassTypes[className]; - const getGraphQLQueryName = `get${className}`; - parseGraphQLSchema.graphQLObjectsQueries[getGraphQLQueryName] = { - description: `The ${getGraphQLQueryName} query can be used to get an object of the ${className} class by its id.`, - args: { - objectId: defaultGraphQLTypes.OBJECT_ID_ATT, - readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT, - includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT, - }, - type: new GraphQLNonNull(classGraphQLOutputType), - async resolve(_source, args, context, queryInfo) { - try { - const { objectId, readPreference, includeReadPreference } = args; - const { config, auth, info } = context; - const selectedFields = getFieldNames(queryInfo); + if (isGetEnabled) { + const getGraphQLQueryName = `get${className}`; + parseGraphQLSchema.graphQLObjectsQueries[getGraphQLQueryName] = { + description: `The ${getGraphQLQueryName} query can be used to get an object of the ${className} class by its id.`, + args: { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT, + includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT, + }, + type: new GraphQLNonNull(classGraphQLOutputType), + async resolve(_source, args, context, queryInfo) { + try { + const { objectId, readPreference, includeReadPreference } = args; + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); - const { keys, include } = parseClassTypes.extractKeysAndInclude( - selectedFields - ); + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + ); - return await objectsQueries.getObject( - className, - objectId, - keys, - include, - readPreference, - includeReadPreference, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, - }; + return await objectsQueries.getObject( + className, + objectId, + keys, + include, + readPreference, + includeReadPreference, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + } - const findGraphQLQueryName = `find${className}`; - parseGraphQLSchema.graphQLObjectsQueries[findGraphQLQueryName] = { - description: `The ${findGraphQLQueryName} query can be used to find objects of the ${className} class.`, - args: classGraphQLFindArgs, - type: new GraphQLNonNull(classGraphQLFindResultType), - async resolve(_source, args, context, queryInfo) { - try { - const { - where, - order, - skip, - limit, - readPreference, - includeReadPreference, - subqueryReadPreference, - } = args; - const { config, auth, info } = context; - const selectedFields = getFieldNames(queryInfo); + if (isFindEnabled) { + const findGraphQLQueryName = `find${className}`; + parseGraphQLSchema.graphQLObjectsQueries[findGraphQLQueryName] = { + description: `The ${findGraphQLQueryName} query can be used to find objects of the ${className} class.`, + args: classGraphQLFindArgs, + type: new GraphQLNonNull(classGraphQLFindResultType), + async resolve(_source, args, context, queryInfo) { + try { + const { + where, + order, + skip, + limit, + readPreference, + includeReadPreference, + subqueryReadPreference, + } = args; + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); - const { keys, include } = parseClassTypes.extractKeysAndInclude( - selectedFields - .filter(field => field.includes('.')) - .map(field => field.slice(field.indexOf('.') + 1)) - ); - const parseOrder = order && order.join(','); + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + .filter(field => field.includes('.')) + .map(field => field.slice(field.indexOf('.') + 1)) + ); + const parseOrder = order && order.join(','); - return await objectsQueries.findObjects( - className, - where, - parseOrder, - skip, - limit, - keys, - include, - false, - readPreference, - includeReadPreference, - subqueryReadPreference, - config, - auth, - info, - selectedFields.map(field => field.split('.', 1)[0]) - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, - }; + return await objectsQueries.findObjects( + className, + where, + parseOrder, + skip, + limit, + keys, + include, + false, + readPreference, + includeReadPreference, + subqueryReadPreference, + config, + auth, + info, + selectedFields.map(field => field.split('.', 1)[0]) + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }; + } }; export { load }; diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 2a18bedf09..ee5ee66e5d 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -13,6 +13,7 @@ import { import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; +import { ParseGraphQLClassConfig } from '../../Options/index'; const mapInputType = (parseType, targetClass, parseClassTypes) => { switch (parseType) { @@ -161,14 +162,156 @@ const extractKeysAndInclude = selectedFields => { return { keys, include }; }; -const load = (parseGraphQLSchema, parseClass) => { - const className = parseClass.className; +const getParseClassTypeConfig = ( + parseClassConfig: ?ParseGraphQLClassConfig +) => { + if (parseClassConfig) { + const { type } = parseClassConfig; + const { inputFields } = type; + if (Array.isArray(inputFields)) { + type.inputFields = { + create: inputFields, + update: inputFields, + }; + } + return type; + } else { + return {}; + } +}; +const getInputFieldsAndConstraints = function( + parseClass, + parseClassConfig: ?ParseGraphQLClassConfig +) { + const { className } = parseClass; const classFields = Object.keys(parseClass.fields); + const { + inputFields: allowedInputFields, + outputFields: allowedOutputFields, + constraintFields: allowedConstraintFields, + sortFields: allowedSortFields, + } = getParseClassTypeConfig(parseClassConfig); - const classCustomFields = classFields.filter( - field => !Object.keys(defaultGraphQLTypes.CLASS_FIELDS).includes(field) - ); + let classOutputFields; + let classCreateFields; + let classUpdateFields; + let classConstraintFields; + let classSortFields; + + // All allowed customs fields + const classCustomFields = classFields.filter(field => { + return !Object.keys(defaultGraphQLTypes.CLASS_FIELDS).includes(field); + }); + + if (allowedInputFields && allowedInputFields.create) { + classCreateFields = classCustomFields.filter(field => { + return allowedInputFields.create.includes(field); + }); + if (className === '_User') { + // _User createFields is used for Sign Up, and + // so username and password must be added to create. + if (classCreateFields.indexOf('username') === -1) { + classCreateFields.push('username'); + } + if (classCreateFields.indexOf('password') === -1) { + classCreateFields.push('password'); + } + } + } else { + classCreateFields = classCustomFields; + } + if (allowedInputFields && allowedInputFields.update) { + classUpdateFields = classCustomFields.filter(field => { + return allowedInputFields.update.includes(field); + }); + } else { + classUpdateFields = classCustomFields; + } + + if (allowedOutputFields) { + classOutputFields = classCustomFields.filter(field => { + return allowedOutputFields.includes(field); + }); + } else { + classOutputFields = classCustomFields; + } + + if (allowedConstraintFields) { + classConstraintFields = classCustomFields.filter(field => { + return allowedConstraintFields.includes(field); + }); + } else { + classConstraintFields = classFields; + } + + if (allowedSortFields) { + const getSortFieldAndDirection = (fieldConfig: string): string => { + if (fieldConfig.endsWith('_ASC')) { + return { + field: fieldConfig.substr(fieldConfig.length - 4), + asc: true, + }; + } else if (fieldConfig.endsWith('_DESC')) { + return { + field: fieldConfig.substr(fieldConfig.length - 5), + desc: true, + }; + } else { + return { + field: fieldConfig, + asc: true, + desc: true, + }; + } + }; + classSortFields = []; + allowedSortFields.forEach(fieldConfig => { + const { field, ...direction } = getSortFieldAndDirection(fieldConfig); + if (classCustomFields.includes(field)) { + classSortFields.push({ + field, + ...direction, + }); + } + }); + if (!classSortFields.length) { + // must have at least 1 order field + // otherwise the FindArgs Input Type will throw. + classSortFields.push({ + field: 'objectId', + asc: true, + desc: true, + }); + } + } else { + classSortFields = classFields.map(field => { + return { field, asc: true, desc: true }; + }); + } + + return { + classCreateFields, + classUpdateFields, + classConstraintFields, + classOutputFields, + classSortFields, + }; +}; + +const load = ( + parseGraphQLSchema, + parseClass, + parseClassConfig: ?ParseGraphQLClassConfig +) => { + const { className } = parseClass; + const { + classCreateFields, + classUpdateFields, + classOutputFields, + classConstraintFields, + classSortFields, + } = getInputFieldsAndConstraints(parseClass, parseClassConfig); const classGraphQLScalarTypeName = `${className}Pointer`; const parseScalarValue = value => { @@ -271,12 +414,12 @@ const load = (parseGraphQLSchema, parseClass) => { }); parseGraphQLSchema.graphQLTypes.push(classGraphQLRelationOpType); - const classGraphQLInputTypeName = `${className}Fields`; - const classGraphQLInputType = new GraphQLInputObjectType({ - name: classGraphQLInputTypeName, - description: `The ${classGraphQLInputTypeName} input type is used in operations that involve inputting objects of ${className} class.`, + const classGraphQLCreateTypeName = `${className}CreateFields`; + const classGraphQLCreateType = new GraphQLInputObjectType({ + name: classGraphQLCreateTypeName, + description: `The ${classGraphQLCreateTypeName} input type is used in operations that involve creation of objects in the ${className} class.`, fields: () => - classCustomFields.reduce( + classCreateFields.reduce( (fields, field) => { const type = mapInputType( parseClass.fields[field].type, @@ -300,7 +443,38 @@ const load = (parseGraphQLSchema, parseClass) => { } ), }); - parseGraphQLSchema.graphQLTypes.push(classGraphQLInputType); + parseGraphQLSchema.graphQLTypes.push(classGraphQLCreateType); + + const classGraphQLUpdateTypeName = `${className}UpdateFields`; + const classGraphQLUpdateType = new GraphQLInputObjectType({ + name: classGraphQLUpdateTypeName, + description: `The ${classGraphQLUpdateTypeName} input type is used in operations that involve creation of objects in the ${className} class.`, + fields: () => + classUpdateFields.reduce( + (fields, field) => { + const type = mapInputType( + parseClass.fields[field].type, + parseClass.fields[field].targetClass, + parseGraphQLSchema.parseClassTypes + ); + if (type) { + return { + ...fields, + [field]: { + description: `This is the object ${field}.`, + type, + }, + }; + } else { + return fields; + } + }, + { + ACL: defaultGraphQLTypes.ACL_ATT, + } + ), + }); + parseGraphQLSchema.graphQLTypes.push(classGraphQLUpdateType); const classGraphQLConstraintTypeName = `${className}PointerConstraint`; const classGraphQLConstraintType = new GraphQLInputObjectType({ @@ -333,7 +507,7 @@ const load = (parseGraphQLSchema, parseClass) => { name: classGraphQLConstraintsTypeName, description: `The ${classGraphQLConstraintsTypeName} input type is used in operations that involve filtering objects of ${className} class.`, fields: () => ({ - ...classFields.reduce((fields, field) => { + ...classConstraintFields.reduce((fields, field) => { const type = mapConstraintType( parseClass.fields[field].type, parseClass.fields[field].targetClass, @@ -371,12 +545,18 @@ const load = (parseGraphQLSchema, parseClass) => { const classGraphQLOrderType = new GraphQLEnumType({ name: classGraphQLOrderTypeName, description: `The ${classGraphQLOrderTypeName} input type is used when sorting objects of the ${className} class.`, - values: classFields.reduce((orderFields, field) => { - return { - ...orderFields, - [`${field}_ASC`]: { value: field }, - [`${field}_DESC`]: { value: `-${field}` }, + values: classSortFields.reduce((sortFields, fieldConfig) => { + const { field, asc, desc } = fieldConfig; + const updatedSortFields = { + ...sortFields, }; + if (asc) { + updatedSortFields[`${field}_ASC`] = { value: field }; + } + if (desc) { + updatedSortFields[`${field}_DESC`] = { value: `-${field}` }; + } + return updatedSortFields; }, {}), }); parseGraphQLSchema.graphQLTypes.push(classGraphQLOrderType); @@ -400,7 +580,7 @@ const load = (parseGraphQLSchema, parseClass) => { const classGraphQLOutputTypeName = `${className}Class`; const outputFields = () => { - return classCustomFields.reduce((fields, field) => { + return classOutputFields.reduce((fields, field) => { const type = mapOutputType( parseClass.fields[field].type, parseClass.fields[field].targetClass, @@ -531,7 +711,8 @@ const load = (parseGraphQLSchema, parseClass) => { parseGraphQLSchema.parseClassTypes[className] = { classGraphQLScalarType, classGraphQLRelationOpType, - classGraphQLInputType, + classGraphQLCreateType, + classGraphQLUpdateType, classGraphQLConstraintType, classGraphQLConstraintsType, classGraphQLFindArgs, @@ -552,37 +733,32 @@ const load = (parseGraphQLSchema, parseClass) => { parseGraphQLSchema.meType = meType; parseGraphQLSchema.graphQLTypes.push(meType); - const userSignUpInputTypeName = `_UserSignUpFields`; + const userSignUpInputTypeName = '_UserSignUpFields'; const userSignUpInputType = new GraphQLInputObjectType({ name: userSignUpInputTypeName, description: `The ${userSignUpInputTypeName} input type is used in operations that involve inputting objects of ${className} class when signing up.`, fields: () => - classCustomFields.reduce( - (fields, field) => { - const type = mapInputType( - parseClass.fields[field].type, - parseClass.fields[field].targetClass, - parseGraphQLSchema.parseClassTypes - ); - if (type) { - return { - ...fields, - [field]: { - description: `This is the object ${field}.`, - type: - field === 'username' || field === 'password' - ? new GraphQLNonNull(type) - : type, - }, - }; - } else { - return fields; - } - }, - { - ACL: defaultGraphQLTypes.ACL_ATT, + classCreateFields.reduce((fields, field) => { + const type = mapInputType( + parseClass.fields[field].type, + parseClass.fields[field].targetClass, + parseGraphQLSchema.parseClassTypes + ); + if (type) { + return { + ...fields, + [field]: { + description: `This is the object ${field}.`, + type: + field === 'username' || field === 'password' + ? new GraphQLNonNull(type) + : type, + }, + }; + } else { + return fields; } - ), + }), }); parseGraphQLSchema.parseClassTypes[ '_User' diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index d81a630245..71c0c46670 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -11,6 +11,9 @@ import * as objectsMutations from './objectsMutations'; const usersRouter = new UsersRouter(); const load = parseGraphQLSchema => { + if (parseGraphQLSchema.isUsersClassDisabled) { + return; + } const fields = {}; fields.signUp = { diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index fca8d43e5a..f5f6c3443e 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -6,6 +6,9 @@ import Auth from '../../Auth'; import { extractKeysAndInclude } from './parseClassTypes'; const load = parseGraphQLSchema => { + if (parseGraphQLSchema.isUsersClassDisabled) { + return; + } const fields = {}; fields.me = { diff --git a/src/Options/index.js b/src/Options/index.js index a1d3c377a2..ea91f422c3 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -5,6 +5,7 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import { CacheAdapter } from '../Adapters/Cache/CacheAdapter'; import { MailAdapter } from '../Adapters/Email/MailAdapter'; import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter'; +import { GraphQLSchema } from 'graphql'; // @flow type Adapter = string | any | T; @@ -196,6 +197,8 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_PLAYGROUND_PATH :DEFAULT: /playground */ playgroundPath: ?string; + /* GraphQL auto-generated schema options; ignored if mountGraphQL is false */ + graphQLSchemaConfig: ?ParseGraphQLSchemaConfig; serverStartComplete: ?(error: ?Error) => void; } @@ -256,3 +259,43 @@ export interface LiveQueryServerOptions { /* LiveQuery pubsub adapter */ pubSubAdapter: ?Adapter; } + +export interface ParseGraphQLClassConfig { + /* The `type` object contains options for how the class types are generated */ + type: ?{ + /* Fields that are allowed when creating or updating an object. */ + inputFields: + | ?(string[]) + | ?{ + /* Leave blank to allow all available fields in the schema. */ + create?: string[], + update?: string[], + }, + /* Fields on the edges that can be resolved from a query, i.e. the Result Type. */ + outputFields: ?(string[]), + /* Fields by which a query can be filtered, i.e. the `where` object. */ + constraintFields: ?(string[]), + /* Fields by which a query can be sorted; suffix with _ASC or _DESC to enforce direction. */ + sortFields: ?(string[]), + }; + /* The `query` object contains options for which class queries are generated */ + query: ?{ + get: ?boolean, + find: ?boolean, + }; + /* The `mutation` object contains options for which class mutations are generated */ + mutation: ?{ + create: ?boolean, + update: ?boolean, + delete: ?boolean, + }; +} + +export interface ParseGraphQLSchemaConfig { + // if empty, all classes in Schema will be enabled + enabledForClasses?: string[]; + // overrides any from `enabledForClasses` + disabledForClasses?: string[]; + parseClassConfigResolver?: (className: string) => ParseGraphQLClassConfig; + additionalSchema?: () => GraphQLSchema; +} diff --git a/src/ParseServer.js b/src/ParseServer.js index 738a0624b9..9e751e8e2e 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -270,6 +270,7 @@ class ParseServer { const parseGraphQLServer = new ParseGraphQLServer(this, { graphQLPath: options.graphQLPath, playgroundPath: options.playgroundPath, + graphQLSchemaConfig: options.graphQLSchemaConfig, }); if (options.mountGraphQL) { From 5121073247c69656b61e55efb2861389dc5bebdf Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 00:54:50 +0100 Subject: [PATCH 02/26] refactor and add graphql router, controller and config cache --- src/Controllers/CacheController.js | 1 + src/Controllers/GraphQLController.js | 165 +++++++++++++++++++++++++ src/Controllers/index.js | 16 +++ src/GraphQL/ParseGraphQLSchema.js | 132 +++++++++++++++++--- src/GraphQL/ParseGraphQLServer.js | 21 +++- src/GraphQL/loaders/parseClassTypes.js | 2 +- src/Options/index.js | 43 ------- src/ParseServer.js | 3 +- src/Routers/GraphQLRouter.js | 50 ++++++++ 9 files changed, 363 insertions(+), 70 deletions(-) create mode 100644 src/Controllers/GraphQLController.js create mode 100644 src/Routers/GraphQLRouter.js diff --git a/src/Controllers/CacheController.js b/src/Controllers/CacheController.js index f387765f60..0c645c5236 100644 --- a/src/Controllers/CacheController.js +++ b/src/Controllers/CacheController.js @@ -45,6 +45,7 @@ export class CacheController extends AdaptableController { this.role = new SubCache('role', this); this.user = new SubCache('user', this); + this.graphQL = new SubCache('graphQL', this); } get(key) { diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js new file mode 100644 index 0000000000..424ac86451 --- /dev/null +++ b/src/Controllers/GraphQLController.js @@ -0,0 +1,165 @@ +import requiredParameter from '../../lib/requiredParameter'; +import DatabaseController from './DatabaseController'; +import CacheController from './CacheController'; + +const GraphQLConfigClass = '_GraphQLConfig'; +const GraphQLConfigId = '1'; +const GraphQLConfigKey = 'config'; + +class GraphQLController { + databaseController: DatabaseController; + cacheController: CacheController; + isEnabled: boolean; + + constructor(params: { + databaseController: DatabaseController, + cacheController: CacheController, + mountGraphQL: boolean, + }) { + this.databaseController = + params.databaseController || + requiredParameter( + `GraphQLController requires a "databaseController" to be instantiated.` + ); + this.cacheController = + params.cacheController || + requiredParameter( + `GraphQLController requires a "cacheController" to be instantiated.` + ); + this.isEnabled = !!params.mountGraphQL; + } + + async getGraphQLConfig(): Promise { + const _cachedConfig = await this._getCachedGraphQLConfig(); + if (_cachedConfig) { + return _cachedConfig; + } + + const results = await this.databaseController.find( + GraphQLConfigClass, + { objectId: GraphQLConfigId }, + { limit: 1 } + ); + + let graphQLConfig; + if (results.length != 1) { + // If there is no config in the database - return empty config. + return {}; + } else { + graphQLConfig = results[0][GraphQLConfigKey]; + } + + await this._putCachedGraphQLConfig(graphQLConfig); + + return graphQLConfig; + } + + async updateGraphQLConfig( + graphQLConfig: ParseGraphQLConfig + ): Promise { + // throws if invalid + this._validateGraphQLConfig(graphQLConfig); + + // Transform in dot notation to make sure it works + const update = Object.keys(graphQLConfig).reduce((acc, key) => { + acc[`${GraphQLConfigKey}.${key}`] = graphQLConfig[key]; + return acc; + }, {}); + + await this.databaseController.update( + GraphQLConfigClass, + { objectId: GraphQLConfigId }, + update, + { upsert: true } + ); + + await this._putCachedGraphQLConfig(graphQLConfig); + + return { response: { result: true } }; + } + + async _getCachedGraphQLConfig() { + return this.cacheController.graphQL.get(GraphQLConfigKey); + } + + async _putCachedGraphQLConfig(graphQLConfig: ParseGraphQLConfig) { + return this.cacheController.graphQL.put( + GraphQLConfigKey, + graphQLConfig, + 60000 + ); + } + + _validateGraphQLConfig(graphQLConfig: ?ParseGraphQLConfig): void { + let errorMessage: string; + if (!graphQLConfig) { + errorMessage = 'cannot be undefined, null or empty.'; + } else if (typeof graphQLConfig !== 'object') { + errorMessage = 'must be a valid object.'; + } else { + const { + enabledForClasses, + disabledForClasses, + classConfigs, + ...invalidKeys + } = graphQLConfig; + + if (invalidKeys.length) { + errorMessage = `encountered invalid keys: ${invalidKeys}`; + } + // TODO use more rigirous structural validations + if (enabledForClasses && !Array.isArray(enabledForClasses)) { + errorMessage = `"enabledForClasses" is not a valid array.`; + } + if (disabledForClasses && !Array.isArray(disabledForClasses)) { + errorMessage = `"disabledForClasses" is not a valid array.`; + } + if (classConfigs && !Array.isArray(classConfigs)) { + errorMessage = `"classConfigs" is not a valid array.`; + } + } + if (errorMessage) { + throw new Error(`Invalid graphQLConfig: ${errorMessage}`); + } + } +} + +export interface ParseGraphQLConfig { + enabledForClasses?: string[]; + disabledForClasses?: string[]; + classConfigs?: ParseGraphQLClassConfig[]; +} + +export interface ParseGraphQLClassConfig { + className: string; + /* The `type` object contains options for how the class types are generated */ + type: ?{ + /* Fields that are allowed when creating or updating an object. */ + inputFields: + | ?(string[]) + | ?{ + /* Leave blank to allow all available fields in the schema. */ + create?: string[], + update?: string[], + }, + /* Fields on the edges that can be resolved from a query, i.e. the Result Type. */ + outputFields: ?(string[]), + /* Fields by which a query can be filtered, i.e. the `where` object. */ + constraintFields: ?(string[]), + /* Fields by which a query can be sorted; suffix with _ASC or _DESC to enforce direction. */ + sortFields: ?(string[]), + }; + /* The `query` object contains options for which class queries are generated */ + query: ?{ + get: ?boolean, + find: ?boolean, + }; + /* The `mutation` object contains options for which class mutations are generated */ + mutation: ?{ + create: ?boolean, + update: ?boolean, + delete: ?boolean, + }; +} + +export default GraphQLController; diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 81e7cfbead..8ef42a68e9 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -25,6 +25,7 @@ import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter'; import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter'; import ParsePushAdapter from '@parse/push-adapter'; +import GraphQLController from './GraphQLController'; export function getControllers(options: ParseServerOptions) { const loggerController = getLoggerController(options); @@ -43,6 +44,10 @@ export function getControllers(options: ParseServerOptions) { const databaseController = getDatabaseController(options, cacheController); const hooksController = getHooksController(options, databaseController); const authDataManager = getAuthDataManager(options); + const graphQLController = getGraphQLController(options, { + databaseController, + cacheController, + }); return { loggerController, filesController, @@ -54,6 +59,7 @@ export function getControllers(options: ParseServerOptions) { pushControllerQueue, analyticsController, cacheController, + graphQLController, liveQueryController, databaseController, hooksController, @@ -123,6 +129,16 @@ export function getCacheController( return new CacheController(cacheControllerAdapter, appId); } +export function getGraphQLController( + options: ParseServerOptions, + controllerDeps +): GraphQLController { + return new GraphQLController({ + mountGraphQL: options.mountGraphQL, + ...controllerDeps, + }); +} + export function getAnalyticsController( options: ParseServerOptions ): AnalyticsController { diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 76f9b85721..1861971eb0 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -8,22 +8,31 @@ import * as parseClassQueries from './loaders/parseClassQueries'; import * as parseClassMutations from './loaders/parseClassMutations'; import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; -import { ParseGraphQLSchemaConfig } from '../Options/index'; +import GraphQLController from '../Controllers/GraphQLController'; +import DatabaseController from '../Controllers/DatabaseController'; class ParseGraphQLSchema { - constructor( - databaseController, - log, - graphQLSchemaConfig: ?ParseGraphQLSchemaConfig - ) { + databaseController: DatabaseController; + graphQLController: GraphQLController; + + constructor(params: { + databaseController: DatabaseController, + graphQLController: GraphQLController, + log: any, + }) { + this.graphQLController = + params.graphQLController || + requiredParameter('You must provide a graphQLController instance!'); this.databaseController = - databaseController || + params.databaseController || requiredParameter('You must provide a databaseController instance!'); - this.log = log || requiredParameter('You must provide a log instance!'); - this.graphQLSchemaConfig = graphQLSchemaConfig || {}; + this.log = + params.log || requiredParameter('You must provide a log instance!'); } async load() { + await this._initializeSchemaAndConfig(); + const parseClasses = await this._getClassesForSchema(); const parseClassesString = JSON.stringify(parseClasses); @@ -61,25 +70,26 @@ class ParseGraphQLSchema { ); defaultGraphQLQueries.load(this); - defaultGraphQLMutations.load(this); let graphQLQuery = undefined; if (Object.keys(this.graphQLQueries).length > 0) { + const fields = this._getAllQueries(); graphQLQuery = new GraphQLObjectType({ name: 'Query', description: 'Query is the top level type for queries.', - fields: this.graphQLQueries, + fields, }); this.graphQLTypes.push(graphQLQuery); } let graphQLMutation = undefined; if (Object.keys(this.graphQLMutations).length > 0) { + const fields = this._getAllMutations(); graphQLMutation = new GraphQLObjectType({ name: 'Mutation', description: 'Mutation is the top level type for mutations.', - fields: this.graphQLMutations, + fields, }); this.graphQLTypes.push(graphQLMutation); } @@ -95,7 +105,7 @@ class ParseGraphQLSchema { } this.graphQLSchema = new GraphQLSchema({ - types: this.graphQLTypes, + types: this._mergeAdditionalTypes(this.graphQLTypes), query: graphQLQuery, mutation: graphQLMutation, subscription: graphQLSubscription, @@ -118,15 +128,24 @@ class ParseGraphQLSchema { throw new ApolloError(message, code); } + async _initializeSchemaAndConfig() { + const [schemaController, parseGraphQLConfig] = await Promise.all([ + this.databaseController.loadSchema(), + this.graphQLController.getGraphQLConfig(), + ]); + + this.schemaController = schemaController; + this.parseGraphQLConfig = parseGraphQLConfig || {}; + } + /** * Gets all classes found by the `schemaController` - * minus those filtered out by the app's configuration + * minus those filtered out by the app's parseGraphQLConfig. */ async _getClassesForSchema() { - const { enabledForClasses, disabledForClasses } = this.graphQLSchemaConfig; - const schemaController = await this.databaseController.loadSchema(); + const { enabledForClasses, disabledForClasses } = this.parseGraphQLConfig; + const allClasses = await this.schemaController.getAllClasses(); - const allClasses = await schemaController.getAllClasses(); if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) { let includedClasses = allClasses; if (enabledForClasses) { @@ -159,15 +178,88 @@ class ParseGraphQLSchema { * its parseClassConfig where provided. */ _getParseClassesWithConfig(parseClasses) { - const { parseClassConfigResolver } = this.graphQLSchemaConfig; + const { classConfigs } = this.parseGraphQLConfig; return parseClasses.map(parseClass => { let parseClassConfig; - if (parseClassConfigResolver) { - parseClassConfig = parseClassConfigResolver(parseClass.className); + if (classConfigs) { + parseClassConfig = classConfigs.find( + c => c.className === parseClass.className + ); } return [parseClass, parseClassConfig]; }); } + + _storeAdditionalSchema() { + const { additionalSchemaResolver: resolver } = this.parseGraphQLConfig; + if (resolver) { + this.additionalSchema = resolver(); + } + } + + _getAllQueries() { + const { graphQLQueries: baseQueryFields, additionalSchema } = this; + if (additionalSchema && additionalSchema.query) { + const { query: additionalQueryFields } = additionalSchema; + const reservedKeys = ['users', 'objects', 'health']; + if ( + Object.keys(additionalQueryFields).some(key => + reservedKeys.includes(key) + ) + ) { + throw new Error( + `Additional graphql schema cannot use reserved query fields: ${reservedKeys}` + ); + } + return { + ...baseQueryFields, + ...additionalQueryFields, + }; + } else { + return baseQueryFields; + } + } + + _getAllMutations() { + const { graphQLMutations: baseMutationFields, additionalSchema } = this; + if (additionalSchema && additionalSchema.mutation) { + const { mutation: additionalMutationFields } = additionalSchema; + const reservedKeys = ['users', 'objects']; + if ( + Object.keys(additionalMutationFields).some(key => + reservedKeys.includes(key) + ) + ) { + throw new Error( + `Additional graphql schema cannot use reserved mutation fields: ${reservedKeys}` + ); + } + return { + ...baseMutationFields, + ...additionalMutationFields, + }; + } else { + return baseMutationFields; + } + } + + _mergeAdditionalTypes(baseTypes) { + const { additionalSchema } = this; + if (additionalSchema && Array.isArray(additionalSchema.types)) { + const { types: additionalTypes } = additionalSchema; + if (additionalTypes.length) { + const mergedTypes = [...baseTypes]; + const baseTypeNames = baseTypes.map(type => type.name); + additionalTypes.forEach(additionalType => { + if (!baseTypeNames.includes(additionalType.name)) { + mergedTypes.push(additionalType); + } + }); + return mergedTypes; + } + } + return baseTypes; + } } export { ParseGraphQLSchema }; diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index c299d89e06..a19d9e1a24 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -9,8 +9,13 @@ import { handleParseHeaders } from '../middlewares'; import requiredParameter from '../requiredParameter'; import defaultLogger from '../logger'; import { ParseGraphQLSchema } from './ParseGraphQLSchema'; +import GraphQLController, { + ParseGraphQLConfig, +} from '../Controllers/GraphQLController'; class ParseGraphQLServer { + graphQLController: GraphQLController; + constructor(parseServer, config) { this.parseServer = parseServer || @@ -19,12 +24,14 @@ class ParseGraphQLServer { requiredParameter('You must provide a config.graphQLPath!'); } this.config = config; - this.parseGraphQLSchema = new ParseGraphQLSchema( - this.parseServer.config.databaseController, - (this.parseServer.config && this.parseServer.config.loggerController) || + this.graphQLController = this.parseServer.config.graphQLController; + this.parseGraphQLSchema = new ParseGraphQLSchema({ + graphQLController: this.graphQLController, + databaseController: this.parseServer.config.databaseController, + log: + (this.parseServer.config && this.parseServer.config.loggerController) || defaultLogger, - config.graphQLSchemaConfig - ); + }); } async _getGraphQLOptions(req) { @@ -110,6 +117,10 @@ class ParseGraphQLServer { } ); } + + async setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { + await this.graphQLController.updateGraphQLConfig(graphQLConfig); + } } export { ParseGraphQLServer }; diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index ee5ee66e5d..5f7f007928 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -758,7 +758,7 @@ const load = ( } else { return fields; } - }), + }, {}), }); parseGraphQLSchema.parseClassTypes[ '_User' diff --git a/src/Options/index.js b/src/Options/index.js index ea91f422c3..a1d3c377a2 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -5,7 +5,6 @@ import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; import { CacheAdapter } from '../Adapters/Cache/CacheAdapter'; import { MailAdapter } from '../Adapters/Email/MailAdapter'; import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter'; -import { GraphQLSchema } from 'graphql'; // @flow type Adapter = string | any | T; @@ -197,8 +196,6 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_PLAYGROUND_PATH :DEFAULT: /playground */ playgroundPath: ?string; - /* GraphQL auto-generated schema options; ignored if mountGraphQL is false */ - graphQLSchemaConfig: ?ParseGraphQLSchemaConfig; serverStartComplete: ?(error: ?Error) => void; } @@ -259,43 +256,3 @@ export interface LiveQueryServerOptions { /* LiveQuery pubsub adapter */ pubSubAdapter: ?Adapter; } - -export interface ParseGraphQLClassConfig { - /* The `type` object contains options for how the class types are generated */ - type: ?{ - /* Fields that are allowed when creating or updating an object. */ - inputFields: - | ?(string[]) - | ?{ - /* Leave blank to allow all available fields in the schema. */ - create?: string[], - update?: string[], - }, - /* Fields on the edges that can be resolved from a query, i.e. the Result Type. */ - outputFields: ?(string[]), - /* Fields by which a query can be filtered, i.e. the `where` object. */ - constraintFields: ?(string[]), - /* Fields by which a query can be sorted; suffix with _ASC or _DESC to enforce direction. */ - sortFields: ?(string[]), - }; - /* The `query` object contains options for which class queries are generated */ - query: ?{ - get: ?boolean, - find: ?boolean, - }; - /* The `mutation` object contains options for which class mutations are generated */ - mutation: ?{ - create: ?boolean, - update: ?boolean, - delete: ?boolean, - }; -} - -export interface ParseGraphQLSchemaConfig { - // if empty, all classes in Schema will be enabled - enabledForClasses?: string[]; - // overrides any from `enabledForClasses` - disabledForClasses?: string[]; - parseClassConfigResolver?: (className: string) => ParseGraphQLClassConfig; - additionalSchema?: () => GraphQLSchema; -} diff --git a/src/ParseServer.js b/src/ParseServer.js index 9e751e8e2e..5d9537911f 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -19,6 +19,7 @@ import { FeaturesRouter } from './Routers/FeaturesRouter'; import { FilesRouter } from './Routers/FilesRouter'; import { FunctionsRouter } from './Routers/FunctionsRouter'; import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; +import { GraphQLRouter } from './Routers/GraphQLRouter'; import { HooksRouter } from './Routers/HooksRouter'; import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; @@ -229,6 +230,7 @@ class ParseServer { new IAPValidationRouter(), new FeaturesRouter(), new GlobalConfigRouter(), + new GraphQLRouter(), new PurgeRouter(), new HooksRouter(), new CloudCodeRouter(), @@ -270,7 +272,6 @@ class ParseServer { const parseGraphQLServer = new ParseGraphQLServer(this, { graphQLPath: options.graphQLPath, playgroundPath: options.playgroundPath, - graphQLSchemaConfig: options.graphQLSchemaConfig, }); if (options.mountGraphQL) { diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js new file mode 100644 index 0000000000..f431a3cfd0 --- /dev/null +++ b/src/Routers/GraphQLRouter.js @@ -0,0 +1,50 @@ +import Parse from 'parse/node'; +import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; + +const GraphQLConfigPath = '/graphql-config'; + +export class GraphQLRouter extends PromiseRouter { + async getGraphQLConfig(req) { + const result = await req.config.graphQLController.getGraphQLConfig(); + return { + response: result, + }; + } + + async updateGraphQLConfig(req) { + if (req.auth.isReadOnly) { + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to update the GraphQL config." + ); + } + const data = await req.config.graphQLController.updateGraphQLConfig( + req.body.params + ); + return { + response: data, + }; + } + + mountRoutes() { + this.route( + 'GET', + GraphQLConfigPath, + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.getGraphQLConfig(req); + } + ); + this.route( + 'PUT', + GraphQLConfigPath, + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.updateGraphQLConfig(req); + } + ); + } +} + +export default GraphQLRouter; From 17145286313f6e50de50133c80fe29be864a4a50 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 01:10:28 +0100 Subject: [PATCH 03/26] fix(GraphQLController): add missing check isEnabled --- src/Controllers/GraphQLController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js index 424ac86451..0794a2b3ae 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/GraphQLController.js @@ -57,6 +57,9 @@ class GraphQLController { async updateGraphQLConfig( graphQLConfig: ParseGraphQLConfig ): Promise { + if(!this.isEnabled) { + throw new Error('GraphQL is not enabled on this application.'); + } // throws if invalid this._validateGraphQLConfig(graphQLConfig); From 078e4ad05f2ca1d1b06563fd806b47ff4581abad Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 09:12:21 +0100 Subject: [PATCH 04/26] chore(GraphQLController): remove awaits from cache put --- src/Controllers/GraphQLController.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js index 0794a2b3ae..dae625e862 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/GraphQLController.js @@ -49,7 +49,7 @@ class GraphQLController { graphQLConfig = results[0][GraphQLConfigKey]; } - await this._putCachedGraphQLConfig(graphQLConfig); + this._putCachedGraphQLConfig(graphQLConfig); return graphQLConfig; } @@ -57,7 +57,7 @@ class GraphQLController { async updateGraphQLConfig( graphQLConfig: ParseGraphQLConfig ): Promise { - if(!this.isEnabled) { + if (!this.isEnabled) { throw new Error('GraphQL is not enabled on this application.'); } // throws if invalid @@ -76,16 +76,16 @@ class GraphQLController { { upsert: true } ); - await this._putCachedGraphQLConfig(graphQLConfig); + this._putCachedGraphQLConfig(graphQLConfig); return { response: { result: true } }; } - async _getCachedGraphQLConfig() { + _getCachedGraphQLConfig() { return this.cacheController.graphQL.get(GraphQLConfigKey); } - async _putCachedGraphQLConfig(graphQLConfig: ParseGraphQLConfig) { + _putCachedGraphQLConfig(graphQLConfig: ParseGraphQLConfig) { return this.cacheController.graphQL.put( GraphQLConfigKey, graphQLConfig, From 8dba72ed8f55e05cb71a03930b5070e04f1ce5c0 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 09:15:25 +0100 Subject: [PATCH 05/26] chore(GraphQLController): remove check for if its enabled --- src/Controllers/GraphQLController.js | 6 ------ src/Controllers/index.js | 5 +---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js index dae625e862..b98428ddad 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/GraphQLController.js @@ -9,12 +9,10 @@ const GraphQLConfigKey = 'config'; class GraphQLController { databaseController: DatabaseController; cacheController: CacheController; - isEnabled: boolean; constructor(params: { databaseController: DatabaseController, cacheController: CacheController, - mountGraphQL: boolean, }) { this.databaseController = params.databaseController || @@ -26,7 +24,6 @@ class GraphQLController { requiredParameter( `GraphQLController requires a "cacheController" to be instantiated.` ); - this.isEnabled = !!params.mountGraphQL; } async getGraphQLConfig(): Promise { @@ -57,9 +54,6 @@ class GraphQLController { async updateGraphQLConfig( graphQLConfig: ParseGraphQLConfig ): Promise { - if (!this.isEnabled) { - throw new Error('GraphQL is not enabled on this application.'); - } // throws if invalid this._validateGraphQLConfig(graphQLConfig); diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 8ef42a68e9..49f317dcc6 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -133,10 +133,7 @@ export function getGraphQLController( options: ParseServerOptions, controllerDeps ): GraphQLController { - return new GraphQLController({ - mountGraphQL: options.mountGraphQL, - ...controllerDeps, - }); + return new GraphQLController(controllerDeps); } export function getAnalyticsController( From 03400f7958eb0e1d9603f146e00f23e850670534 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 09:58:46 +0100 Subject: [PATCH 06/26] refactor(GraphQLController): only use cache if mounted --- src/Controllers/GraphQLController.js | 24 ++++++++++++++---------- src/Controllers/index.js | 5 ++++- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js index b98428ddad..6fa6a011a5 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/GraphQLController.js @@ -9,6 +9,7 @@ const GraphQLConfigKey = 'config'; class GraphQLController { databaseController: DatabaseController; cacheController: CacheController; + isMounted: boolean; constructor(params: { databaseController: DatabaseController, @@ -19,17 +20,16 @@ class GraphQLController { requiredParameter( `GraphQLController requires a "databaseController" to be instantiated.` ); - this.cacheController = - params.cacheController || - requiredParameter( - `GraphQLController requires a "cacheController" to be instantiated.` - ); + this.cacheController = params.cacheController; + this.isMounted = !!params.mountGraphQL; } async getGraphQLConfig(): Promise { - const _cachedConfig = await this._getCachedGraphQLConfig(); - if (_cachedConfig) { - return _cachedConfig; + if (this.isMounted) { + const _cachedConfig = await this._getCachedGraphQLConfig(); + if (_cachedConfig) { + return _cachedConfig; + } } const results = await this.databaseController.find( @@ -46,7 +46,9 @@ class GraphQLController { graphQLConfig = results[0][GraphQLConfigKey]; } - this._putCachedGraphQLConfig(graphQLConfig); + if (this.isMounted) { + this._putCachedGraphQLConfig(graphQLConfig); + } return graphQLConfig; } @@ -70,7 +72,9 @@ class GraphQLController { { upsert: true } ); - this._putCachedGraphQLConfig(graphQLConfig); + if (this.isMounted) { + this._putCachedGraphQLConfig(graphQLConfig); + } return { response: { result: true } }; } diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 49f317dcc6..8ef42a68e9 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -133,7 +133,10 @@ export function getGraphQLController( options: ParseServerOptions, controllerDeps ): GraphQLController { - return new GraphQLController(controllerDeps); + return new GraphQLController({ + mountGraphQL: options.mountGraphQL, + ...controllerDeps, + }); } export function getAnalyticsController( From 7334bd790c9a3596f9f7323cd1e50060ad425108 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 10:05:51 +0100 Subject: [PATCH 07/26] chore(GraphQLController): group all validation errors and throw at once --- src/Controllers/GraphQLController.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js index 6fa6a011a5..8886044a31 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/GraphQLController.js @@ -92,11 +92,11 @@ class GraphQLController { } _validateGraphQLConfig(graphQLConfig: ?ParseGraphQLConfig): void { - let errorMessage: string; + const errorMessages: string = []; if (!graphQLConfig) { - errorMessage = 'cannot be undefined, null or empty.'; + errorMessages.push('cannot be undefined, null or empty.'); } else if (typeof graphQLConfig !== 'object') { - errorMessage = 'must be a valid object.'; + errorMessages.push('must be a valid object.'); } else { const { enabledForClasses, @@ -106,21 +106,21 @@ class GraphQLController { } = graphQLConfig; if (invalidKeys.length) { - errorMessage = `encountered invalid keys: ${invalidKeys}`; + errorMessages.push(`encountered invalid keys: ${invalidKeys}`); } - // TODO use more rigirous structural validations if (enabledForClasses && !Array.isArray(enabledForClasses)) { - errorMessage = `"enabledForClasses" is not a valid array.`; + errorMessages.push(`"enabledForClasses" is not a valid array.`); } if (disabledForClasses && !Array.isArray(disabledForClasses)) { - errorMessage = `"disabledForClasses" is not a valid array.`; + errorMessages.push(`"disabledForClasses" is not a valid array.`); } if (classConfigs && !Array.isArray(classConfigs)) { - errorMessage = `"classConfigs" is not a valid array.`; + errorMessages.push(`"classConfigs" is not a valid array.`); } + // TODO use schematic validation to ensure complete validity of data. } - if (errorMessage) { - throw new Error(`Invalid graphQLConfig: ${errorMessage}`); + if (errorMessages.length) { + throw new Error(`Invalid graphQLConfig: ${errorMessages.join(';')}`); } } } From b10d09bce34dd0dd394bfbfa90228f6abdc16034 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 10:44:04 +0100 Subject: [PATCH 08/26] chore(GraphQLSchema): move transformations into controller validation --- src/Controllers/GraphQLController.js | 183 ++++++++++++++++++++++--- src/GraphQL/loaders/parseClassTypes.js | 59 +------- 2 files changed, 170 insertions(+), 72 deletions(-) diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js index 8886044a31..500d6f07df 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/GraphQLController.js @@ -94,9 +94,9 @@ class GraphQLController { _validateGraphQLConfig(graphQLConfig: ?ParseGraphQLConfig): void { const errorMessages: string = []; if (!graphQLConfig) { - errorMessages.push('cannot be undefined, null or empty.'); + errorMessages.push('cannot be undefined, null or empty'); } else if (typeof graphQLConfig !== 'object') { - errorMessages.push('must be a valid object.'); + errorMessages.push('must be a valid object'); } else { const { enabledForClasses, @@ -109,22 +109,168 @@ class GraphQLController { errorMessages.push(`encountered invalid keys: ${invalidKeys}`); } if (enabledForClasses && !Array.isArray(enabledForClasses)) { - errorMessages.push(`"enabledForClasses" is not a valid array.`); + errorMessages.push(`"enabledForClasses" is not a valid array`); } if (disabledForClasses && !Array.isArray(disabledForClasses)) { - errorMessages.push(`"disabledForClasses" is not a valid array.`); + errorMessages.push(`"disabledForClasses" is not a valid array`); } - if (classConfigs && !Array.isArray(classConfigs)) { - errorMessages.push(`"classConfigs" is not a valid array.`); + if (classConfigs) { + if (Array.isArray(classConfigs)) { + classConfigs.forEach(classConfig => { + const errorMessage = this._validateClassConfig(classConfig); + if (errorMessage) { + errorMessages.push( + `config for ${classConfig.className} is invalid: ${errorMessage}` + ); + } + }); + } else { + errorMessages.push(`"classConfigs" is not a valid array`); + } } - // TODO use schematic validation to ensure complete validity of data. } if (errorMessages.length) { throw new Error(`Invalid graphQLConfig: ${errorMessages.join(';')}`); } } + + _validateClassConfig( + classConfig: ?ParseGraphQLClassConfig + ): string | undefined { + let errorMessage: string; + if (classConfig === null || typeof classConfig !== 'object') { + errorMessage = 'must be a valid object'; + } else { + const { + className, + type = null, + query = null, + mutation = null, + } = classConfig; + if (typeof className !== 'string' || !className.length) { + // TODO consider checking class exists in schema? + errorMessage = `"className" must be a valid string`; + } else if (type !== null) { + if (typeof type !== 'object') { + errorMessage = `"type" must be a valid object`; + } + const { + inputFields = null, + outputFields = null, + constraintFields = null, + sortFields = null, + ...invalidKeys + } = type; + if (invalidKeys.length) { + errorMessage = `"type" contains invalid keys: ${invalidKeys}`; + } else if (outputFields !== null && !isValidStringArray(outputFields)) { + errorMessage = `"outputFields" must be a valid string array`; + } else if ( + constraintFields !== null && + !isValidStringArray(constraintFields) + ) { + errorMessage = `"constraintFields" must be a valid string array`; + } else if (sortFields !== null) { + if (Array.isArray(sortFields)) { + sortFields.every((sortField, index) => { + if (sortField === null || typeof sortField !== 'object') { + errorMessage = `"sortField" at index ${index} is not a valid object`; + return false; + } else { + const { field, asc, desc, ...invalidKeys } = sortField; + if (invalidKeys.length) { + errorMessage = `"sortField" at index ${index} contains invalid keys: ${invalidKeys}`; + return false; + } else { + if (typeof field !== 'string') { + errorMessage = `"sortField" at index ${index} did not provide the "field" as a string`; + return false; + } else if ( + typeof asc !== 'boolean' || + typeof desc !== 'boolean' + ) { + errorMessage = `"sortField" at index ${index} did not provide "asc" or "desc" as booleans`; + return false; + } + } + } + }); + } else { + errorMessage = `"sortFields" must be a valid array.`; + } + } else if (inputFields !== null) { + if (typeof inputFields !== 'object') { + const { + create = null, + update = null, + ...invalidKeys + } = inputFields; + if (invalidKeys.length) { + errorMessage = `"inputFields" contains invalid keys: ${invalidKeys}`; + } else { + if (update !== null && !isValidStringArray(update)) { + errorMessage = `"inputFields.update" must be a valid string array`; + } else if (create !== null) { + if (!isValidStringArray(create)) { + errorMessage = `"inputFields.create" must be a valid string array`; + } else if (className === '_User') { + if ( + !create.includes('username') || + !create.includes('password') + ) { + errorMessage = `"inputFields.create" must include required fields, username and password`; + } + } + } + } + } else { + errorMessage = `"inputFields" must be a valid object.`; + } + } + } else if (query !== null) { + if (typeof query !== 'object') { + const { find = null, get = null, ...invalidKeys } = query; + if (invalidKeys.length) { + errorMessage = `"query" contains invalid keys: ${invalidKeys}`; + } else if (find !== null && typeof find !== 'boolean') { + errorMessage = `"query.find" must be a boolean`; + } else if (get !== null && typeof get !== 'boolean') { + errorMessage = `"query.get" must be a boolean`; + } + } else { + errorMessage = `"query" must be a valid object`; + } + } else if (mutation !== null) { + if (typeof mutation !== 'object') { + const { + create = null, + update = null, + destroy = null, + ...invalidKeys + } = mutation; + if (invalidKeys.length) { + errorMessage = `"query" contains invalid keys: ${invalidKeys}`; + } else if (create !== null && typeof create !== 'boolean') { + errorMessage = `"query.create" must be a boolean`; + } else if (update !== null && typeof update !== 'boolean') { + errorMessage = `"query.update" must be a boolean`; + } else if (destroy !== null && typeof destroy !== 'boolean') { + errorMessage = `"query.destroy" must be a boolean`; + } + } else { + errorMessage = `"mutation" must be a valid object`; + } + } + } + + return errorMessage; + } } +const isValidStringArray = function(array): boolean { + return Array.isArray(array) ? !array.some(s => typeof s !== 'string') : false; +}; + export interface ParseGraphQLConfig { enabledForClasses?: string[]; disabledForClasses?: string[]; @@ -136,19 +282,21 @@ export interface ParseGraphQLClassConfig { /* The `type` object contains options for how the class types are generated */ type: ?{ /* Fields that are allowed when creating or updating an object. */ - inputFields: - | ?(string[]) - | ?{ - /* Leave blank to allow all available fields in the schema. */ - create?: string[], - update?: string[], - }, + inputFields: ?{ + /* Leave blank to allow all available fields in the schema. */ + create?: string[], + update?: string[], + }, /* Fields on the edges that can be resolved from a query, i.e. the Result Type. */ outputFields: ?(string[]), /* Fields by which a query can be filtered, i.e. the `where` object. */ constraintFields: ?(string[]), - /* Fields by which a query can be sorted; suffix with _ASC or _DESC to enforce direction. */ - sortFields: ?(string[]), + /* Fields by which a query can be sorted; */ + sortFields: ?({ + field: string, + asc: boolean, + desc: boolean, + }[]), }; /* The `query` object contains options for which class queries are generated */ query: ?{ @@ -159,7 +307,8 @@ export interface ParseGraphQLClassConfig { mutation: ?{ create: ?boolean, update: ?boolean, - delete: ?boolean, + // delete is a reserved key word in js + destroy: ?boolean, }; } diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 5f7f007928..f5a33135a4 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -162,29 +162,16 @@ const extractKeysAndInclude = selectedFields => { return { keys, include }; }; -const getParseClassTypeConfig = ( +const getParseClassTypeConfig = function( parseClassConfig: ?ParseGraphQLClassConfig -) => { - if (parseClassConfig) { - const { type } = parseClassConfig; - const { inputFields } = type; - if (Array.isArray(inputFields)) { - type.inputFields = { - create: inputFields, - update: inputFields, - }; - } - return type; - } else { - return {}; - } +) { + return parseClassConfig || {}; }; const getInputFieldsAndConstraints = function( parseClass, parseClassConfig: ?ParseGraphQLClassConfig ) { - const { className } = parseClass; const classFields = Object.keys(parseClass.fields); const { inputFields: allowedInputFields, @@ -208,16 +195,6 @@ const getInputFieldsAndConstraints = function( classCreateFields = classCustomFields.filter(field => { return allowedInputFields.create.includes(field); }); - if (className === '_User') { - // _User createFields is used for Sign Up, and - // so username and password must be added to create. - if (classCreateFields.indexOf('username') === -1) { - classCreateFields.push('username'); - } - if (classCreateFields.indexOf('password') === -1) { - classCreateFields.push('password'); - } - } } else { classCreateFields = classCustomFields; } @@ -246,35 +223,7 @@ const getInputFieldsAndConstraints = function( } if (allowedSortFields) { - const getSortFieldAndDirection = (fieldConfig: string): string => { - if (fieldConfig.endsWith('_ASC')) { - return { - field: fieldConfig.substr(fieldConfig.length - 4), - asc: true, - }; - } else if (fieldConfig.endsWith('_DESC')) { - return { - field: fieldConfig.substr(fieldConfig.length - 5), - desc: true, - }; - } else { - return { - field: fieldConfig, - asc: true, - desc: true, - }; - } - }; - classSortFields = []; - allowedSortFields.forEach(fieldConfig => { - const { field, ...direction } = getSortFieldAndDirection(fieldConfig); - if (classCustomFields.includes(field)) { - classSortFields.push({ - field, - ...direction, - }); - } - }); + classSortFields = allowedSortFields; if (!classSortFields.length) { // must have at least 1 order field // otherwise the FindArgs Input Type will throw. From eef0b2b3535e11d16bd2a233b67b015eea2fdbcf Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 11:46:51 +0100 Subject: [PATCH 09/26] refactor(GraphQL): improve ctrl validation and fix schema usage of config --- src/Controllers/GraphQLController.js | 145 +++++++++++++-------- src/GraphQL/loaders/parseClassMutations.js | 18 +-- src/GraphQL/loaders/parseClassQueries.js | 14 +- src/GraphQL/loaders/parseClassTypes.js | 2 +- 4 files changed, 101 insertions(+), 78 deletions(-) diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/GraphQLController.js index 500d6f07df..045e692a0e 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/GraphQLController.js @@ -95,32 +95,34 @@ class GraphQLController { const errorMessages: string = []; if (!graphQLConfig) { errorMessages.push('cannot be undefined, null or empty'); - } else if (typeof graphQLConfig !== 'object') { + } else if (!isValidSimpleObject(graphQLConfig)) { errorMessages.push('must be a valid object'); } else { const { - enabledForClasses, - disabledForClasses, - classConfigs, + enabledForClasses = null, + disabledForClasses = null, + classConfigs = null, ...invalidKeys } = graphQLConfig; if (invalidKeys.length) { - errorMessages.push(`encountered invalid keys: ${invalidKeys}`); + errorMessages.push( + `encountered invalid keys: [${Object.keys(invalidKeys)}]` + ); } - if (enabledForClasses && !Array.isArray(enabledForClasses)) { + if (enabledForClasses !== null && !Array.isArray(enabledForClasses)) { errorMessages.push(`"enabledForClasses" is not a valid array`); } - if (disabledForClasses && !Array.isArray(disabledForClasses)) { + if (disabledForClasses !== null && !Array.isArray(disabledForClasses)) { errorMessages.push(`"disabledForClasses" is not a valid array`); } - if (classConfigs) { + if (classConfigs !== null) { if (Array.isArray(classConfigs)) { classConfigs.forEach(classConfig => { const errorMessage = this._validateClassConfig(classConfig); if (errorMessage) { errorMessages.push( - `config for ${classConfig.className} is invalid: ${errorMessage}` + `classConfig:${classConfig.className} is invalid because ${errorMessage}` ); } }); @@ -130,16 +132,13 @@ class GraphQLController { } } if (errorMessages.length) { - throw new Error(`Invalid graphQLConfig: ${errorMessages.join(';')}`); + throw new Error(`Invalid graphQLConfig: ${errorMessages.join('; ')}`); } } - _validateClassConfig( - classConfig: ?ParseGraphQLClassConfig - ): string | undefined { - let errorMessage: string; - if (classConfig === null || typeof classConfig !== 'object') { - errorMessage = 'must be a valid object'; + _validateClassConfig(classConfig: ?ParseGraphQLClassConfig): string | void { + if (!isValidSimpleObject(classConfig)) { + return 'it must be a valid object'; } else { const { className, @@ -149,10 +148,11 @@ class GraphQLController { } = classConfig; if (typeof className !== 'string' || !className.length) { // TODO consider checking class exists in schema? - errorMessage = `"className" must be a valid string`; - } else if (type !== null) { - if (typeof type !== 'object') { - errorMessage = `"type" must be a valid object`; + return `"className" must be a valid string`; + } + if (type !== null) { + if (!isValidSimpleObject(type)) { + return `"type" must be a valid object`; } const { inputFields = null, @@ -161,25 +161,29 @@ class GraphQLController { sortFields = null, ...invalidKeys } = type; - if (invalidKeys.length) { - errorMessage = `"type" contains invalid keys: ${invalidKeys}`; + if (Object.keys(invalidKeys).length) { + return `"type" contains invalid keys, [${Object.keys(invalidKeys)}]`; } else if (outputFields !== null && !isValidStringArray(outputFields)) { - errorMessage = `"outputFields" must be a valid string array`; + return `"outputFields" must be a valid string array`; } else if ( constraintFields !== null && !isValidStringArray(constraintFields) ) { - errorMessage = `"constraintFields" must be a valid string array`; - } else if (sortFields !== null) { + return `"constraintFields" must be a valid string array`; + } + if (sortFields !== null) { if (Array.isArray(sortFields)) { + let errorMessage; sortFields.every((sortField, index) => { - if (sortField === null || typeof sortField !== 'object') { + if (!isValidSimpleObject(sortField)) { errorMessage = `"sortField" at index ${index} is not a valid object`; return false; } else { const { field, asc, desc, ...invalidKeys } = sortField; - if (invalidKeys.length) { - errorMessage = `"sortField" at index ${index} contains invalid keys: ${invalidKeys}`; + if (Object.keys(invalidKeys).length) { + errorMessage = `"sortField" at index ${index} contains invalid keys, [${Object.keys( + invalidKeys + )}]`; return false; } else { if (typeof field !== 'string') { @@ -194,82 +198,109 @@ class GraphQLController { } } } + return true; }); + if (errorMessage) { + return errorMessage; + } } else { - errorMessage = `"sortFields" must be a valid array.`; + return `"sortFields" must be a valid array.`; } - } else if (inputFields !== null) { - if (typeof inputFields !== 'object') { + } + if (inputFields !== null) { + if (isValidSimpleObject(inputFields)) { const { create = null, update = null, ...invalidKeys } = inputFields; - if (invalidKeys.length) { - errorMessage = `"inputFields" contains invalid keys: ${invalidKeys}`; + if (Object.keys(invalidKeys).length) { + return `"inputFields" contains invalid keys: [${Object.keys( + invalidKeys + )}]`; } else { if (update !== null && !isValidStringArray(update)) { - errorMessage = `"inputFields.update" must be a valid string array`; + return `"inputFields.update" must be a valid string array`; } else if (create !== null) { if (!isValidStringArray(create)) { - errorMessage = `"inputFields.create" must be a valid string array`; + return `"inputFields.create" must be a valid string array`; } else if (className === '_User') { if ( !create.includes('username') || !create.includes('password') ) { - errorMessage = `"inputFields.create" must include required fields, username and password`; + return `"inputFields.create" must include required fields, username and password`; } } } } } else { - errorMessage = `"inputFields" must be a valid object.`; + return `"inputFields" must be a valid object`; } } - } else if (query !== null) { - if (typeof query !== 'object') { + } + if (query !== null) { + if (isValidSimpleObject(query)) { const { find = null, get = null, ...invalidKeys } = query; - if (invalidKeys.length) { - errorMessage = `"query" contains invalid keys: ${invalidKeys}`; + if (Object.keys(invalidKeys).length) { + return `"query" contains invalid keys, [${Object.keys( + invalidKeys + )}]`; } else if (find !== null && typeof find !== 'boolean') { - errorMessage = `"query.find" must be a boolean`; + return `"query.find" must be a boolean`; } else if (get !== null && typeof get !== 'boolean') { - errorMessage = `"query.get" must be a boolean`; + return `"query.get" must be a boolean`; } } else { - errorMessage = `"query" must be a valid object`; + return `"query" must be a valid object`; } - } else if (mutation !== null) { - if (typeof mutation !== 'object') { + } + if (mutation !== null) { + if (isValidSimpleObject(mutation)) { const { create = null, update = null, destroy = null, ...invalidKeys } = mutation; - if (invalidKeys.length) { - errorMessage = `"query" contains invalid keys: ${invalidKeys}`; - } else if (create !== null && typeof create !== 'boolean') { - errorMessage = `"query.create" must be a boolean`; - } else if (update !== null && typeof update !== 'boolean') { - errorMessage = `"query.update" must be a boolean`; - } else if (destroy !== null && typeof destroy !== 'boolean') { - errorMessage = `"query.destroy" must be a boolean`; + if (Object.keys(invalidKeys).length) { + return `"mutation" contains invalid keys, [${Object.keys( + invalidKeys + )}]`; + } + if (create !== null && typeof create !== 'boolean') { + return `"mutation.create" must be a boolean`; + } + if (update !== null && typeof update !== 'boolean') { + return `"mutation.update" must be a boolean`; + } + if (destroy !== null && typeof destroy !== 'boolean') { + return `"mutation.destroy" must be a boolean`; } } else { - errorMessage = `"mutation" must be a valid object`; + return `"mutation" must be a valid object`; } } } - - return errorMessage; } } const isValidStringArray = function(array): boolean { return Array.isArray(array) ? !array.some(s => typeof s !== 'string') : false; }; +/** + * Ensures the obj is a simple JSON/{} + * object, i.e. not an array, null, date + * etc. + */ +const isValidSimpleObject = function(obj): boolean { + return ( + typeof obj === 'object' && + !Array.isArray(obj) && + obj !== null && + obj instanceof Date !== true + ); +}; export interface ParseGraphQLConfig { enabledForClasses?: string[]; diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index e6d910c4de..6094525064 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -3,26 +3,22 @@ import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsMutations from './objectsMutations'; import { ParseGraphQLClassConfig } from '../../Options/index'; -const getParseClassMutationConfig = ( +const getParseClassMutationConfig = function( parseClassConfig: ?ParseGraphQLClassConfig -) => { - if (parseClassConfig) { - return parseClassConfig.mutation || {}; - } else { - return {}; - } +) { + return parseClassConfig ? parseClassConfig.mutation : {}; }; -const load = ( +const load = function( parseGraphQLSchema, parseClass, parseClassConfig: ?ParseGraphQLClassConfig -) => { +) { const { className } = parseClass; const { create: isCreateEnabled = true, update: isUpdateEnabled = true, - delete: isDeleteEnabled = true, + destroy: isDestroyEnabled = true, } = getParseClassMutationConfig(parseClassConfig); const { @@ -128,7 +124,7 @@ const load = ( }; } - if (isDeleteEnabled) { + if (isDestroyEnabled) { const deleteGraphQLMutationName = `delete${className}`; parseGraphQLSchema.graphQLObjectsMutations[deleteGraphQLMutationName] = { description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`, diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index db88ac48b8..536dd06e46 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -5,21 +5,17 @@ import * as objectsQueries from './objectsQueries'; import * as parseClassTypes from './parseClassTypes'; import { ParseGraphQLClassConfig } from '../../Options/index'; -const getParseClassQueryConfig = ( +const getParseClassQueryConfig = function( parseClassConfig: ?ParseGraphQLClassConfig -) => { - if (parseClassConfig) { - return parseClassConfig.query || {}; - } else { - return {}; - } +) { + return parseClassConfig ? parseClassConfig.query : {}; }; -const load = ( +const load = function( parseGraphQLSchema, parseClass, parseClassConfig: ?ParseGraphQLClassConfig -) => { +) { const { className } = parseClass; const { get: isGetEnabled = true, diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index f5a33135a4..c55a44ccaa 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -165,7 +165,7 @@ const extractKeysAndInclude = selectedFields => { const getParseClassTypeConfig = function( parseClassConfig: ?ParseGraphQLClassConfig ) { - return parseClassConfig || {}; + return parseClassConfig ? parseClassConfig.type : {}; }; const getInputFieldsAndConstraints = function( From 206f8bc9a4a615839121b1f887c53c0235b4a6f3 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 11:52:24 +0100 Subject: [PATCH 10/26] refactor(GraphQLSchema): remove code related to additional schema This code has been moved into a separate feature branch. --- src/GraphQL/ParseGraphQLSchema.js | 79 ++----------------------------- 1 file changed, 3 insertions(+), 76 deletions(-) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 1861971eb0..35bcc64f1c 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -74,22 +74,20 @@ class ParseGraphQLSchema { let graphQLQuery = undefined; if (Object.keys(this.graphQLQueries).length > 0) { - const fields = this._getAllQueries(); graphQLQuery = new GraphQLObjectType({ name: 'Query', description: 'Query is the top level type for queries.', - fields, + fields: this.graphQLQueries, }); this.graphQLTypes.push(graphQLQuery); } let graphQLMutation = undefined; if (Object.keys(this.graphQLMutations).length > 0) { - const fields = this._getAllMutations(); graphQLMutation = new GraphQLObjectType({ name: 'Mutation', description: 'Mutation is the top level type for mutations.', - fields, + fields: this.graphQLMutations, }); this.graphQLTypes.push(graphQLMutation); } @@ -105,7 +103,7 @@ class ParseGraphQLSchema { } this.graphQLSchema = new GraphQLSchema({ - types: this._mergeAdditionalTypes(this.graphQLTypes), + types: this.graphQLTypes, query: graphQLQuery, mutation: graphQLMutation, subscription: graphQLSubscription, @@ -189,77 +187,6 @@ class ParseGraphQLSchema { return [parseClass, parseClassConfig]; }); } - - _storeAdditionalSchema() { - const { additionalSchemaResolver: resolver } = this.parseGraphQLConfig; - if (resolver) { - this.additionalSchema = resolver(); - } - } - - _getAllQueries() { - const { graphQLQueries: baseQueryFields, additionalSchema } = this; - if (additionalSchema && additionalSchema.query) { - const { query: additionalQueryFields } = additionalSchema; - const reservedKeys = ['users', 'objects', 'health']; - if ( - Object.keys(additionalQueryFields).some(key => - reservedKeys.includes(key) - ) - ) { - throw new Error( - `Additional graphql schema cannot use reserved query fields: ${reservedKeys}` - ); - } - return { - ...baseQueryFields, - ...additionalQueryFields, - }; - } else { - return baseQueryFields; - } - } - - _getAllMutations() { - const { graphQLMutations: baseMutationFields, additionalSchema } = this; - if (additionalSchema && additionalSchema.mutation) { - const { mutation: additionalMutationFields } = additionalSchema; - const reservedKeys = ['users', 'objects']; - if ( - Object.keys(additionalMutationFields).some(key => - reservedKeys.includes(key) - ) - ) { - throw new Error( - `Additional graphql schema cannot use reserved mutation fields: ${reservedKeys}` - ); - } - return { - ...baseMutationFields, - ...additionalMutationFields, - }; - } else { - return baseMutationFields; - } - } - - _mergeAdditionalTypes(baseTypes) { - const { additionalSchema } = this; - if (additionalSchema && Array.isArray(additionalSchema.types)) { - const { types: additionalTypes } = additionalSchema; - if (additionalTypes.length) { - const mergedTypes = [...baseTypes]; - const baseTypeNames = baseTypes.map(type => type.name); - additionalTypes.forEach(additionalType => { - if (!baseTypeNames.includes(additionalType.name)) { - mergedTypes.push(additionalType); - } - }); - return mergedTypes; - } - } - return baseTypes; - } } export { ParseGraphQLSchema }; From a2fb12ec062c15d7695b24b5fa19b1b521a6e75f Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 12:14:46 +0100 Subject: [PATCH 11/26] fix(GraphQLSchema): fix incorrect default return type for class configs --- src/GraphQL/loaders/parseClassMutations.js | 2 +- src/GraphQL/loaders/parseClassQueries.js | 2 +- src/GraphQL/loaders/parseClassTypes.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 6094525064..deaeade99d 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -6,7 +6,7 @@ import { ParseGraphQLClassConfig } from '../../Options/index'; const getParseClassMutationConfig = function( parseClassConfig: ?ParseGraphQLClassConfig ) { - return parseClassConfig ? parseClassConfig.mutation : {}; + return (parseClassConfig && parseClassConfig.mutation) || {}; }; const load = function( diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 536dd06e46..92242be94c 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -8,7 +8,7 @@ import { ParseGraphQLClassConfig } from '../../Options/index'; const getParseClassQueryConfig = function( parseClassConfig: ?ParseGraphQLClassConfig ) { - return parseClassConfig ? parseClassConfig.query : {}; + return (parseClassConfig && parseClassConfig.query) || {}; }; const load = function( diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index c55a44ccaa..bc14be1a52 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -165,7 +165,7 @@ const extractKeysAndInclude = selectedFields => { const getParseClassTypeConfig = function( parseClassConfig: ?ParseGraphQLClassConfig ) { - return parseClassConfig ? parseClassConfig.type : {}; + return (parseClassConfig && parseClassConfig.type) || {}; }; const getInputFieldsAndConstraints = function( From 5a7004d633a026844e09ca56e10f92e5bc844055 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 12 Jul 2019 12:15:20 +0100 Subject: [PATCH 12/26] refactor(GraphQLSchema): update staleness check code to account for config --- src/GraphQL/ParseGraphQLSchema.js | 73 +++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 35bcc64f1c..1c98ec2173 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -8,12 +8,15 @@ import * as parseClassQueries from './loaders/parseClassQueries'; import * as parseClassMutations from './loaders/parseClassMutations'; import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; -import GraphQLController from '../Controllers/GraphQLController'; +import GraphQLController, { + ParseGraphQLConfig, +} from '../Controllers/GraphQLController'; import DatabaseController from '../Controllers/DatabaseController'; class ParseGraphQLSchema { databaseController: DatabaseController; graphQLController: GraphQLController; + parseGraphQLConfig: ParseGraphQLConfig; constructor(params: { databaseController: DatabaseController, @@ -31,24 +34,25 @@ class ParseGraphQLSchema { } async load() { - await this._initializeSchemaAndConfig(); + const { parseGraphQLConfig } = await this._initializeSchemaAndConfig(); - const parseClasses = await this._getClassesForSchema(); + const parseClasses = await this._getClassesForSchema(parseGraphQLConfig); const parseClassesString = JSON.stringify(parseClasses); - if (this.graphQLSchema) { - if (this.parseClasses === parseClasses) { - return this.graphQLSchema; - } - - if (this.parseClassesString === parseClassesString) { - this.parseClasses = parseClasses; - return this.graphQLSchema; - } + if ( + this.graphQLSchema && + !this._hasSchemaInputChanged({ + parseClasses, + parseClassesString, + parseGraphQLConfig, + }) + ) { + return this.graphQLSchema; } this.parseClasses = parseClasses; this.parseClassesString = parseClassesString; + this.parseGraphQLConfig = parseGraphQLConfig; this.parseClassTypes = {}; this.meType = null; this.graphQLSchema = null; @@ -61,7 +65,7 @@ class ParseGraphQLSchema { defaultGraphQLTypes.load(this); - this._getParseClassesWithConfig(parseClasses).forEach( + this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach( ([parseClass, parseClassConfig]) => { parseClassTypes.load(this, parseClass, parseClassConfig); parseClassQueries.load(this, parseClass, parseClassConfig); @@ -133,15 +137,18 @@ class ParseGraphQLSchema { ]); this.schemaController = schemaController; - this.parseGraphQLConfig = parseGraphQLConfig || {}; + + return { + parseGraphQLConfig: parseGraphQLConfig || {}, + }; } /** * Gets all classes found by the `schemaController` * minus those filtered out by the app's parseGraphQLConfig. */ - async _getClassesForSchema() { - const { enabledForClasses, disabledForClasses } = this.parseGraphQLConfig; + async _getClassesForSchema(parseGraphQLConfig: ParseGraphQLConfig) { + const { enabledForClasses, disabledForClasses } = parseGraphQLConfig; const allClasses = await this.schemaController.getAllClasses(); if (Array.isArray(enabledForClasses) || Array.isArray(disabledForClasses)) { @@ -175,8 +182,11 @@ class ParseGraphQLSchema { * that provide the parseClass along with * its parseClassConfig where provided. */ - _getParseClassesWithConfig(parseClasses) { - const { classConfigs } = this.parseGraphQLConfig; + _getParseClassesWithConfig( + parseClasses, + parseGraphQLConfig: ParseGraphQLConfig + ) { + const { classConfigs } = parseGraphQLConfig; return parseClasses.map(parseClass => { let parseClassConfig; if (classConfigs) { @@ -187,6 +197,33 @@ class ParseGraphQLSchema { return [parseClass, parseClassConfig]; }); } + + /** + * Checks for changes to the parseClasses + * objects (i.e. database schema) or to + * the parseGraphQLConfig object. If no + * changes are found, return true; + */ + _hasSchemaInputChanged(params: { + parseClasses: any, + parseClassesString: string, + parseGraphQLConfig: ?ParseGraphQLConfig, + }): boolean { + const { parseClasses, parseClassesString, parseGraphQLConfig } = params; + + if (this.parseGraphQLConfig === parseGraphQLConfig) { + if (this.parseClasses === parseClasses) { + return false; + } + + if (this.parseClassesString === parseClassesString) { + this.parseClasses = parseClasses; + return false; + } + } + + return true; + } } export { ParseGraphQLSchema }; From eb7356c9ac55f8d669ca172a8048cd7193be259f Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Sat, 13 Jul 2019 14:21:37 +0100 Subject: [PATCH 13/26] fix(GraphQLServer): fix regressed tests due to internal schema changes This will be followed up with a backwards compatability fix for the `ClassFields` issue to avoid breakages for our users --- spec/ParseGraphQLSchema.spec.js | 28 ++++++++++++++------- spec/ParseGraphQLServer.spec.js | 42 +++++++++++++++---------------- src/GraphQL/ParseGraphQLSchema.js | 19 ++++++++------ 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/spec/ParseGraphQLSchema.spec.js b/spec/ParseGraphQLSchema.spec.js index 7437183f37..951bbbbe64 100644 --- a/spec/ParseGraphQLSchema.spec.js +++ b/spec/ParseGraphQLSchema.spec.js @@ -4,6 +4,7 @@ const { ParseGraphQLSchema } = require('../lib/GraphQL/ParseGraphQLSchema'); describe('ParseGraphQLSchema', () => { let parseServer; let databaseController; + let graphQLController; let parseGraphQLSchema; beforeAll(async () => { @@ -11,28 +12,37 @@ describe('ParseGraphQLSchema', () => { schemaCacheTTL: 100, }); databaseController = parseServer.config.databaseController; - parseGraphQLSchema = new ParseGraphQLSchema( + graphQLController = parseServer.config.graphQLController; + parseGraphQLSchema = new ParseGraphQLSchema({ databaseController, - defaultLogger - ); + graphQLController, + log: defaultLogger, + }); }); describe('constructor', () => { - it('should require a databaseController and a log instance', () => { + it('should require a graphQLController, databaseController and a log instance', () => { expect(() => new ParseGraphQLSchema()).toThrow( - 'You must provide a databaseController instance!' + 'You must provide a graphQLController instance!' ); - expect(() => new ParseGraphQLSchema({})).toThrow( - 'You must provide a log instance!' + expect(() => new ParseGraphQLSchema({ graphQLController: {} })).toThrow( + 'You must provide a databaseController instance!' ); - expect(() => new ParseGraphQLSchema({}, {})).not.toThrow(); + expect( + () => + new ParseGraphQLSchema({ + graphQLController: {}, + databaseController: {}, + }) + ).toThrow('You must provide a log instance!'); }); }); describe('load', () => { it('should cache schema', async () => { const graphQLSchema = await parseGraphQLSchema.load(); - expect(graphQLSchema).toBe(await parseGraphQLSchema.load()); + const updatedGraphQLSchema = await parseGraphQLSchema.load(); + expect(graphQLSchema).toBe(updatedGraphQLSchema); await new Promise(resolve => setTimeout(resolve, 200)); expect(graphQLSchema).toBe(await parseGraphQLSchema.load()); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 5b3839026f..ac40a66a6a 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -2242,7 +2242,7 @@ describe('ParseGraphQLServer', () => { const result = await apolloClient.mutate({ mutation: gql` - mutation CreateCustomer($fields: CustomerFields) { + mutation CreateCustomer($fields: CustomerCreateFields) { objects { createCustomer(fields: $fields) { objectId @@ -2408,7 +2408,7 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation UpdateCustomer( $objectId: ID! - $fields: CustomerFields + $fields: CustomerUpdateFields ) { objects { updateCustomer(objectId: $objectId, fields: $fields) { @@ -2624,7 +2624,7 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation UpdateSomeObject( $objectId: ID! - $fields: ${className}Fields + $fields: ${className}UpdateFields ) { objects { update${className}( @@ -3300,7 +3300,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3367,7 +3367,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3440,7 +3440,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3513,7 +3513,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3601,7 +3601,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3732,8 +3732,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateChildObject( - $fields1: ChildClassFields - $fields2: ChildClassFields + $fields1: ChildClassCreateFields + $fields2: ChildClassCreateFields ) { objects { createChildClass1: createChildClass(fields: $fields1) { @@ -3862,7 +3862,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateMainObject($fields: MainClassFields) { + mutation CreateMainObject($fields: MainClassCreateFields) { objects { createMainClass(fields: $fields) { objectId @@ -4050,8 +4050,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject( - $fields1: SomeClassFields - $fields2: SomeClassFields + $fields1: SomeClassCreateFields + $fields2: SomeClassCreateFields ) { objects { createSomeClass1: createSomeClass(fields: $fields1) { @@ -4151,7 +4151,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4219,7 +4219,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4362,8 +4362,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject( - $fields1: SomeClassFields - $fields2: SomeClassFields + $fields1: SomeClassCreateFields + $fields2: SomeClassCreateFields ) { objects { createSomeClass1: createSomeClass(fields: $fields1) { @@ -4445,7 +4445,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4522,7 +4522,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4649,7 +4649,7 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4692,7 +4692,7 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation UpdateSomeObject( $objectId: ID! - $fields: SomeClassFields + $fields: SomeClassUpdateFields ) { objects { updateSomeClass(objectId: $objectId, fields: $fields) { diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 1c98ec2173..aa14af8ce6 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -18,11 +18,13 @@ class ParseGraphQLSchema { graphQLController: GraphQLController; parseGraphQLConfig: ParseGraphQLConfig; - constructor(params: { - databaseController: DatabaseController, - graphQLController: GraphQLController, - log: any, - }) { + constructor( + params: { + databaseController: DatabaseController, + graphQLController: GraphQLController, + log: any, + } = {} + ) { this.graphQLController = params.graphQLController || requiredParameter('You must provide a graphQLController instance!'); @@ -139,7 +141,7 @@ class ParseGraphQLSchema { this.schemaController = schemaController; return { - parseGraphQLConfig: parseGraphQLConfig || {}, + parseGraphQLConfig, }; } @@ -211,7 +213,10 @@ class ParseGraphQLSchema { }): boolean { const { parseClasses, parseClassesString, parseGraphQLConfig } = params; - if (this.parseGraphQLConfig === parseGraphQLConfig) { + if ( + JSON.stringify(this.parseGraphQLConfig) === + JSON.stringify(parseGraphQLConfig) + ) { if (this.parseClasses === parseClasses) { return false; } From df8aa90bbb5d46362b0cda74488aefedb32b889f Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Sun, 14 Jul 2019 22:14:06 +0100 Subject: [PATCH 14/26] refactor: rename to ParseGraphQLController for consistency --- spec/ParseGraphQLSchema.spec.js | 18 +++++++++--------- ...Controller.js => ParseGraphQLController.js} | 6 +++--- src/Controllers/index.js | 12 ++++++------ src/GraphQL/ParseGraphQLSchema.js | 16 ++++++++-------- src/GraphQL/ParseGraphQLServer.js | 12 ++++++------ src/Routers/GraphQLRouter.js | 4 ++-- 6 files changed, 34 insertions(+), 34 deletions(-) rename src/Controllers/{GraphQLController.js => ParseGraphQLController.js} (98%) diff --git a/spec/ParseGraphQLSchema.spec.js b/spec/ParseGraphQLSchema.spec.js index 951bbbbe64..e9b4f20ee5 100644 --- a/spec/ParseGraphQLSchema.spec.js +++ b/spec/ParseGraphQLSchema.spec.js @@ -4,7 +4,7 @@ const { ParseGraphQLSchema } = require('../lib/GraphQL/ParseGraphQLSchema'); describe('ParseGraphQLSchema', () => { let parseServer; let databaseController; - let graphQLController; + let parseGraphQLController; let parseGraphQLSchema; beforeAll(async () => { @@ -12,26 +12,26 @@ describe('ParseGraphQLSchema', () => { schemaCacheTTL: 100, }); databaseController = parseServer.config.databaseController; - graphQLController = parseServer.config.graphQLController; + parseGraphQLController = parseServer.config.parseGraphQLController; parseGraphQLSchema = new ParseGraphQLSchema({ databaseController, - graphQLController, + parseGraphQLController, log: defaultLogger, }); }); describe('constructor', () => { - it('should require a graphQLController, databaseController and a log instance', () => { + it('should require a parseGraphQLController, databaseController and a log instance', () => { expect(() => new ParseGraphQLSchema()).toThrow( - 'You must provide a graphQLController instance!' - ); - expect(() => new ParseGraphQLSchema({ graphQLController: {} })).toThrow( - 'You must provide a databaseController instance!' + 'You must provide a parseGraphQLController instance!' ); + expect( + () => new ParseGraphQLSchema({ parseGraphQLController: {} }) + ).toThrow('You must provide a databaseController instance!'); expect( () => new ParseGraphQLSchema({ - graphQLController: {}, + parseGraphQLController: {}, databaseController: {}, }) ).toThrow('You must provide a log instance!'); diff --git a/src/Controllers/GraphQLController.js b/src/Controllers/ParseGraphQLController.js similarity index 98% rename from src/Controllers/GraphQLController.js rename to src/Controllers/ParseGraphQLController.js index 045e692a0e..6d9f82df2e 100644 --- a/src/Controllers/GraphQLController.js +++ b/src/Controllers/ParseGraphQLController.js @@ -6,7 +6,7 @@ const GraphQLConfigClass = '_GraphQLConfig'; const GraphQLConfigId = '1'; const GraphQLConfigKey = 'config'; -class GraphQLController { +class ParseGraphQLController { databaseController: DatabaseController; cacheController: CacheController; isMounted: boolean; @@ -18,7 +18,7 @@ class GraphQLController { this.databaseController = params.databaseController || requiredParameter( - `GraphQLController requires a "databaseController" to be instantiated.` + `ParseGraphQLController requires a "databaseController" to be instantiated.` ); this.cacheController = params.cacheController; this.isMounted = !!params.mountGraphQL; @@ -343,4 +343,4 @@ export interface ParseGraphQLClassConfig { }; } -export default GraphQLController; +export default ParseGraphQLController; diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 8ef42a68e9..b016d265bc 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -25,7 +25,7 @@ import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter'; import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter'; import ParsePushAdapter from '@parse/push-adapter'; -import GraphQLController from './GraphQLController'; +import ParseGraphQLController from './ParseGraphQLController'; export function getControllers(options: ParseServerOptions) { const loggerController = getLoggerController(options); @@ -44,7 +44,7 @@ export function getControllers(options: ParseServerOptions) { const databaseController = getDatabaseController(options, cacheController); const hooksController = getHooksController(options, databaseController); const authDataManager = getAuthDataManager(options); - const graphQLController = getGraphQLController(options, { + const parseGraphQLController = getParseGraphQLController(options, { databaseController, cacheController, }); @@ -59,7 +59,7 @@ export function getControllers(options: ParseServerOptions) { pushControllerQueue, analyticsController, cacheController, - graphQLController, + parseGraphQLController, liveQueryController, databaseController, hooksController, @@ -129,11 +129,11 @@ export function getCacheController( return new CacheController(cacheControllerAdapter, appId); } -export function getGraphQLController( +export function getParseGraphQLController( options: ParseServerOptions, controllerDeps -): GraphQLController { - return new GraphQLController({ +): ParseGraphQLController { + return new ParseGraphQLController({ mountGraphQL: options.mountGraphQL, ...controllerDeps, }); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index aa14af8ce6..8c581cc81a 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -8,26 +8,26 @@ import * as parseClassQueries from './loaders/parseClassQueries'; import * as parseClassMutations from './loaders/parseClassMutations'; import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries'; import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations'; -import GraphQLController, { +import ParseGraphQLController, { ParseGraphQLConfig, -} from '../Controllers/GraphQLController'; +} from '../Controllers/ParseGraphQLController'; import DatabaseController from '../Controllers/DatabaseController'; class ParseGraphQLSchema { databaseController: DatabaseController; - graphQLController: GraphQLController; + parseGraphQLController: ParseGraphQLController; parseGraphQLConfig: ParseGraphQLConfig; constructor( params: { databaseController: DatabaseController, - graphQLController: GraphQLController, + parseGraphQLController: ParseGraphQLController, log: any, } = {} ) { - this.graphQLController = - params.graphQLController || - requiredParameter('You must provide a graphQLController instance!'); + this.parseGraphQLController = + params.parseGraphQLController || + requiredParameter('You must provide a parseGraphQLController instance!'); this.databaseController = params.databaseController || requiredParameter('You must provide a databaseController instance!'); @@ -135,7 +135,7 @@ class ParseGraphQLSchema { async _initializeSchemaAndConfig() { const [schemaController, parseGraphQLConfig] = await Promise.all([ this.databaseController.loadSchema(), - this.graphQLController.getGraphQLConfig(), + this.parseGraphQLController.getGraphQLConfig(), ]); this.schemaController = schemaController; diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index a19d9e1a24..d5ede04cd1 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -9,12 +9,12 @@ import { handleParseHeaders } from '../middlewares'; import requiredParameter from '../requiredParameter'; import defaultLogger from '../logger'; import { ParseGraphQLSchema } from './ParseGraphQLSchema'; -import GraphQLController, { +import ParseGraphQLController, { ParseGraphQLConfig, -} from '../Controllers/GraphQLController'; +} from '../Controllers/ParseGraphQLController'; class ParseGraphQLServer { - graphQLController: GraphQLController; + parseGraphQLController: ParseGraphQLController; constructor(parseServer, config) { this.parseServer = @@ -24,9 +24,9 @@ class ParseGraphQLServer { requiredParameter('You must provide a config.graphQLPath!'); } this.config = config; - this.graphQLController = this.parseServer.config.graphQLController; + this.parseGraphQLController = this.parseServer.config.parseGraphQLController; this.parseGraphQLSchema = new ParseGraphQLSchema({ - graphQLController: this.graphQLController, + parseGraphQLController: this.parseGraphQLController, databaseController: this.parseServer.config.databaseController, log: (this.parseServer.config && this.parseServer.config.loggerController) || @@ -119,7 +119,7 @@ class ParseGraphQLServer { } async setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { - await this.graphQLController.updateGraphQLConfig(graphQLConfig); + await this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); } } diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index f431a3cfd0..cdf2565926 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -6,7 +6,7 @@ const GraphQLConfigPath = '/graphql-config'; export class GraphQLRouter extends PromiseRouter { async getGraphQLConfig(req) { - const result = await req.config.graphQLController.getGraphQLConfig(); + const result = await req.config.parseGraphQLController.getGraphQLConfig(); return { response: result, }; @@ -19,7 +19,7 @@ export class GraphQLRouter extends PromiseRouter { "read-only masterKey isn't allowed to update the GraphQL config." ); } - const data = await req.config.graphQLController.updateGraphQLConfig( + const data = await req.config.parseGraphQLController.updateGraphQLConfig( req.body.params ); return { From 8f87292cc1ce9b40fb6043df3568427452d14c8f Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Tue, 16 Jul 2019 11:12:25 +0100 Subject: [PATCH 15/26] fix(ParseGraphQLCtrl): numerous fixes for validity checking Also includes some minor code refactoring --- src/Controllers/ParseGraphQLController.js | 71 ++++++++++++++++------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/src/Controllers/ParseGraphQLController.js b/src/Controllers/ParseGraphQLController.js index 6d9f82df2e..ccc858b741 100644 --- a/src/Controllers/ParseGraphQLController.js +++ b/src/Controllers/ParseGraphQLController.js @@ -2,7 +2,7 @@ import requiredParameter from '../../lib/requiredParameter'; import DatabaseController from './DatabaseController'; import CacheController from './CacheController'; -const GraphQLConfigClass = '_GraphQLConfig'; +const GraphQLConfigClassName = '_GraphQLConfig'; const GraphQLConfigId = '1'; const GraphQLConfigKey = 'config'; @@ -10,11 +10,14 @@ class ParseGraphQLController { databaseController: DatabaseController; cacheController: CacheController; isMounted: boolean; + configCacheKey: string; - constructor(params: { - databaseController: DatabaseController, - cacheController: CacheController, - }) { + constructor( + params: { + databaseController: DatabaseController, + cacheController: CacheController, + } = {} + ) { this.databaseController = params.databaseController || requiredParameter( @@ -22,6 +25,7 @@ class ParseGraphQLController { ); this.cacheController = params.cacheController; this.isMounted = !!params.mountGraphQL; + this.configCacheKey = GraphQLConfigKey; } async getGraphQLConfig(): Promise { @@ -33,7 +37,7 @@ class ParseGraphQLController { } const results = await this.databaseController.find( - GraphQLConfigClass, + GraphQLConfigClassName, { objectId: GraphQLConfigId }, { limit: 1 } ); @@ -57,16 +61,25 @@ class ParseGraphQLController { graphQLConfig: ParseGraphQLConfig ): Promise { // throws if invalid - this._validateGraphQLConfig(graphQLConfig); + this._validateGraphQLConfig( + graphQLConfig || requiredParameter('You must provide a graphQLConfig!') + ); // Transform in dot notation to make sure it works - const update = Object.keys(graphQLConfig).reduce((acc, key) => { - acc[`${GraphQLConfigKey}.${key}`] = graphQLConfig[key]; - return acc; - }, {}); + const update = Object.keys(graphQLConfig).reduce( + (acc, key) => { + return { + ...acc, + [GraphQLConfigKey]: { + [key]: graphQLConfig[key], + }, + }; + }, + { [GraphQLConfigKey]: {} } + ); await this.databaseController.update( - GraphQLConfigClass, + GraphQLConfigClassName, { objectId: GraphQLConfigId }, update, { upsert: true } @@ -80,12 +93,12 @@ class ParseGraphQLController { } _getCachedGraphQLConfig() { - return this.cacheController.graphQL.get(GraphQLConfigKey); + return this.cacheController.graphQL.get(this.configCacheKey); } _putCachedGraphQLConfig(graphQLConfig: ParseGraphQLConfig) { return this.cacheController.graphQL.put( - GraphQLConfigKey, + this.configCacheKey, graphQLConfig, 60000 ); @@ -105,15 +118,21 @@ class ParseGraphQLController { ...invalidKeys } = graphQLConfig; - if (invalidKeys.length) { + if (Object.keys(invalidKeys).length) { errorMessages.push( `encountered invalid keys: [${Object.keys(invalidKeys)}]` ); } - if (enabledForClasses !== null && !Array.isArray(enabledForClasses)) { + if ( + enabledForClasses !== null && + !isValidStringArray(enabledForClasses) + ) { errorMessages.push(`"enabledForClasses" is not a valid array`); } - if (disabledForClasses !== null && !Array.isArray(disabledForClasses)) { + if ( + disabledForClasses !== null && + !isValidStringArray(disabledForClasses) + ) { errorMessages.push(`"disabledForClasses" is not a valid array`); } if (classConfigs !== null) { @@ -145,8 +164,14 @@ class ParseGraphQLController { type = null, query = null, mutation = null, + ...invalidKeys } = classConfig; - if (typeof className !== 'string' || !className.length) { + if (Object.keys(invalidKeys).length) { + return `"invalidKeys" [${Object.keys( + invalidKeys + )}] should not be present`; + } + if (typeof className !== 'string' || !className.trim().length) { // TODO consider checking class exists in schema? return `"className" must be a valid string`; } @@ -186,7 +211,7 @@ class ParseGraphQLController { )}]`; return false; } else { - if (typeof field !== 'string') { + if (typeof field !== 'string' || field.trim().length === 0) { errorMessage = `"sortField" at index ${index} did not provide the "field" as a string`; return false; } else if ( @@ -286,7 +311,9 @@ class ParseGraphQLController { } const isValidStringArray = function(array): boolean { - return Array.isArray(array) ? !array.some(s => typeof s !== 'string') : false; + return Array.isArray(array) + ? !array.some(s => typeof s !== 'string' || s.trim().length < 1) + : false; }; /** * Ensures the obj is a simple JSON/{} @@ -298,7 +325,8 @@ const isValidSimpleObject = function(obj): boolean { typeof obj === 'object' && !Array.isArray(obj) && obj !== null && - obj instanceof Date !== true + obj instanceof Date !== true && + obj instanceof Promise !== true ); }; @@ -344,3 +372,4 @@ export interface ParseGraphQLClassConfig { } export default ParseGraphQLController; +export { GraphQLConfigClassName, GraphQLConfigId, GraphQLConfigKey }; From 5ef997c90f16b0a9ca2cfde28d47e898a0d54687 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Tue, 16 Jul 2019 11:12:52 +0100 Subject: [PATCH 16/26] chore(GraphQL): minor syntax cleanup From 1f9a7a63a6ac03ab9a341fc3cea4d40311854a2f Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Tue, 16 Jul 2019 11:13:09 +0100 Subject: [PATCH 17/26] fix(SchemaController): add _GraphQLConfig to volatile classes --- src/Controllers/SchemaController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index e3dfc040b9..a6410fc14d 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -163,6 +163,7 @@ const volatileClasses = Object.freeze([ '_PushStatus', '_Hooks', '_GlobalConfig', + '_GraphQLConfig', '_JobSchedule', '_Audience', ]); From 543b3196d74096276c0a6f734b4715771ca0b6f0 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Tue, 16 Jul 2019 11:13:47 +0100 Subject: [PATCH 18/26] refactor(ParseGraphQLServer): return update config value in setGraphQLConfig --- src/GraphQL/ParseGraphQLServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index d5ede04cd1..5b9e8c1afe 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -118,8 +118,8 @@ class ParseGraphQLServer { ); } - async setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { - await this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); + setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { + return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); } } From 091e92fc716af216c7ca6cf5b785571d6c912e39 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Tue, 16 Jul 2019 11:16:54 +0100 Subject: [PATCH 19/26] testing(ParseGraphQL): add test cases for new graphQLConfig --- spec/ParseGraphQLController.spec.js | 941 ++++++++++++++++++++++++++++ spec/ParseGraphQLSchema.spec.js | 74 ++- spec/ParseGraphQLServer.spec.js | 859 +++++++++++++++++++++++++ 3 files changed, 1860 insertions(+), 14 deletions(-) create mode 100644 spec/ParseGraphQLController.spec.js diff --git a/spec/ParseGraphQLController.spec.js b/spec/ParseGraphQLController.spec.js new file mode 100644 index 0000000000..1b17443649 --- /dev/null +++ b/spec/ParseGraphQLController.spec.js @@ -0,0 +1,941 @@ +const { + default: ParseGraphQLController, + GraphQLConfigClassName, + GraphQLConfigId, + GraphQLConfigKey, +} = require('../lib/Controllers/ParseGraphQLController'); +const { isEqual } = require('lodash'); + +describe('ParseGraphQLController', () => { + let parseServer; + let databaseController; + let cacheController; + + // Holds the graphQLConfig in memory instead of using the db + let graphQLConfigRecord; + + const setConfigOnDb = graphQLConfigData => { + graphQLConfigRecord = { + objectId: GraphQLConfigId, + [GraphQLConfigKey]: graphQLConfigData, + }; + }; + const removeConfigFromDb = () => { + graphQLConfigRecord = null; + }; + const getConfigFromDb = () => { + return graphQLConfigRecord; + }; + + beforeAll(async () => { + parseServer = await global.reconfigureServer({ + schemaCacheTTL: 100, + }); + databaseController = parseServer.config.databaseController; + cacheController = parseServer.config.cacheController; + + const defaultFind = databaseController.find.bind(databaseController); + databaseController.find = async (className, query, ...args) => { + if ( + className === GraphQLConfigClassName && + isEqual(query, { objectId: GraphQLConfigId }) + ) { + const graphQLConfigRecord = getConfigFromDb(); + return graphQLConfigRecord ? [graphQLConfigRecord] : []; + } else { + return defaultFind(className, query, ...args); + } + }; + + const defaultUpdate = databaseController.update.bind(databaseController); + databaseController.update = async ( + className, + query, + update, + fullQueryOptions + ) => { + if ( + className === GraphQLConfigClassName && + isEqual(query, { objectId: GraphQLConfigId }) && + update && + !!update[GraphQLConfigKey] && + fullQueryOptions && + isEqual(fullQueryOptions, { upsert: true }) + ) { + setConfigOnDb(update[GraphQLConfigKey]); + } else { + return defaultUpdate(className, query, update, fullQueryOptions); + } + }; + }); + + describe('constructor', () => { + it('should require a databaseController', () => { + expect(() => new ParseGraphQLController()).toThrow( + 'ParseGraphQLController requires a "databaseController" to be instantiated.' + ); + expect(() => new ParseGraphQLController({ cacheController })).toThrow( + 'ParseGraphQLController requires a "databaseController" to be instantiated.' + ); + expect( + () => + new ParseGraphQLController({ + cacheController, + mountGraphQL: false, + }) + ).toThrow( + 'ParseGraphQLController requires a "databaseController" to be instantiated.' + ); + }); + it('should construct without a cacheController', () => { + expect( + () => + new ParseGraphQLController({ + databaseController, + }) + ).not.toThrow(); + expect( + () => + new ParseGraphQLController({ + databaseController, + mountGraphQL: true, + }) + ).not.toThrow(); + }); + it('should set isMounted to true if config.mountGraphQL is true', () => { + const mountedController = new ParseGraphQLController({ + databaseController, + mountGraphQL: true, + }); + expect(mountedController.isMounted).toBe(true); + const unmountedController = new ParseGraphQLController({ + databaseController, + mountGraphQL: false, + }); + expect(unmountedController.isMounted).toBe(false); + const unmountedController2 = new ParseGraphQLController({ + databaseController, + }); + expect(unmountedController2.isMounted).toBe(false); + }); + }); + + describe('getGraphQLConfig', () => { + it('should return an empty graphQLConfig if collection has none', async () => { + removeConfigFromDb(); + + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + mountGraphQL: false, + }); + + const graphQLConfig = await parseGraphQLController.getGraphQLConfig(); + expect(graphQLConfig).toEqual({}); + }); + it('should return an existing graphQLConfig', async () => { + setConfigOnDb({ enabledForClasses: ['_User'] }); + + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + mountGraphQL: false, + }); + const graphQLConfig = await parseGraphQLController.getGraphQLConfig(); + expect(graphQLConfig).toEqual({ enabledForClasses: ['_User'] }); + }); + it('should use the cache if mounted, and return the stored graphQLConfig', async () => { + removeConfigFromDb(); + cacheController.graphQL.clear(); + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + cacheController, + mountGraphQL: true, + }); + cacheController.graphQL.put(parseGraphQLController.configCacheKey, { + enabledForClasses: ['SuperCar'], + }); + + const graphQLConfig = await parseGraphQLController.getGraphQLConfig(); + expect(graphQLConfig).toEqual({ enabledForClasses: ['SuperCar'] }); + }); + it('should use the database when mounted and cache is empty', async () => { + setConfigOnDb({ disabledForClasses: ['SuperCar'] }); + cacheController.graphQL.clear(); + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + cacheController, + mountGraphQL: true, + }); + const graphQLConfig = await parseGraphQLController.getGraphQLConfig(); + expect(graphQLConfig).toEqual({ disabledForClasses: ['SuperCar'] }); + }); + it('should store the graphQLConfig in cache if mounted', async () => { + setConfigOnDb({ enabledForClasses: ['SuperCar'] }); + cacheController.graphQL.clear(); + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + cacheController, + mountGraphQL: true, + }); + const cachedValueBefore = await cacheController.graphQL.get( + parseGraphQLController.configCacheKey + ); + expect(cachedValueBefore).toBeNull(); + await parseGraphQLController.getGraphQLConfig(); + const cachedValueAfter = await cacheController.graphQL.get( + parseGraphQLController.configCacheKey + ); + expect(cachedValueAfter).toEqual({ enabledForClasses: ['SuperCar'] }); + }); + }); + + describe('updateGraphQLConfig', () => { + const successfulUpdateResponse = { response: { result: true } }; + + it('should throw if graphQLConfig is not provided', async function() { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig() + ).toBeRejectedWith('You must provide a graphQLConfig!'); + }); + it('should throw if graphQLConfig is not an object', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig([]) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig(function() {}) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig(Promise.resolve({})) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig('') + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({}) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if graphQLConfig has an invalid root key', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ invalidKey: true }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({}) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if graphQLConfig has invalid class filters', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ enabledForClasses: {} }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + enabledForClasses: [undefined], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + disabledForClasses: [null], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + enabledForClasses: ['_User', null], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ disabledForClasses: [''] }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + enabledForClasses: [], + disabledForClasses: ['_User'], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if classConfigs array is invalid', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ classConfigs: {} }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ classConfigs: [null] }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [undefined], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [{ className: 'ValidClass' }, null], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ classConfigs: [] }) + ).toBeResolvedTo(successfulUpdateResponse); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if a classConfig has invalid type settings', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: [], + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + invalidKey: true, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: {}, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if a classConfig has invalid type.inputFields settings', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: [], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: { + invalidKey: true, + }, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: { + create: {}, + }, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: { + update: [null], + }, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: { + create: [], + update: [], + }, + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: { + create: ['make', 'model'], + update: [], + }, + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if a classConfig has invalid type.outputFields settings', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + outputFields: {}, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + outputFields: [null], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + outputFields: ['name', undefined], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + outputFields: [''], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + outputFields: [], + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + outputFields: ['name'], + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if a classConfig has invalid type.constraintFields settings', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + constraintFields: {}, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + constraintFields: [null], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + constraintFields: ['name', undefined], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + constraintFields: [''], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + constraintFields: [], + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + constraintFields: ['name'], + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if a classConfig has invalid type.sortFields settings', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: {}, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: [null], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: [ + { + field: undefined, + asc: true, + desc: true, + }, + ], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: [ + { + field: '', + asc: true, + desc: false, + }, + ], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: [ + { + field: 'name', + asc: true, + desc: 'false', + }, + ], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: [ + { + field: 'name', + asc: true, + desc: true, + }, + null, + ], + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: [], + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + sortFields: [ + { + field: 'name', + asc: true, + desc: true, + }, + ], + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if a classConfig has invalid query params', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + query: [], + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + query: { + invalidKey: true, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + query: { + get: 1, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + query: { + find: 'true', + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + query: { + get: false, + find: true, + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + query: {}, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + it('should throw if a classConfig has invalid mutation params', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + mutation: [], + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + mutation: { + invalidKey: true, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + mutation: { + destroy: 1, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + mutation: { + update: 'true', + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + mutation: {}, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + mutation: { + create: true, + update: true, + destroy: false, + }, + }, + ], + }) + ).toBeResolvedTo(successfulUpdateResponse); + }); + + it('should throw if _User create fields is missing username or password', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + inputFields: { + create: ['username', 'no-password'], + }, + }, + }, + ], + }) + ).toBeRejected(); + expectAsync( + parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: '_User', + type: { + inputFields: { + create: ['username', 'password'], + }, + }, + }, + ], + }) + ).toBeResolved(successfulUpdateResponse); + }); + it('should update the cache if mounted', async () => { + removeConfigFromDb(); + cacheController.graphQL.clear(); + const mountedController = new ParseGraphQLController({ + databaseController, + cacheController, + mountGraphQL: true, + }); + const unmountedController = new ParseGraphQLController({ + databaseController, + cacheController, + mountGraphQL: false, + }); + + let cacheBeforeValue; + let cacheAfterValue; + + cacheBeforeValue = await cacheController.graphQL.get( + mountedController.configCacheKey + ); + expect(cacheBeforeValue).toBeNull(); + + await mountedController.updateGraphQLConfig({ + enabledForClasses: ['SuperCar'], + }); + cacheAfterValue = await cacheController.graphQL.get( + mountedController.configCacheKey + ); + expect(cacheAfterValue).toEqual({ enabledForClasses: ['SuperCar'] }); + + // reset + removeConfigFromDb(); + cacheController.graphQL.clear(); + + cacheBeforeValue = await cacheController.graphQL.get( + unmountedController.configCacheKey + ); + expect(cacheBeforeValue).toBeNull(); + + await unmountedController.updateGraphQLConfig({ + enabledForClasses: ['SuperCar'], + }); + cacheAfterValue = await cacheController.graphQL.get( + unmountedController.configCacheKey + ); + expect(cacheAfterValue).toBeNull(); + }); + }); +}); diff --git a/spec/ParseGraphQLSchema.spec.js b/spec/ParseGraphQLSchema.spec.js index e9b4f20ee5..e39e8782ec 100644 --- a/spec/ParseGraphQLSchema.spec.js +++ b/spec/ParseGraphQLSchema.spec.js @@ -50,26 +50,72 @@ describe('ParseGraphQLSchema', () => { it('should load a brand new GraphQL Schema if Parse Schema changes', async () => { await parseGraphQLSchema.load(); const parseClasses = parseGraphQLSchema.parseClasses; - const parseClassesString = parseGraphQLSchema.parseClasses; - const parseClassTypes = parseGraphQLSchema.parseClasses; - const graphQLSchema = parseGraphQLSchema.parseClasses; - const graphQLTypes = parseGraphQLSchema.parseClasses; - const graphQLQueries = parseGraphQLSchema.parseClasses; - const graphQLMutations = parseGraphQLSchema.parseClasses; - const graphQLSubscriptions = parseGraphQLSchema.parseClasses; + const parseClassesString = parseGraphQLSchema.parseClassesString; + const parseClassTypes = parseGraphQLSchema.parseClassTypes; + const graphQLSchema = parseGraphQLSchema.graphQLSchema; + const graphQLTypes = parseGraphQLSchema.graphQLTypes; + const graphQLQueries = parseGraphQLSchema.graphQLQueries; + const graphQLMutations = parseGraphQLSchema.graphQLMutations; + const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions; const newClassObject = new Parse.Object('NewClass'); await newClassObject.save(); await databaseController.schemaCache.clear(); await new Promise(resolve => setTimeout(resolve, 200)); await parseGraphQLSchema.load(); expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses); - expect(parseClassesString).not.toBe(parseGraphQLSchema.parseClasses); - expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClasses); - expect(graphQLSchema).not.toBe(parseGraphQLSchema.parseClasses); - expect(graphQLTypes).not.toBe(parseGraphQLSchema.parseClasses); - expect(graphQLQueries).not.toBe(parseGraphQLSchema.parseClasses); - expect(graphQLMutations).not.toBe(parseGraphQLSchema.parseClasses); - expect(graphQLSubscriptions).not.toBe(parseGraphQLSchema.parseClasses); + expect(parseClassesString).not.toBe( + parseGraphQLSchema.parseClassesString + ); + expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes); + expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema); + expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes); + expect(graphQLQueries).not.toBe(parseGraphQLSchema.graphQLQueries); + expect(graphQLMutations).not.toBe(parseGraphQLSchema.graphQLMutations); + expect(graphQLSubscriptions).not.toBe( + parseGraphQLSchema.graphQLSubscriptions + ); + }); + + it('should load a brand new GraphQL Schema if graphQLConfig changes', async () => { + const parseGraphQLController = { + graphQLConfig: { enabledForClasses: [] }, + getGraphQLConfig() { + return this.graphQLConfig; + }, + }; + const parseGraphQLSchema = new ParseGraphQLSchema({ + databaseController, + parseGraphQLController, + log: defaultLogger, + }); + await parseGraphQLSchema.load(); + const parseClasses = parseGraphQLSchema.parseClasses; + const parseClassesString = parseGraphQLSchema.parseClassesString; + const parseClassTypes = parseGraphQLSchema.parseClassTypes; + const graphQLSchema = parseGraphQLSchema.graphQLSchema; + const graphQLTypes = parseGraphQLSchema.graphQLTypes; + const graphQLQueries = parseGraphQLSchema.graphQLQueries; + const graphQLMutations = parseGraphQLSchema.graphQLMutations; + const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions; + + parseGraphQLController.graphQLConfig = { + enabledForClasses: ['_User'], + }; + + await new Promise(resolve => setTimeout(resolve, 200)); + await parseGraphQLSchema.load(); + expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses); + expect(parseClassesString).not.toBe( + parseGraphQLSchema.parseClassesString + ); + expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes); + expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema); + expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes); + expect(graphQLQueries).not.toBe(parseGraphQLSchema.graphQLQueries); + expect(graphQLMutations).not.toBe(parseGraphQLSchema.graphQLMutations); + expect(graphQLSubscriptions).not.toBe( + parseGraphQLSchema.graphQLSubscriptions + ); }); }); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index ac40a66a6a..cf3cf1ab4b 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -189,6 +189,47 @@ describe('ParseGraphQLServer', () => { }); }); + describe('setGraphQLConfig', () => { + let parseGraphQLServer; + beforeEach(() => { + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: 'graphql', + }); + }); + it('should pass the graphQLConfig onto the parseGraphQLController', async () => { + let received; + parseGraphQLServer.parseGraphQLController = { + async updateGraphQLConfig(graphQLConfig) { + received = graphQLConfig; + return {}; + }, + }; + const graphQLConfig = { enabledForClasses: [] }; + await parseGraphQLServer.setGraphQLConfig(graphQLConfig); + expect(received).toBe(graphQLConfig); + }); + it('should not absorb exceptions from parseGraphQLController', async () => { + parseGraphQLServer.parseGraphQLController = { + async updateGraphQLConfig() { + throw new Error('Network request failed'); + }, + }; + await expectAsync( + parseGraphQLServer.setGraphQLConfig({}) + ).toBeRejectedWith(new Error('Network request failed')); + }); + it('should return the response from parseGraphQLController', async () => { + parseGraphQLServer.parseGraphQLController = { + async updateGraphQLConfig() { + return { response: { result: true } }; + }, + }; + await expectAsync(parseGraphQLServer.setGraphQLConfig({})).toBeResolvedTo( + { response: { result: true } } + ); + }); + }); + describe('API', () => { const headers = { 'X-Parse-Application-Id': 'test', @@ -721,6 +762,824 @@ describe('ParseGraphQLServer', () => { }); }); + describe('Configuration', function() { + const resetGraphQLCache = async () => { + await Promise.all([ + parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), + parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), + ]); + }; + + beforeEach(async () => { + await parseGraphQLServer.setGraphQLConfig({}); + await resetGraphQLCache(); + }); + + it('should only include types in the enabledForClasses list', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SuperCar', { + foo: { type: 'String' }, + }); + + const graphQLConfig = { + enabledForClasses: ['SuperCar'], + }; + await parseGraphQLServer.setGraphQLConfig(graphQLConfig); + await resetGraphQLCache(); + + const { data } = await apolloClient.query({ + query: gql` + query UserType { + userType: __type(name: "_UserClass") { + fields { + name + } + } + superCarType: __type(name: "SuperCarClass") { + fields { + name + } + } + } + `, + }); + expect(data.userType).toBeNull(); + expect(data.superCarType).toBeTruthy(); + }); + it('should not include types in the disabledForClasses list', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SuperCar', { + foo: { type: 'String' }, + }); + + const graphQLConfig = { + disabledForClasses: ['SuperCar'], + }; + await parseGraphQLServer.setGraphQLConfig(graphQLConfig); + await resetGraphQLCache(); + + const { data } = await apolloClient.query({ + query: gql` + query UserType { + userType: __type(name: "_UserClass") { + fields { + name + } + } + superCarType: __type(name: "SuperCarClass") { + fields { + name + } + } + } + `, + }); + expect(data.superCarType).toBeNull(); + expect(data.userType).toBeTruthy(); + }); + it('should remove query operations when disabled', async () => { + const superCar = new Parse.Object('SuperCar'); + await superCar.save({ foo: 'bar' }); + const customer = new Parse.Object('Customer'); + await customer.save({ foo: 'bar' }); + + await expectAsync( + apolloClient.query({ + query: gql` + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + objectId + } + } + } + `, + variables: { + objectId: superCar.id, + }, + }) + ).toBeResolved(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindCustomer { + objects { + findCustomer { + count + } + } + } + `, + }) + ).toBeResolved(); + + const graphQLConfig = { + classConfigs: [ + { + className: 'SuperCar', + query: { + get: false, + find: true, + }, + }, + { + className: 'Customer', + query: { + get: true, + find: false, + }, + }, + ], + }; + await parseGraphQLServer.setGraphQLConfig(graphQLConfig); + await resetGraphQLCache(); + + await expectAsync( + apolloClient.query({ + query: gql` + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + objectId + } + } + } + `, + variables: { + objectId: superCar.id, + }, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + query GetCustomer($objectId: ID!) { + objects { + getCustomer(objectId: $objectId) { + objectId + } + } + } + `, + variables: { + objectId: customer.id, + }, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar { + count + } + } + } + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindCustomer { + objects { + findCustomer { + count + } + } + } + `, + }) + ).toBeRejected(); + }); + + it('should remove mutation operations, create, update and delete, when disabled', async () => { + const superCar1 = new Parse.Object('SuperCar'); + await superCar1.save({ foo: 'bar' }); + const customer1 = new Parse.Object('Customer'); + await customer1.save({ foo: 'bar' }); + + await expectAsync( + apolloClient.query({ + query: gql` + mutation UpdateSuperCar($objectId: ID!, $foo: String!) { + objects { + updateSuperCar(objectId: $objectId, fields: { foo: $foo }) { + updatedAt + } + } + } + `, + variables: { + objectId: superCar1.id, + foo: 'lah', + }, + }) + ).toBeResolved(); + + await expectAsync( + apolloClient.query({ + query: gql` + mutation DeleteCustomer($objectId: ID!) { + objects { + deleteCustomer(objectId: $objectId) + } + } + `, + variables: { + objectId: customer1.id, + }, + }) + ).toBeResolved(); + + const { data: customerData } = await apolloClient.query({ + query: gql` + mutation CreateCustomer($foo: String!) { + objects { + createCustomer(fields: { foo: $foo }) { + objectId + } + } + } + `, + variables: { + foo: 'rah', + }, + }); + expect(customerData.objects.createCustomer).toBeTruthy(); + + // used later + const customer2Id = customerData.objects.createCustomer.objectId; + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + mutation: { + create: true, + update: false, + destroy: true, + }, + }, + { + className: 'Customer', + mutation: { + create: false, + update: true, + destroy: false, + }, + }, + ], + }); + await resetGraphQLCache(); + + const { data: superCarData } = await apolloClient.query({ + query: gql` + mutation CreateSuperCar($foo: String!) { + objects { + createSuperCar(fields: { foo: $foo }) { + objectId + } + } + } + `, + variables: { + foo: 'mah', + }, + }); + expect(superCarData.objects.createSuperCar).toBeTruthy(); + const superCar3Id = superCarData.objects.createSuperCar.objectId; + + await expectAsync( + apolloClient.query({ + query: gql` + mutation UpdateSupercar($objectId: ID!, $foo: String!) { + objects { + updateSuperCar(objectId: $objectId, fields: { foo: $foo }) { + updatedAt + } + } + } + `, + variables: { + objectId: superCar3Id, + }, + }) + ).toBeRejected(); + + await expectAsync( + apolloClient.query({ + query: gql` + mutation DeleteSuperCar($objectId: ID!) { + objects { + deleteSuperCar(objectId: $objectId) + } + } + `, + variables: { + objectId: superCar3Id, + }, + }) + ).toBeResolved(); + + await expectAsync( + apolloClient.query({ + query: gql` + mutation CreateCustomer($foo: String!) { + objects { + createCustomer(fields: { foo: $foo }) { + objectId + } + } + } + `, + variables: { + foo: 'rah', + }, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + mutation UpdateCustomer($objectId: ID!, $foo: String!) { + objects { + updateCustomer(objectId: $objectId, fields: { foo: $foo }) { + updatedAt + } + } + } + `, + variables: { + objectId: customer2Id, + foo: 'tah', + }, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + mutation DeleteCustomer($objectId: ID!, $foo: String!) { + objects { + deleteCustomer(objectId: $objectId) + } + } + `, + variables: { + objectId: customer2Id, + }, + }) + ).toBeRejected(); + }); + it('should only allow the supplied create and update fields for a class', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SuperCar', { + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + }); + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: { + create: ['engine', 'doors', 'price'], + update: ['price', 'mileage'], + }, + }, + }, + ], + }); + + await resetGraphQLCache(); + + await expectAsync( + apolloClient.query({ + query: gql` + mutation InvalidCreateSuperCar { + objects { + createSuperCar( + fields: { engine: "diesel", mileage: 1000 } + ) { + objectId + } + } + } + `, + }) + ).toBeRejected(); + const { objectId: superCarId } = (await apolloClient.query({ + query: gql` + mutation ValidCreateSuperCar { + objects { + createSuperCar( + fields: { engine: "diesel", doors: 5, price: "£10000" } + ) { + objectId + } + } + } + `, + })).data.objects.createSuperCar; + + expect(superCarId).toBeTruthy(); + + await expectAsync( + apolloClient.query({ + query: gql` + mutation InvalidUpdateSuperCar($objectId: ID!) { + objects { + updateSuperCar( + objectId: $objectId + fields: { engine: "petrol" } + ) { + updatedAt + } + } + } + `, + variables: { + objectId: superCarId, + }, + }) + ).toBeRejected(); + + const updatedSuperCar = (await apolloClient.query({ + query: gql` + mutation ValidUpdateSuperCar($objectId: ID!) { + objects { + updateSuperCar( + objectId: $objectId + fields: { mileage: 2000 } + ) { + updatedAt + } + } + } + `, + variables: { + objectId: superCarId, + }, + })).data.objects.updateSuperCar; + expect(updatedSuperCar).toBeTruthy(); + }); + + it('should only allow the supplied output fields for a class', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + + await schemaController.addClassIfNotExists('SuperCar', { + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + insuranceClaims: { type: 'Number' }, + }); + + const superCar = await new Parse.Object('SuperCar').save({ + engine: 'petrol', + doors: 3, + price: '£7500', + mileage: 0, + insuranceCertificate: 'private-file.pdf', + }); + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + outputFields: ['engine', 'doors', 'price', 'mileage'], + }, + }, + ], + }); + + await resetGraphQLCache(); + + await expectAsync( + apolloClient.query({ + query: gql` + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + objectId + engine + doors + price + mileage + insuranceCertificate + } + } + } + `, + variables: { + objectId: superCar.id, + }, + }) + ).toBeRejected(); + let getSuperCar = (await apolloClient.query({ + query: gql` + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + objectId + engine + doors + price + mileage + } + } + } + `, + variables: { + objectId: superCar.id, + }, + })).data.objects.getSuperCar; + expect(getSuperCar).toBeTruthy(); + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + outputFields: [], + }, + }, + ], + }); + + await resetGraphQLCache(); + await expectAsync( + apolloClient.query({ + query: gql` + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + engine + } + } + } + `, + variables: { + objectId: superCar.id, + }, + }) + ).toBeRejected(); + getSuperCar = (await apolloClient.query({ + query: gql` + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + objectId + } + } + } + `, + variables: { + objectId: superCar.id, + }, + })).data.objects.getSuperCar; + expect(getSuperCar.objectId).toBe(superCar.id); + }); + it('should only allow the supplied constraint fields for a class', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + + await schemaController.addClassIfNotExists('SuperCar', { + model: { type: 'String' }, + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + insuranceCertificate: { type: 'String' }, + }); + + await new Parse.Object('SuperCar').save({ + model: 'McLaren', + engine: 'petrol', + doors: 3, + price: '£7500', + mileage: 0, + insuranceCertificate: 'private-file.pdf', + }); + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + constraintFields: ['engine', 'doors', 'price'], + }, + }, + ], + }); + + await resetGraphQLCache(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar( + where: { + insuranceCertificate: { _eq: "private-file.pdf" } + } + ) { + count + } + } + } + `, + }) + ).toBeRejected(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(where: { mileage: { _eq: 0 } }) { + count + } + } + } + `, + }) + ).toBeRejected(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(where: { engine: { _eq: "petrol" } }) { + count + } + } + } + `, + }) + ).toBeResolved(); + }); + it('should only allow the supplied sort fields for a class', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + + await schemaController.addClassIfNotExists('SuperCar', { + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + }); + + await new Parse.Object('SuperCar').save({ + engine: 'petrol', + doors: 3, + price: '£7500', + mileage: 0, + }); + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + sortFields: [ + { + field: 'doors', + asc: true, + desc: true, + }, + { + field: 'price', + asc: true, + desc: true, + }, + { + field: 'mileage', + asc: true, + desc: false, + }, + ], + }, + }, + ], + }); + + await resetGraphQLCache(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(order: [engine_ASC]) { + results { + objectId + } + } + } + } + `, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(order: [engine_DESC]) { + results { + objectId + } + } + } + } + `, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(order: [mileage_DESC]) { + results { + objectId + } + } + } + } + `, + }) + ).toBeRejected(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(order: [mileage_ASC]) { + results { + objectId + } + } + } + } + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(order: [doors_ASC]) { + results { + objectId + } + } + } + } + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(order: [price_DESC]) { + results { + objectId + } + } + } + } + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(order: [price_ASC, doors_DESC]) { + results { + objectId + } + } + } + } + `, + }) + ).toBeResolved(); + }); + }); + describe('Objects Queries', () => { describe('Get', () => { it('should return a class object using generic query', async () => { From a77d2c25c067254c9f6a2626324e27f02703936f Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Tue, 16 Jul 2019 13:07:18 +0100 Subject: [PATCH 20/26] fix(GraphQLController): fix issue where config with multiple items was not being mapped to the db --- spec/ParseGraphQLController.spec.js | 34 ++++++++++++++++++++++- src/Controllers/ParseGraphQLController.js | 2 +- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/spec/ParseGraphQLController.spec.js b/spec/ParseGraphQLController.spec.js index 1b17443649..c7d7ccf9ff 100644 --- a/spec/ParseGraphQLController.spec.js +++ b/spec/ParseGraphQLController.spec.js @@ -10,6 +10,7 @@ describe('ParseGraphQLController', () => { let parseServer; let databaseController; let cacheController; + let databaseUpdateArgs; // Holds the graphQLConfig in memory instead of using the db let graphQLConfigRecord; @@ -54,6 +55,7 @@ describe('ParseGraphQLController', () => { update, fullQueryOptions ) => { + databaseUpdateArgs = [className, query, update, fullQueryOptions]; if ( className === GraphQLConfigClassName && isEqual(query, { objectId: GraphQLConfigId }) && @@ -64,11 +66,15 @@ describe('ParseGraphQLController', () => { ) { setConfigOnDb(update[GraphQLConfigKey]); } else { - return defaultUpdate(className, query, update, fullQueryOptions); + return defaultUpdate(...databaseUpdateArgs); } }; }); + beforeEach(() => { + databaseUpdateArgs = null; + }); + describe('constructor', () => { it('should require a databaseController', () => { expect(() => new ParseGraphQLController()).toThrow( @@ -199,6 +205,32 @@ describe('ParseGraphQLController', () => { parseGraphQLController.updateGraphQLConfig() ).toBeRejectedWith('You must provide a graphQLConfig!'); }); + + it('should correct update the graphQLConfig object using the databaseController', async () => { + const parseGraphQLController = new ParseGraphQLController({ + databaseController, + }); + const graphQLConfig = { + enabledForClasses: ['ClassA', 'ClassB'], + disabledForClasses: [], + classConfigs: [ + { className: 'ClassA', query: { get: false } }, + { className: 'ClassB', mutation: { destroy: false }, type: {} }, + ], + }; + + await parseGraphQLController.updateGraphQLConfig(graphQLConfig); + + expect(databaseUpdateArgs).toBeTruthy(); + const [className, query, update, op] = databaseUpdateArgs; + expect(className).toBe(GraphQLConfigClassName); + expect(query).toEqual({ objectId: GraphQLConfigId }); + expect(update).toEqual({ + [GraphQLConfigKey]: graphQLConfig, + }); + expect(op).toEqual({ upsert: true }); + }); + it('should throw if graphQLConfig is not an object', async () => { const parseGraphQLController = new ParseGraphQLController({ databaseController, diff --git a/src/Controllers/ParseGraphQLController.js b/src/Controllers/ParseGraphQLController.js index ccc858b741..6194ad5687 100644 --- a/src/Controllers/ParseGraphQLController.js +++ b/src/Controllers/ParseGraphQLController.js @@ -69,8 +69,8 @@ class ParseGraphQLController { const update = Object.keys(graphQLConfig).reduce( (acc, key) => { return { - ...acc, [GraphQLConfigKey]: { + ...acc[GraphQLConfigKey], [key]: graphQLConfig[key], }, }; From 4a139ad349aa94b538124f5d0cb1ab02ba977c89 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Mon, 22 Jul 2019 16:03:36 +0100 Subject: [PATCH 21/26] fix(postgres): add _GraphQLConfig default schema on load fixes failing postgres tests --- src/Adapters/Storage/Mongo/MongoTransform.js | 4 ++-- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 1 + src/Controllers/SchemaController.js | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index d4f9657f7f..7db6994868 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -47,7 +47,7 @@ const transformKeyValueForUpdate = ( switch (key) { case 'objectId': case '_id': - if (className === '_GlobalConfig') { + if (['_GlobalConfig', '_GraphQLConfig'].includes(className)) { return { key: key, value: parseInt(restValue), @@ -252,7 +252,7 @@ function transformQueryKeyValue(className, key, value, schema, count = false) { } break; case 'objectId': { - if (className === '_GlobalConfig') { + if (['_GlobalConfig', '_GraphQLConfig'].includes(className)) { value = parseInt(value); } return { key: '_id', value }; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index a449dbb39f..0b4372396a 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1131,6 +1131,7 @@ export class PostgresStorageAdapter implements StorageAdapter { '_JobSchedule', '_Hooks', '_GlobalConfig', + '_GraphQLConfig', '_Audience', ...results.map(result => result.className), ...joins, diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index a6410fc14d..44c9b28216 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -132,6 +132,10 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({ objectId: { type: 'String' }, params: { type: 'Object' }, }, + _GraphQLConfig: { + objectId: { type: 'String' }, + config: { type: 'Object' }, + }, _Audience: { objectId: { type: 'String' }, name: { type: 'String' }, @@ -476,6 +480,10 @@ const _GlobalConfigSchema = { className: '_GlobalConfig', fields: defaultColumns._GlobalConfig, }; +const _GraphQLConfigSchema = { + className: '_GraphQLConfig', + fields: defaultColumns._GraphQLConfig, +}; const _PushStatusSchema = convertSchemaToAdapterSchema( injectDefaultSchema({ className: '_PushStatus', @@ -510,6 +518,7 @@ const VolatileClassesSchemas = [ _JobScheduleSchema, _PushStatusSchema, _GlobalConfigSchema, + _GraphQLConfigSchema, _AudienceSchema, ]; From 2e0940c996516692ff9ac8497bcb7181c9fe72a9 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 22 Jul 2019 11:19:40 -0700 Subject: [PATCH 22/26] GraphQL @mock directive (#5836) * Add mock directive * Include tests for @mock directive --- spec/ParseGraphQLServer.spec.js | 32 +++++++++++++++++++++++++ src/GraphQL/loaders/schemaDirectives.js | 11 +++++++++ 2 files changed, 43 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 83c9bc4969..f708f78b9d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -5272,6 +5272,8 @@ describe('ParseGraphQLServer', () => { hello: String @resolve hello2: String @resolve(to: "hello") userEcho(user: _UserFields!): _UserClass! @resolve + hello3: String! @mock(with: "Hello world!") + hello4: _UserClass! @mock(with: { username: "somefolk" }) } `, }); @@ -5357,5 +5359,35 @@ describe('ParseGraphQLServer', () => { expect(result.data.custom.userEcho.username).toEqual('somefolk'); }); + + it('can mock a custom query with string', async () => { + const result = await apolloClient.query({ + query: gql` + query Hello { + custom { + hello3 + } + } + `, + }); + + expect(result.data.custom.hello3).toEqual('Hello world!'); + }); + + it('can mock a custom query with auto type', async () => { + const result = await apolloClient.query({ + query: gql` + query Hello { + custom { + hello4 { + username + } + } + } + `, + }); + + expect(result.data.custom.hello4.username).toEqual('somefolk'); + }); }); }); diff --git a/src/GraphQL/loaders/schemaDirectives.js b/src/GraphQL/loaders/schemaDirectives.js index e7e5149308..171a5d2bd7 100644 --- a/src/GraphQL/loaders/schemaDirectives.js +++ b/src/GraphQL/loaders/schemaDirectives.js @@ -5,6 +5,7 @@ import { FunctionsRouter } from '../../Routers/FunctionsRouter'; export const definitions = gql` directive @namespace on FIELD_DEFINITION directive @resolve(to: String) on FIELD_DEFINITION + directive @mock(with: Any!) on FIELD_DEFINITION `; const load = parseGraphQLSchema => { @@ -46,6 +47,16 @@ const load = parseGraphQLSchema => { } parseGraphQLSchema.graphQLSchemaDirectives.resolve = ResolveDirectiveVisitor; + + class MockDirectiveVisitor extends SchemaDirectiveVisitor { + visitFieldDefinition(field) { + field.resolve = () => { + return this.args.with; + }; + } + } + + parseGraphQLSchema.graphQLSchemaDirectives.mock = MockDirectiveVisitor; }; export { load }; From c9595ecc37b8829a0c3850c87a533f8f6e12df4d Mon Sep 17 00:00:00 2001 From: = Date: Mon, 22 Jul 2019 15:36:50 -0700 Subject: [PATCH 23/26] Fix existing tests due to the change from ClassFields to ClassCreateFields --- spec/ParseGraphQLServer.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 49bcb6854d..f7c190672a 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -6132,7 +6132,7 @@ describe('ParseGraphQLServer', () => { type Custom { hello: String @resolve hello2: String @resolve(to: "hello") - userEcho(user: _UserFields!): _UserClass! @resolve + userEcho(user: _UserCreateFields!): _UserClass! @resolve hello3: String! @mock(with: "Hello world!") hello4: _UserClass! @mock(with: { username: "somefolk" }) } @@ -6203,7 +6203,7 @@ describe('ParseGraphQLServer', () => { const result = await apolloClient.query({ query: gql` - query UserEcho($user: _UserFields!) { + query UserEcho($user: _UserCreateFields!) { custom { userEcho(user: $user) { username From aedd4bc357af126c69b3c500af368dffa4158a83 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Thu, 25 Jul 2019 14:16:35 +0100 Subject: [PATCH 24/26] fix(parseClassMutations): safer type transformation based on input type --- src/GraphQL/loaders/parseClassMutations.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index deaeade99d..442756f260 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -38,12 +38,15 @@ const load = function( const classGraphQLCreateTypeFields = classGraphQLCreateType.getFields(); const classGraphQLUpdateTypeFields = classGraphQLUpdateType.getFields(); - const transformTypes = fields => { + const transformTypes = (inputType: 'create' | 'update', fields) => { if (fields) { Object.keys(fields).forEach(field => { - const inputTypeField = - classGraphQLCreateTypeFields[field] || - classGraphQLUpdateTypeFields[field]; + let inputTypeField; + if (inputType === 'create') { + inputTypeField = classGraphQLCreateTypeFields[field]; + } else { + inputTypeField = classGraphQLUpdateTypeFields[field]; + } if (inputTypeField) { switch (inputTypeField.type) { case defaultGraphQLTypes.GEO_POINT: @@ -77,7 +80,7 @@ const load = function( const { fields } = args; const { config, auth, info } = context; - transformTypes(fields); + transformTypes('create', fields); return await objectsMutations.createObject( className, @@ -107,7 +110,7 @@ const load = function( const { objectId, fields } = args; const { config, auth, info } = context; - transformTypes(fields); + transformTypes('update', fields); return await objectsMutations.updateObject( className, From 17e4a5a015ebb4c2dde2a19cf25b9d4021d0c697 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Thu, 25 Jul 2019 14:27:07 +0100 Subject: [PATCH 25/26] fix(parseClassMutations): only define necessary input fields --- src/GraphQL/loaders/parseClassMutations.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 442756f260..c1c75ba127 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,7 +1,7 @@ import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsMutations from './objectsMutations'; -import { ParseGraphQLClassConfig } from '../../Options/index'; +import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; const getParseClassMutationConfig = function( parseClassConfig: ?ParseGraphQLClassConfig @@ -35,8 +35,12 @@ const load = function( type: classGraphQLUpdateType, }; - const classGraphQLCreateTypeFields = classGraphQLCreateType.getFields(); - const classGraphQLUpdateTypeFields = classGraphQLUpdateType.getFields(); + const classGraphQLCreateTypeFields = isCreateEnabled + ? classGraphQLCreateType.getFields() + : null; + const classGraphQLUpdateTypeFields = isUpdateEnabled + ? classGraphQLUpdateType.getFields() + : null; const transformTypes = (inputType: 'create' | 'update', fields) => { if (fields) { From 441dead4b578dda2caffa2ff8ffdfccfae430b56 Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Thu, 25 Jul 2019 14:27:26 +0100 Subject: [PATCH 26/26] fix(GraphQL): fix incorrect import paths --- src/GraphQL/loaders/parseClassQueries.js | 2 +- src/GraphQL/loaders/parseClassTypes.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 92242be94c..1f02f962fb 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -3,7 +3,7 @@ import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; import * as parseClassTypes from './parseClassTypes'; -import { ParseGraphQLClassConfig } from '../../Options/index'; +import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; const getParseClassQueryConfig = function( parseClassConfig: ?ParseGraphQLClassConfig diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index bc14be1a52..013229b340 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -13,7 +13,7 @@ import { import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; -import { ParseGraphQLClassConfig } from '../../Options/index'; +import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; const mapInputType = (parseType, targetClass, parseClassTypes) => { switch (parseType) {