diff --git a/spec/ParseGraphQLController.spec.js b/spec/ParseGraphQLController.spec.js new file mode 100644 index 0000000000..c7d7ccf9ff --- /dev/null +++ b/spec/ParseGraphQLController.spec.js @@ -0,0 +1,973 @@ +const { + default: ParseGraphQLController, + GraphQLConfigClassName, + GraphQLConfigId, + GraphQLConfigKey, +} = require('../lib/Controllers/ParseGraphQLController'); +const { isEqual } = require('lodash'); + +describe('ParseGraphQLController', () => { + let parseServer; + let databaseController; + let cacheController; + let databaseUpdateArgs; + + // 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 + ) => { + databaseUpdateArgs = [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(...databaseUpdateArgs); + } + }; + }); + + beforeEach(() => { + databaseUpdateArgs = null; + }); + + 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 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, + }); + 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 7437183f37..e39e8782ec 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 parseGraphQLController; let parseGraphQLSchema; beforeAll(async () => { @@ -11,28 +12,37 @@ describe('ParseGraphQLSchema', () => { schemaCacheTTL: 100, }); databaseController = parseServer.config.databaseController; - parseGraphQLSchema = new ParseGraphQLSchema( + parseGraphQLController = parseServer.config.parseGraphQLController; + parseGraphQLSchema = new ParseGraphQLSchema({ databaseController, - defaultLogger - ); + parseGraphQLController, + log: defaultLogger, + }); }); describe('constructor', () => { - it('should require a databaseController and a log instance', () => { + it('should require a parseGraphQLController, databaseController and a log instance', () => { expect(() => new ParseGraphQLSchema()).toThrow( - 'You must provide a databaseController instance!' - ); - expect(() => new ParseGraphQLSchema({})).toThrow( - 'You must provide a log instance!' + 'You must provide a parseGraphQLController instance!' ); - expect(() => new ParseGraphQLSchema({}, {})).not.toThrow(); + expect( + () => new ParseGraphQLSchema({ parseGraphQLController: {} }) + ).toThrow('You must provide a databaseController instance!'); + expect( + () => + new ParseGraphQLSchema({ + parseGraphQLController: {}, + 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()); }); @@ -40,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 83c9bc4969..f7c190672a 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -189,9 +189,49 @@ 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('Auto API', () => { let httpServer; - const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-Javascript-Key': 'test', @@ -614,113 +654,934 @@ describe('ParseGraphQLServer', () => { name } } - } - `, - })).data['__type']; - expect(findResultType.kind).toEqual('OBJECT'); - expect(findResultType.fields.map(name => name.name).sort()).toEqual([ - 'count', - 'results', - ]); - }); - - it('should have GraphQLUpload object type', async () => { - const graphQLUploadType = (await apolloClient.query({ + } + `, + })).data['__type']; + expect(findResultType.kind).toEqual('OBJECT'); + expect(findResultType.fields.map(name => name.name).sort()).toEqual([ + 'count', + 'results', + ]); + }); + + it('should have GraphQLUpload object type', async () => { + const graphQLUploadType = (await apolloClient.query({ + query: gql` + query GraphQLUploadType { + __type(name: "Upload") { + kind + fields { + name + } + } + } + `, + })).data['__type']; + expect(graphQLUploadType.kind).toEqual('SCALAR'); + }); + + it('should have all expected types', async () => { + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } + } + } + `, + })).data['__schema'].types.map(type => type.name); + + const expectedTypes = [ + 'Class', + 'CreateResult', + 'Date', + 'File', + 'FilesMutation', + 'FindResult', + 'ObjectsMutation', + 'ObjectsQuery', + 'ReadPreference', + 'UpdateResult', + 'Upload', + ]; + expect( + expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) + ).toBeTruthy(JSON.stringify(schemaTypes.types)); + }); + }); + + describe('Parse Class Types', () => { + it('should have all expected types', async () => { + await parseServer.config.databaseController.loadSchema(); + + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } + } + } + `, + })).data['__schema'].types.map(type => type.name); + + const expectedTypes = [ + '_RoleClass', + '_RoleConstraints', + '_RoleCreateFields', + '_RoleUpdateFields', + '_RoleFindResult', + '_UserClass', + '_UserConstraints', + '_UserFindResult', + '_UserSignUpFields', + '_UserCreateFields', + '_UserUpdateFields', + ]; + expect( + expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) + ).toBeTruthy(JSON.stringify(schemaTypes)); + }); + + it('should update schema when it changes', async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.updateClass('_User', { + foo: { type: 'String' }, + }); + + const userFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "_UserClass") { + fields { + name + } + } + } + `, + })).data['__type'].fields.map(field => field.name); + expect(userFields.indexOf('foo') !== -1).toBeTruthy(); + }); + }); + + 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 GraphQLUploadType { - __type(name: "Upload") { - kind - fields { - name + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + objectId + engine + doors + price + mileage } } } `, - })).data['__type']; - expect(graphQLUploadType.kind).toEqual('SCALAR'); - }); + variables: { + objectId: superCar.id, + }, + })).data.objects.getSuperCar; + expect(getSuperCar).toBeTruthy(); - it('should have all expected types', async () => { - const schemaTypes = (await apolloClient.query({ + 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 SchemaTypes { - __schema { - types { - name + query GetSuperCar($objectId: ID!) { + objects { + getSuperCar(objectId: $objectId) { + objectId } } } `, - })).data['__schema'].types.map(type => type.name); - - const expectedTypes = [ - 'Class', - 'CreateResult', - 'Date', - 'File', - 'FilesMutation', - 'FindResult', - 'ObjectsMutation', - 'ObjectsQuery', - 'ReadPreference', - 'UpdateResult', - 'Upload', - ]; - expect( - expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) - ).toBeTruthy(JSON.stringify(schemaTypes.types)); + 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(); - describe('Parse Class Types', () => { - it('should have all expected types', async () => { - 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' }, + }); - const schemaTypes = (await apolloClient.query({ - query: gql` - query SchemaTypes { - __schema { - types { - name + 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 + } } } - } - `, - })).data['__schema'].types.map(type => type.name); + `, + }) + ).toBeRejected(); - const expectedTypes = [ - '_RoleClass', - '_RoleConstraints', - '_RoleFields', - '_RoleFindResult', - '_UserClass', - '_UserConstraints', - '_UserFindResult', - '_UserFields', - ]; - expect( - expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) - ).toBeTruthy(JSON.stringify(schemaTypes)); - }); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + objects { + findSuperCar(where: { mileage: { _eq: 0 } }) { + count + } + } + } + `, + }) + ).toBeRejected(); - it('should update schema when it changes', async () => { + 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.updateClass('_User', { - foo: { type: 'String' }, + + await schemaController.addClassIfNotExists('SuperCar', { + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, }); - const userFields = (await apolloClient.query({ - query: gql` - query UserType { - __type(name: "_UserClass") { - fields { - name + 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 + } + } } } - } - `, - })).data['__type'].fields.map(field => field.name); - expect(userFields.indexOf('foo') !== -1).toBeTruthy(); + `, + }) + ).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(); }); }); @@ -2245,7 +3106,7 @@ describe('ParseGraphQLServer', () => { const result = await apolloClient.mutate({ mutation: gql` - mutation CreateCustomer($fields: CustomerFields) { + mutation CreateCustomer($fields: CustomerCreateFields) { objects { createCustomer(fields: $fields) { objectId @@ -2411,7 +3272,7 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation UpdateCustomer( $objectId: ID! - $fields: CustomerFields + $fields: CustomerUpdateFields ) { objects { updateCustomer(objectId: $objectId, fields: $fields) { @@ -2627,7 +3488,7 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation UpdateSomeObject( $objectId: ID! - $fields: ${className}Fields + $fields: ${className}UpdateFields ) { objects { update${className}( @@ -3549,7 +4410,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3616,7 +4477,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3689,7 +4550,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3762,7 +4623,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3850,7 +4711,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -3981,8 +4842,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateChildObject( - $fields1: ChildClassFields - $fields2: ChildClassFields + $fields1: ChildClassCreateFields + $fields2: ChildClassCreateFields ) { objects { createChildClass1: createChildClass(fields: $fields1) { @@ -4111,7 +4972,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateMainObject($fields: MainClassFields) { + mutation CreateMainObject($fields: MainClassCreateFields) { objects { createMainClass(fields: $fields) { objectId @@ -4299,8 +5160,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject( - $fields1: SomeClassFields - $fields2: SomeClassFields + $fields1: SomeClassCreateFields + $fields2: SomeClassCreateFields ) { objects { createSomeClass1: createSomeClass(fields: $fields1) { @@ -4400,7 +5261,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4468,7 +5329,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4611,8 +5472,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject( - $fields1: SomeClassFields - $fields2: SomeClassFields + $fields1: SomeClassCreateFields + $fields2: SomeClassCreateFields ) { objects { createSomeClass1: createSomeClass(fields: $fields1) { @@ -4694,7 +5555,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4771,7 +5632,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4898,7 +5759,7 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` - mutation CreateSomeObject($fields: SomeClassFields) { + mutation CreateSomeObject($fields: SomeClassCreateFields) { objects { createSomeClass(fields: $fields) { objectId @@ -4941,7 +5802,7 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation UpdateSomeObject( $objectId: ID! - $fields: SomeClassFields + $fields: SomeClassUpdateFields ) { objects { updateSomeClass(objectId: $objectId, fields: $fields) { @@ -5271,7 +6132,9 @@ 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" }) } `, }); @@ -5340,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 @@ -5357,5 +6220,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/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/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/ParseGraphQLController.js b/src/Controllers/ParseGraphQLController.js new file mode 100644 index 0000000000..6194ad5687 --- /dev/null +++ b/src/Controllers/ParseGraphQLController.js @@ -0,0 +1,375 @@ +import requiredParameter from '../../lib/requiredParameter'; +import DatabaseController from './DatabaseController'; +import CacheController from './CacheController'; + +const GraphQLConfigClassName = '_GraphQLConfig'; +const GraphQLConfigId = '1'; +const GraphQLConfigKey = 'config'; + +class ParseGraphQLController { + databaseController: DatabaseController; + cacheController: CacheController; + isMounted: boolean; + configCacheKey: string; + + constructor( + params: { + databaseController: DatabaseController, + cacheController: CacheController, + } = {} + ) { + this.databaseController = + params.databaseController || + requiredParameter( + `ParseGraphQLController requires a "databaseController" to be instantiated.` + ); + this.cacheController = params.cacheController; + this.isMounted = !!params.mountGraphQL; + this.configCacheKey = GraphQLConfigKey; + } + + async getGraphQLConfig(): Promise { + if (this.isMounted) { + const _cachedConfig = await this._getCachedGraphQLConfig(); + if (_cachedConfig) { + return _cachedConfig; + } + } + + const results = await this.databaseController.find( + GraphQLConfigClassName, + { 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]; + } + + if (this.isMounted) { + this._putCachedGraphQLConfig(graphQLConfig); + } + + return graphQLConfig; + } + + async updateGraphQLConfig( + graphQLConfig: ParseGraphQLConfig + ): Promise { + // throws if invalid + 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) => { + return { + [GraphQLConfigKey]: { + ...acc[GraphQLConfigKey], + [key]: graphQLConfig[key], + }, + }; + }, + { [GraphQLConfigKey]: {} } + ); + + await this.databaseController.update( + GraphQLConfigClassName, + { objectId: GraphQLConfigId }, + update, + { upsert: true } + ); + + if (this.isMounted) { + this._putCachedGraphQLConfig(graphQLConfig); + } + + return { response: { result: true } }; + } + + _getCachedGraphQLConfig() { + return this.cacheController.graphQL.get(this.configCacheKey); + } + + _putCachedGraphQLConfig(graphQLConfig: ParseGraphQLConfig) { + return this.cacheController.graphQL.put( + this.configCacheKey, + graphQLConfig, + 60000 + ); + } + + _validateGraphQLConfig(graphQLConfig: ?ParseGraphQLConfig): void { + const errorMessages: string = []; + if (!graphQLConfig) { + errorMessages.push('cannot be undefined, null or empty'); + } else if (!isValidSimpleObject(graphQLConfig)) { + errorMessages.push('must be a valid object'); + } else { + const { + enabledForClasses = null, + disabledForClasses = null, + classConfigs = null, + ...invalidKeys + } = graphQLConfig; + + if (Object.keys(invalidKeys).length) { + errorMessages.push( + `encountered invalid keys: [${Object.keys(invalidKeys)}]` + ); + } + if ( + enabledForClasses !== null && + !isValidStringArray(enabledForClasses) + ) { + errorMessages.push(`"enabledForClasses" is not a valid array`); + } + if ( + disabledForClasses !== null && + !isValidStringArray(disabledForClasses) + ) { + errorMessages.push(`"disabledForClasses" is not a valid array`); + } + if (classConfigs !== null) { + if (Array.isArray(classConfigs)) { + classConfigs.forEach(classConfig => { + const errorMessage = this._validateClassConfig(classConfig); + if (errorMessage) { + errorMessages.push( + `classConfig:${classConfig.className} is invalid because ${errorMessage}` + ); + } + }); + } else { + errorMessages.push(`"classConfigs" is not a valid array`); + } + } + } + if (errorMessages.length) { + throw new Error(`Invalid graphQLConfig: ${errorMessages.join('; ')}`); + } + } + + _validateClassConfig(classConfig: ?ParseGraphQLClassConfig): string | void { + if (!isValidSimpleObject(classConfig)) { + return 'it must be a valid object'; + } else { + const { + className, + type = null, + query = null, + mutation = null, + ...invalidKeys + } = classConfig; + 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`; + } + if (type !== null) { + if (!isValidSimpleObject(type)) { + return `"type" must be a valid object`; + } + const { + inputFields = null, + outputFields = null, + constraintFields = null, + sortFields = null, + ...invalidKeys + } = type; + if (Object.keys(invalidKeys).length) { + return `"type" contains invalid keys, [${Object.keys(invalidKeys)}]`; + } else if (outputFields !== null && !isValidStringArray(outputFields)) { + return `"outputFields" must be a valid string array`; + } else if ( + constraintFields !== null && + !isValidStringArray(constraintFields) + ) { + return `"constraintFields" must be a valid string array`; + } + if (sortFields !== null) { + if (Array.isArray(sortFields)) { + let errorMessage; + sortFields.every((sortField, index) => { + if (!isValidSimpleObject(sortField)) { + errorMessage = `"sortField" at index ${index} is not a valid object`; + return false; + } else { + const { field, asc, desc, ...invalidKeys } = sortField; + if (Object.keys(invalidKeys).length) { + errorMessage = `"sortField" at index ${index} contains invalid keys, [${Object.keys( + invalidKeys + )}]`; + return false; + } else { + 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 ( + typeof asc !== 'boolean' || + typeof desc !== 'boolean' + ) { + errorMessage = `"sortField" at index ${index} did not provide "asc" or "desc" as booleans`; + return false; + } + } + } + return true; + }); + if (errorMessage) { + return errorMessage; + } + } else { + return `"sortFields" must be a valid array.`; + } + } + if (inputFields !== null) { + if (isValidSimpleObject(inputFields)) { + const { + create = null, + update = null, + ...invalidKeys + } = inputFields; + if (Object.keys(invalidKeys).length) { + return `"inputFields" contains invalid keys: [${Object.keys( + invalidKeys + )}]`; + } else { + if (update !== null && !isValidStringArray(update)) { + return `"inputFields.update" must be a valid string array`; + } else if (create !== null) { + if (!isValidStringArray(create)) { + return `"inputFields.create" must be a valid string array`; + } else if (className === '_User') { + if ( + !create.includes('username') || + !create.includes('password') + ) { + return `"inputFields.create" must include required fields, username and password`; + } + } + } + } + } else { + return `"inputFields" must be a valid object`; + } + } + } + if (query !== null) { + if (isValidSimpleObject(query)) { + const { find = null, get = null, ...invalidKeys } = query; + if (Object.keys(invalidKeys).length) { + return `"query" contains invalid keys, [${Object.keys( + invalidKeys + )}]`; + } else if (find !== null && typeof find !== 'boolean') { + return `"query.find" must be a boolean`; + } else if (get !== null && typeof get !== 'boolean') { + return `"query.get" must be a boolean`; + } + } else { + return `"query" must be a valid object`; + } + } + if (mutation !== null) { + if (isValidSimpleObject(mutation)) { + const { + create = null, + update = null, + destroy = null, + ...invalidKeys + } = mutation; + 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 { + return `"mutation" must be a valid object`; + } + } + } + } +} + +const isValidStringArray = function(array): boolean { + return Array.isArray(array) + ? !array.some(s => typeof s !== 'string' || s.trim().length < 1) + : 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 && + obj instanceof Promise !== true + ); +}; + +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: ?{ + /* 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; */ + sortFields: ?({ + field: string, + asc: boolean, + desc: boolean, + }[]), + }; + /* 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 is a reserved key word in js + destroy: ?boolean, + }; +} + +export default ParseGraphQLController; +export { GraphQLConfigClassName, GraphQLConfigId, GraphQLConfigKey }; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index e3dfc040b9..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' }, @@ -163,6 +167,7 @@ const volatileClasses = Object.freeze([ '_PushStatus', '_Hooks', '_GlobalConfig', + '_GraphQLConfig', '_JobSchedule', '_Audience', ]); @@ -475,6 +480,10 @@ const _GlobalConfigSchema = { className: '_GlobalConfig', fields: defaultColumns._GlobalConfig, }; +const _GraphQLConfigSchema = { + className: '_GraphQLConfig', + fields: defaultColumns._GraphQLConfig, +}; const _PushStatusSchema = convertSchemaToAdapterSchema( injectDefaultSchema({ className: '_PushStatus', @@ -509,6 +518,7 @@ const VolatileClassesSchemas = [ _JobScheduleSchema, _PushStatusSchema, _GlobalConfigSchema, + _GraphQLConfigSchema, _AudienceSchema, ]; diff --git a/src/Controllers/index.js b/src/Controllers/index.js index 81e7cfbead..b016d265bc 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 ParseGraphQLController from './ParseGraphQLController'; 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 parseGraphQLController = getParseGraphQLController(options, { + databaseController, + cacheController, + }); return { loggerController, filesController, @@ -54,6 +59,7 @@ export function getControllers(options: ParseServerOptions) { pushControllerQueue, analyticsController, cacheController, + parseGraphQLController, liveQueryController, databaseController, hooksController, @@ -123,6 +129,16 @@ export function getCacheController( return new CacheController(cacheControllerAdapter, appId); } +export function getParseGraphQLController( + options: ParseServerOptions, + controllerDeps +): ParseGraphQLController { + return new ParseGraphQLController({ + mountGraphQL: options.mountGraphQL, + ...controllerDeps, + }); +} + export function getAnalyticsController( options: ParseServerOptions ): AnalyticsController { diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 641d75152b..261045fe81 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -8,36 +8,57 @@ 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 ParseGraphQLController, { + ParseGraphQLConfig, +} from '../Controllers/ParseGraphQLController'; +import DatabaseController from '../Controllers/DatabaseController'; import { toGraphQLError } from './parseGraphQLUtils'; import * as schemaDirectives from './loaders/schemaDirectives'; class ParseGraphQLSchema { - constructor(databaseController, log, graphQLCustomTypeDefs) { + databaseController: DatabaseController; + parseGraphQLController: ParseGraphQLController; + parseGraphQLConfig: ParseGraphQLConfig; + graphQLCustomTypeDefs: any; + + constructor( + params: { + databaseController: DatabaseController, + parseGraphQLController: ParseGraphQLController, + log: any, + } = {} + ) { + this.parseGraphQLController = + params.parseGraphQLController || + requiredParameter('You must provide a parseGraphQLController instance!'); this.databaseController = - databaseController || + params.databaseController || requiredParameter('You must provide a databaseController instance!'); - this.log = log || requiredParameter('You must provide a log instance!'); - this.graphQLCustomTypeDefs = graphQLCustomTypeDefs; + this.log = + params.log || requiredParameter('You must provide a log instance!'); + this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs; } async load() { - const schemaController = await this.databaseController.loadSchema(); - const parseClasses = await schemaController.getAllClasses(); - const parseClassesString = JSON.stringify(parseClasses); + const { parseGraphQLConfig } = await this._initializeSchemaAndConfig(); - if (this.graphQLSchema) { - if (this.parseClasses === parseClasses) { - return this.graphQLSchema; - } + const parseClasses = await this._getClassesForSchema(parseGraphQLConfig); + const parseClassesString = JSON.stringify(parseClasses); - 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.graphQLAutoSchema = null; @@ -53,16 +74,15 @@ class ParseGraphQLSchema { defaultGraphQLTypes.load(this); - parseClasses.forEach(parseClass => { - parseClassTypes.load(this, parseClass); - - parseClassQueries.load(this, parseClass); - - parseClassMutations.load(this, parseClass); - }); + this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach( + ([parseClass, parseClassConfig]) => { + parseClassTypes.load(this, parseClass, parseClassConfig); + parseClassQueries.load(this, parseClass, parseClassConfig); + parseClassMutations.load(this, parseClass, parseClassConfig); + } + ); defaultGraphQLQueries.load(this); - defaultGraphQLMutations.load(this); let graphQLQuery = undefined; @@ -160,6 +180,104 @@ class ParseGraphQLSchema { } throw toGraphQLError(error); } + + async _initializeSchemaAndConfig() { + const [schemaController, parseGraphQLConfig] = await Promise.all([ + this.databaseController.loadSchema(), + this.parseGraphQLController.getGraphQLConfig(), + ]); + + this.schemaController = schemaController; + + return { + parseGraphQLConfig, + }; + } + + /** + * Gets all classes found by the `schemaController` + * minus those filtered out by the app's parseGraphQLConfig. + */ + async _getClassesForSchema(parseGraphQLConfig: ParseGraphQLConfig) { + const { enabledForClasses, disabledForClasses } = parseGraphQLConfig; + const allClasses = await this.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, + parseGraphQLConfig: ParseGraphQLConfig + ) { + const { classConfigs } = parseGraphQLConfig; + return parseClasses.map(parseClass => { + let parseClassConfig; + if (classConfigs) { + parseClassConfig = classConfigs.find( + c => c.className === parseClass.className + ); + } + 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 ( + JSON.stringify(this.parseGraphQLConfig) === + JSON.stringify(parseGraphQLConfig) + ) { + if (this.parseClasses === parseClasses) { + return false; + } + + if (this.parseClassesString === parseClassesString) { + this.parseClasses = parseClasses; + return false; + } + } + + return true; + } } export { ParseGraphQLSchema }; diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 537991ebcb..59c5fe61e1 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -9,8 +9,13 @@ import { handleParseErrors, handleParseHeaders } from '../middlewares'; import requiredParameter from '../requiredParameter'; import defaultLogger from '../logger'; import { ParseGraphQLSchema } from './ParseGraphQLSchema'; +import ParseGraphQLController, { + ParseGraphQLConfig, +} from '../Controllers/ParseGraphQLController'; class ParseGraphQLServer { + parseGraphQLController: ParseGraphQLController; + constructor(parseServer, config) { this.parseServer = parseServer || @@ -19,12 +24,15 @@ 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.parseGraphQLController = this.parseServer.config.parseGraphQLController; + this.parseGraphQLSchema = new ParseGraphQLSchema({ + parseGraphQLController: this.parseGraphQLController, + databaseController: this.parseServer.config.databaseController, + log: + (this.parseServer.config && this.parseServer.config.loggerController) || defaultLogger, - this.config.graphQLCustomTypeDefs - ); + graphQLCustomTypeDefs: this.config.graphQLCustomTypeDefs, + }); } async _getGraphQLOptions(req) { @@ -111,6 +119,10 @@ class ParseGraphQLServer { } ); } + + setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { + return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); + } } export { ParseGraphQLServer }; diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 403faa4310..c1c75ba127 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,23 +1,58 @@ import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsMutations from './objectsMutations'; +import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; -const load = (parseGraphQLSchema, parseClass) => { - const className = parseClass.className; +const getParseClassMutationConfig = function( + parseClassConfig: ?ParseGraphQLClassConfig +) { + return (parseClassConfig && parseClassConfig.mutation) || {}; +}; + +const load = function( + parseGraphQLSchema, + parseClass, + parseClassConfig: ?ParseGraphQLClassConfig +) { + const { className } = parseClass; + const { + create: isCreateEnabled = true, + update: isUpdateEnabled = true, + destroy: isDestroyEnabled = 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 = isCreateEnabled + ? classGraphQLCreateType.getFields() + : null; + const classGraphQLUpdateTypeFields = isUpdateEnabled + ? classGraphQLUpdateType.getFields() + : null; - const transformTypes = fields => { + const transformTypes = (inputType: 'create' | 'update', fields) => { if (fields) { Object.keys(fields).forEach(field => { - if (classGraphQLInputTypeFields[field]) { - switch (classGraphQLInputTypeFields[field].type) { + let inputTypeField; + if (inputType === 'create') { + inputTypeField = classGraphQLCreateTypeFields[field]; + } else { + inputTypeField = classGraphQLUpdateTypeFields[field]; + } + if (inputTypeField) { + switch (inputTypeField.type) { case defaultGraphQLTypes.GEO_POINT: fields[field].__type = 'GeoPoint'; break; @@ -36,86 +71,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('create', 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('update', 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 (isDestroyEnabled) { + 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..1f02f962fb 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -3,9 +3,24 @@ import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; import * as parseClassTypes from './parseClassTypes'; +import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; -const load = (parseGraphQLSchema, parseClass) => { - const className = parseClass.className; +const getParseClassQueryConfig = function( + parseClassConfig: ?ParseGraphQLClassConfig +) { + return (parseClassConfig && parseClassConfig.query) || {}; +}; + +const load = function( + parseGraphQLSchema, + parseClass, + parseClassConfig: ?ParseGraphQLClassConfig +) { + const { className } = parseClass; + const { + get: isGetEnabled = true, + find: isFindEnabled = true, + } = getParseClassQueryConfig(parseClassConfig); const { classGraphQLOutputType, @@ -13,90 +28,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..013229b340 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 '../../Controllers/ParseGraphQLController'; const mapInputType = (parseType, targetClass, parseClassTypes) => { switch (parseType) { @@ -161,14 +162,105 @@ const extractKeysAndInclude = selectedFields => { return { keys, include }; }; -const load = (parseGraphQLSchema, parseClass) => { - const className = parseClass.className; +const getParseClassTypeConfig = function( + parseClassConfig: ?ParseGraphQLClassConfig +) { + return (parseClassConfig && parseClassConfig.type) || {}; +}; +const getInputFieldsAndConstraints = function( + parseClass, + parseClassConfig: ?ParseGraphQLClassConfig +) { 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); + }); + } 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) { + classSortFields = allowedSortFields; + 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 +363,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 +392,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 +456,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 +494,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 +529,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 +660,8 @@ const load = (parseGraphQLSchema, parseClass) => { parseGraphQLSchema.parseClassTypes[className] = { classGraphQLScalarType, classGraphQLRelationOpType, - classGraphQLInputType, + classGraphQLCreateType, + classGraphQLUpdateType, classGraphQLConstraintType, classGraphQLConstraintsType, classGraphQLFindArgs, @@ -552,37 +682,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/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 }; 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/ParseServer.js b/src/ParseServer.js index 1297127d06..556d220b3f 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -21,6 +21,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'; @@ -231,6 +232,7 @@ class ParseServer { new IAPValidationRouter(), new FeaturesRouter(), new GlobalConfigRouter(), + new GraphQLRouter(), new PurgeRouter(), new HooksRouter(), new CloudCodeRouter(), diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js new file mode 100644 index 0000000000..cdf2565926 --- /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.parseGraphQLController.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.parseGraphQLController.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;