From 5b1bf85fae9b0d01700c2650d71c72fd43c9f8f4 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 2 Aug 2019 14:33:13 -0700 Subject: [PATCH 01/34] Add relayStyle option to ParseGraphQLServer config --- src/GraphQL/ParseGraphQLServer.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 59c5fe61e1..266c999d90 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -23,7 +23,7 @@ class ParseGraphQLServer { if (!config || !config.graphQLPath) { requiredParameter('You must provide a config.graphQLPath!'); } - this.config = config; + this._config = Object.assign({}, config); this.parseGraphQLController = this.parseServer.config.parseGraphQLController; this.parseGraphQLSchema = new ParseGraphQLSchema({ parseGraphQLController: this.parseGraphQLController, @@ -31,7 +31,8 @@ class ParseGraphQLServer { log: (this.parseServer.config && this.parseServer.config.loggerController) || defaultLogger, - graphQLCustomTypeDefs: this.config.graphQLCustomTypeDefs, + graphQLCustomTypeDefs: this._config.graphQLCustomTypeDefs, + relayStyle: this._config.relayStyle === true, }); } @@ -60,13 +61,13 @@ class ParseGraphQLServer { gb: 3, }[maxUploadSize.slice(-2).toLowerCase()]; - app.use(this.config.graphQLPath, graphqlUploadExpress({ maxFileSize })); - app.use(this.config.graphQLPath, corsMiddleware()); - app.use(this.config.graphQLPath, bodyParser.json()); - app.use(this.config.graphQLPath, handleParseHeaders); - app.use(this.config.graphQLPath, handleParseErrors); + app.use(this._config.graphQLPath, graphqlUploadExpress({ maxFileSize })); + app.use(this._config.graphQLPath, corsMiddleware()); + app.use(this._config.graphQLPath, bodyParser.json()); + app.use(this._config.graphQLPath, handleParseHeaders); + app.use(this._config.graphQLPath, handleParseErrors); app.use( - this.config.graphQLPath, + this._config.graphQLPath, graphqlExpress(async req => await this._getGraphQLOptions(req)) ); } @@ -76,7 +77,7 @@ class ParseGraphQLServer { requiredParameter('You must provide an Express.js app instance!'); } app.get( - this.config.playgroundPath || + this._config.playgroundPath || requiredParameter( 'You must provide a config.playgroundPath to applyPlayground!' ), @@ -84,8 +85,8 @@ class ParseGraphQLServer { res.setHeader('Content-Type', 'text/html'); res.write( renderPlaygroundPage({ - endpoint: this.config.graphQLPath, - subscriptionEndpoint: this.config.subscriptionsPath, + endpoint: this._config.graphQLPath, + subscriptionEndpoint: this._config.subscriptionsPath, headers: { 'X-Parse-Application-Id': this.parseServer.config.appId, 'X-Parse-Master-Key': this.parseServer.config.masterKey, @@ -112,7 +113,7 @@ class ParseGraphQLServer { { server, path: - this.config.subscriptionsPath || + this._config.subscriptionsPath || requiredParameter( 'You must provide a config.subscriptionsPath to createSubscriptions!' ), @@ -123,6 +124,11 @@ class ParseGraphQLServer { setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); } + + setRelaySyle(relayStyle) { + this._config.relayStyle = relayStyle; + this.parseGraphQLSchema.relayStyle = relayStyle; + } } export { ParseGraphQLServer }; From 3182568e0435ac6144f1f0ff26a99991c835d727 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 2 Aug 2019 14:56:25 -0700 Subject: [PATCH 02/34] Add client key to the playground headers --- src/GraphQL/ParseGraphQLServer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 266c999d90..599ea2fa5b 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -89,6 +89,7 @@ class ParseGraphQLServer { subscriptionEndpoint: this._config.subscriptionsPath, headers: { 'X-Parse-Application-Id': this.parseServer.config.appId, + 'X-Parse-Client-Key': this.parseServer.config.clientKey, 'X-Parse-Master-Key': this.parseServer.config.masterKey, }, }) From 4a660d026ea1e28d7b97a56aec195dbab9c60e08 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 2 Aug 2019 15:19:45 -0700 Subject: [PATCH 03/34] Add relayStyle attribute to ParseGraphQLSchema --- src/GraphQL/ParseGraphQLSchema.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 261045fe81..6aedc55122 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -1,5 +1,10 @@ import Parse from 'parse/node'; -import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { + GraphQLSchema, + GraphQLObjectType, + DocumentNode, + GraphQLNamedType, +} from 'graphql'; import { mergeSchemas, SchemaDirectiveVisitor } from 'graphql-tools'; import requiredParameter from '../requiredParameter'; import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes'; @@ -19,13 +24,26 @@ class ParseGraphQLSchema { databaseController: DatabaseController; parseGraphQLController: ParseGraphQLController; parseGraphQLConfig: ParseGraphQLConfig; - graphQLCustomTypeDefs: any; + graphQLCustomTypeDefs: ?( + | string + | GraphQLSchema + | DocumentNode + | GraphQLNamedType[] + ); + relayStyle: boolean; constructor( params: { databaseController: DatabaseController, parseGraphQLController: ParseGraphQLController, log: any, + graphQLCustomTypeDefs: ?( + | string + | GraphQLSchema + | DocumentNode + | GraphQLNamedType[] + ), + relayStyle?: boolean, } = {} ) { this.parseGraphQLController = @@ -37,6 +55,7 @@ class ParseGraphQLSchema { this.log = params.log || requiredParameter('You must provide a log instance!'); this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs; + this.relayStyle = params.relayStyle === true; } async load() { From e42c6aec5ff38c70e72ff8e9d8e5cf3e4debcc89 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 2 Aug 2019 15:58:41 -0700 Subject: [PATCH 04/34] Include relayStyle information in the criteria for deciding if the schema changed --- src/GraphQL/ParseGraphQLSchema.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 6aedc55122..4e16ff1aea 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -82,6 +82,7 @@ class ParseGraphQLSchema { this.meType = null; this.graphQLAutoSchema = null; this.graphQLSchema = null; + this.graphQLSchemaIsRelayStyle = this.relayStyle; this.graphQLTypes = []; this.graphQLObjectsQueries = {}; this.graphQLQueries = {}; @@ -282,9 +283,15 @@ class ParseGraphQLSchema { const { parseClasses, parseClassesString, parseGraphQLConfig } = params; if ( - JSON.stringify(this.parseGraphQLConfig) === - JSON.stringify(parseGraphQLConfig) + this.graphQLSchemaIsRelayStyle === this.relayStyle && + (this.parseGraphQLConfig === parseGraphQLConfig || + JSON.stringify(this.parseGraphQLConfig) === + JSON.stringify(parseGraphQLConfig)) ) { + if (this.parseGraphQLConfig !== parseGraphQLConfig) { + this.parseGraphQLConfig = parseGraphQLConfig; + } + if (this.parseClasses === parseClasses) { return false; } From 887c145ec1db062427e775a115cbc043fc4120cc Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 2 Aug 2019 18:36:17 -0700 Subject: [PATCH 05/34] Relay node definitions --- package-lock.json | 11 +++++- package.json | 1 + src/GraphQL/ParseGraphQLSchema.js | 5 +++ src/GraphQL/loaders/defaultRelaySchema.js | 45 +++++++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/GraphQL/loaders/defaultRelaySchema.js diff --git a/package-lock.json b/package-lock.json index ba909e1b0d..13e1b6524f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5782,6 +5782,14 @@ "resolved": "https://registry.npmjs.org/graphql-list-fields/-/graphql-list-fields-2.0.2.tgz", "integrity": "sha512-9TSAwcVA3KWw7JWYep5NCk2aw3wl1ayLtbMpmG7l26vh1FZ+gZexNPP+XJfUFyJa71UU0zcKSgtgpsrsA3Xv9Q==" }, + "graphql-relay": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/graphql-relay/-/graphql-relay-0.6.0.tgz", + "integrity": "sha512-OVDi6C9/qOT542Q3KxZdXja3NrDvqzbihn1B44PH8P/c5s0Q90RyQwT6guhGqXqbYEH6zbeLJWjQqiYvcg2vVw==", + "requires": { + "prettier": "^1.16.0" + } + }, "graphql-subscriptions": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz", @@ -9168,8 +9176,7 @@ "prettier": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", - "dev": true + "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==" }, "private": { "version": "0.1.8", diff --git a/package.json b/package.json index 657165ba4b..88d358de14 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "follow-redirects": "1.7.0", "graphql": "14.4.2", "graphql-list-fields": "2.0.2", + "graphql-relay": "^0.6.0", "graphql-tools": "^4.0.5", "graphql-upload": "8.0.7", "intersect": "1.0.1", diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 4e16ff1aea..e1c9ba6a63 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -19,6 +19,7 @@ import ParseGraphQLController, { import DatabaseController from '../Controllers/DatabaseController'; import { toGraphQLError } from './parseGraphQLUtils'; import * as schemaDirectives from './loaders/schemaDirectives'; +import * as defaultRelaySchema from './loaders/defaultRelaySchema'; class ParseGraphQLSchema { databaseController: DatabaseController; @@ -94,6 +95,10 @@ class ParseGraphQLSchema { defaultGraphQLTypes.load(this); + if (this.graphQLSchemaIsRelayStyle) { + defaultRelaySchema.load(this); + } + this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach( ([parseClass, parseClassConfig]) => { parseClassTypes.load(this, parseClass, parseClassConfig); diff --git a/src/GraphQL/loaders/defaultRelaySchema.js b/src/GraphQL/loaders/defaultRelaySchema.js new file mode 100644 index 0000000000..2a65d81568 --- /dev/null +++ b/src/GraphQL/loaders/defaultRelaySchema.js @@ -0,0 +1,45 @@ +import { nodeDefinitions, fromGlobalId } from 'graphql-relay'; +import getFieldNames from 'graphql-list-fields'; +import * as objectsQueries from './objectsQueries'; +import * as parseClassTypes from './parseClassTypes'; + +const load = parseGraphQLSchema => { + const { nodeInterface, nodeField } = nodeDefinitions( + async (globalId, context, queryInfo) => { + try { + const { type, id } = fromGlobalId(globalId); + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); + + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + ); + + return { + className: type, + ...(await objectsQueries.getObject( + type, + id, + keys, + include, + undefined, + undefined, + config, + auth, + info + )), + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + obj => { + return parseGraphQLSchema[obj.className].classGraphQLOutputType; + } + ); + + parseGraphQLSchema.graphQLTypes.push(nodeInterface); + parseGraphQLSchema.graphQLQueries.node = nodeField; +}; + +export { load }; From 9b1f7cb785edf57295f3013b66cd7837bde67cb3 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 2 Aug 2019 19:13:19 -0700 Subject: [PATCH 06/34] Add node interface and global id to the object class output type --- src/GraphQL/ParseGraphQLSchema.js | 1 + src/GraphQL/loaders/defaultRelaySchema.js | 1 + src/GraphQL/loaders/parseClassTypes.js | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index e1c9ba6a63..16f6d83774 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -92,6 +92,7 @@ class ParseGraphQLSchema { this.graphQLSubscriptions = {}; this.graphQLSchemaDirectivesDefinitions = null; this.graphQLSchemaDirectives = {}; + this.relayNodeInterface = null; defaultGraphQLTypes.load(this); diff --git a/src/GraphQL/loaders/defaultRelaySchema.js b/src/GraphQL/loaders/defaultRelaySchema.js index 2a65d81568..e44d06d673 100644 --- a/src/GraphQL/loaders/defaultRelaySchema.js +++ b/src/GraphQL/loaders/defaultRelaySchema.js @@ -38,6 +38,7 @@ const load = parseGraphQLSchema => { } ); + parseGraphQLSchema.relayNodeInterface = nodeInterface; parseGraphQLSchema.graphQLTypes.push(nodeInterface); parseGraphQLSchema.graphQLQueries.node = nodeField; }; diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 013229b340..7759026f96 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -10,6 +10,7 @@ import { GraphQLScalarType, GraphQLEnumType, } from 'graphql'; +import { globalIdField } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; @@ -528,8 +529,12 @@ const load = ( }; const classGraphQLOutputTypeName = `${className}Class`; + const interfaces = [defaultGraphQLTypes.CLASS]; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + interfaces.push(parseGraphQLSchema.relayNodeInterface); + } const outputFields = () => { - return classOutputFields.reduce((fields, field) => { + const outputFields = classOutputFields.reduce((fields, field) => { const type = mapOutputType( parseClass.fields[field].type, parseClass.fields[field].targetClass, @@ -632,11 +637,15 @@ const load = ( return fields; } }, defaultGraphQLTypes.CLASS_FIELDS); + if (parseGraphQLSchema.relayNodeInterface) { + outputFields.id = globalIdField(className, obj => obj.objectId); + } + return outputFields; }; const classGraphQLOutputType = new GraphQLObjectType({ name: classGraphQLOutputTypeName, description: `The ${classGraphQLOutputTypeName} object type is used in operations that involve outputting objects of ${className} class.`, - interfaces: [defaultGraphQLTypes.CLASS], + interfaces, fields: outputFields, }); parseGraphQLSchema.graphQLTypes.push(classGraphQLOutputType); @@ -673,7 +682,7 @@ const load = ( const meType = new GraphQLObjectType({ name: 'Me', description: `The Me object type is used in operations that involve outputting the current user data.`, - interfaces: [defaultGraphQLTypes.CLASS], + interfaces, fields: () => ({ ...outputFields(), sessionToken: defaultGraphQLTypes.SESSION_TOKEN_ATT, From 6d067542fda975aa541353cb0430c4a8cf88b039 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 4 Aug 2019 20:11:44 -0700 Subject: [PATCH 07/34] Add relayStyle option to the CLI --- src/Options/Definitions.js | 6 ++++++ src/Options/docs.js | 1 + src/Options/index.js | 4 ++++ src/ParseServer.js | 1 + 4 files changed, 12 insertions(+) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 27fad63faa..a5d927fd46 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -306,6 +306,12 @@ module.exports.ParseServerOptions = { help: 'Read-only key, which has the same capabilities as MasterKey without writes', }, + relayStyle: { + env: 'PARSE_SERVER_RELAY_STYLE', + help: 'Mounts the GraphQL API using the relay style', + action: parsers.booleanParser, + default: false, + }, restAPIKey: { env: 'PARSE_SERVER_REST_API_KEY', help: 'Key for REST calls', diff --git a/src/Options/docs.js b/src/Options/docs.js index 1ecd8616ba..260f2832c1 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -55,6 +55,7 @@ * @property {String} publicServerURL Public URL to your parse server with http:// or https://. * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes + * @property {Boolean} relayStyle Mounts the GraphQL API using the relay style * @property {String} restAPIKey Key for REST calls * @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. * @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false. diff --git a/src/Options/index.js b/src/Options/index.js index e487509828..9e0fee33a7 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -191,6 +191,10 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_GRAPHQL_PATH :DEFAULT: /graphql */ graphQLPath: ?string; + /* Mounts the GraphQL API using the relay style + :ENV: PARSE_SERVER_RELAY_STYLE + :DEFAULT: false */ + relayStyle: ?boolean; /* Mounts the GraphQL Playground - never use this option in production :ENV: PARSE_SERVER_MOUNT_PLAYGROUND :DEFAULT: false */ diff --git a/src/ParseServer.js b/src/ParseServer.js index 4f7a788c10..9f06148a5f 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -257,6 +257,7 @@ class ParseServer { graphQLPath: options.graphQLPath, playgroundPath: options.playgroundPath, graphQLCustomTypeDefs, + relayStyle: options.relayStyle === true, }); if (options.mountGraphQL) { From 6c9c2d6beec2ec4e4f1f7f8491655a12050e27b4 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 4 Aug 2019 20:22:24 -0700 Subject: [PATCH 08/34] Move the id field to be the first one --- src/GraphQL/loaders/parseClassTypes.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 7759026f96..577845f917 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -534,7 +534,14 @@ const load = ( interfaces.push(parseGraphQLSchema.relayNodeInterface); } const outputFields = () => { - const outputFields = classOutputFields.reduce((fields, field) => { + let classFields = defaultGraphQLTypes.CLASS_FIELDS; + if (parseGraphQLSchema.relayNodeInterface) { + classFields = { + id: globalIdField(className, obj => obj.objectId), + ...classFields, + }; + } + return classOutputFields.reduce((fields, field) => { const type = mapOutputType( parseClass.fields[field].type, parseClass.fields[field].targetClass, @@ -636,11 +643,7 @@ const load = ( } else { return fields; } - }, defaultGraphQLTypes.CLASS_FIELDS); - if (parseGraphQLSchema.relayNodeInterface) { - outputFields.id = globalIdField(className, obj => obj.objectId); - } - return outputFields; + }, classFields); }; const classGraphQLOutputType = new GraphQLObjectType({ name: classGraphQLOutputTypeName, From c18bd3b4e5292a578e88f87fc6a2797ffd69f42f Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 4 Aug 2019 20:27:54 -0700 Subject: [PATCH 09/34] Fix node query --- src/GraphQL/loaders/defaultRelaySchema.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GraphQL/loaders/defaultRelaySchema.js b/src/GraphQL/loaders/defaultRelaySchema.js index e44d06d673..ebcb4b45a2 100644 --- a/src/GraphQL/loaders/defaultRelaySchema.js +++ b/src/GraphQL/loaders/defaultRelaySchema.js @@ -34,7 +34,8 @@ const load = parseGraphQLSchema => { } }, obj => { - return parseGraphQLSchema[obj.className].classGraphQLOutputType; + return parseGraphQLSchema.parseClassTypes[obj.className] + .classGraphQLOutputType; } ); From eabeb7a3e8fea66d66474930f51126aae9078d49 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Wed, 7 Aug 2019 01:04:44 -0700 Subject: [PATCH 10/34] Test for get query returning global id --- spec/ParseGraphQLServerRelay.spec.js | 123 +++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 spec/ParseGraphQLServerRelay.spec.js diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js new file mode 100644 index 0000000000..de413cab16 --- /dev/null +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -0,0 +1,123 @@ +const http = require('http'); +const fetch = require('node-fetch'); +const ws = require('ws'); +const express = require('express'); +const { ParseServer } = require('../'); +const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); +const { SubscriptionClient } = require('subscriptions-transport-ws'); +const { WebSocketLink } = require('apollo-link-ws'); +const { createUploadLink } = require('apollo-upload-client'); +const ApolloClient = require('apollo-client').default; +const { getMainDefinition } = require('apollo-utilities'); +const { split } = require('apollo-link'); +const { InMemoryCache } = require('apollo-cache-inmemory'); +const gql = require('graphql-tag'); + +describe('ParseGraphQLServer - Relay Style', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }; + + let apolloClient; + + beforeAll(async () => { + const parseServer = await global.reconfigureServer({}); + const parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + playgroundPath: '/playground', + subscriptionsPath: '/subscriptions', + relayStyle: true, + }); + const expressApp = express(); + const httpServer = http.createServer(expressApp); + expressApp.use('/parse', parseServer.app); + ParseServer.createLiveQueryServer(httpServer, { + port: 1338, + }); + parseGraphQLServer.applyGraphQL(expressApp); + parseGraphQLServer.applyPlayground(expressApp); + parseGraphQLServer.createSubscriptions(httpServer); + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); + + const subscriptionClient = new SubscriptionClient( + 'ws://localhost:13377/subscriptions', + { + reconnect: true, + connectionParams: headers, + }, + ws + ); + const wsLink = new WebSocketLink(subscriptionClient); + const httpLink = createUploadLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers, + }); + apolloClient = new ApolloClient({ + link: split( + ({ query }) => { + const { kind, operation } = getMainDefinition(query); + return kind === 'OperationDefinition' && operation === 'subscription'; + }, + wsLink, + httpLink + ), + cache: new InMemoryCache(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + }, + }, + }); + }); + + describe('Object Identification', () => { + it('Class get custom method should return valid gobal id', async () => { + const obj = new Parse.Object('SomeClass'); + obj.set('someField', 'some value'); + await obj.save(); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeClass($objectId: ID!) { + objects { + getSomeClass(objectId: $objectId) { + id + objectId + } + } + } + `, + variables: { + objectId: obj.id, + }, + }); + + expect(getResult.data.objects.getSomeClass.objectId).toBe(obj.id); + + const nodeResult = await apolloClient.query({ + query: gql` + query Node($id: ID!) { + node(id: $id) { + id + ... on SomeClassClass { + objectId + someField + } + } + } + `, + variables: { + id: getResult.data.objects.getSomeClass.id, + }, + }); + + expect(nodeResult.data.node.id).toBe( + getResult.data.objects.getSomeClass.id + ); + expect(nodeResult.data.node.objectId).toBe(obj.id); + expect(nodeResult.data.node.someField).toBe('some value'); + }); + }); +}); From 4c4e3cb089d85215c949cb70f44edc2540bc6531 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Wed, 7 Aug 2019 20:11:37 -0700 Subject: [PATCH 11/34] Find test for global id --- spec/ParseGraphQLServerRelay.spec.js | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js index de413cab16..8282c3e59e 100644 --- a/spec/ParseGraphQLServerRelay.spec.js +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -119,5 +119,73 @@ describe('ParseGraphQLServer - Relay Style', () => { expect(nodeResult.data.node.objectId).toBe(obj.id); expect(nodeResult.data.node.someField).toBe('some value'); }); + + it('Class find custom method should return valid gobal id', async () => { + const obj1 = new Parse.Object('SomeClass'); + obj1.set('someField', 'some value 1'); + await obj1.save(); + + const obj2 = new Parse.Object('SomeClass'); + obj2.set('someField', 'some value 2'); + await obj2.save(); + + const findResult = await apolloClient.query({ + query: gql` + query FindSomeClass { + objects { + findSomeClass(order: [createdAt_ASC]) { + results { + id + objectId + } + } + } + } + `, + }); + + expect(findResult.data.objects.findSomeClass.results[0].objectId).toBe( + obj1.id + ); + expect(findResult.data.objects.findSomeClass.results[1].objectId).toBe( + obj2.id + ); + + const nodeResult = await apolloClient.query({ + query: gql` + query Node($id1: ID!, $id2: ID!) { + node1: node(id: $id1) { + id + ... on SomeClassClass { + objectId + someField + } + } + node2: node(id: $id2) { + id + ... on SomeClassClass { + objectId + someField + } + } + } + `, + variables: { + id1: findResult.data.objects.findSomeClass.results[0].id, + id2: findResult.data.objects.findSomeClass.results[1].id, + }, + }); + + expect(nodeResult.data.node1.id).toBe( + findResult.data.objects.findSomeClass.results[0].id + ); + expect(nodeResult.data.node1.objectId).toBe(obj1.id); + expect(nodeResult.data.node1.someField).toBe('some value 1'); + expect(nodeResult.data.node2.id).toBe( + findResult.data.objects.findSomeClass.results[1].id + ); + expect(nodeResult.data.node2.objectId).toBe(obj2.id); + expect(nodeResult.data.node2.someField).toBe('some value 2'); + }); }); }); From 982868587c74c9ecbd80c39bc3a9c617117b3f7c Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Wed, 7 Aug 2019 20:50:41 -0700 Subject: [PATCH 12/34] Tests for relay types --- spec/ParseGraphQLServer.spec.js | 104 ++++++++++++++++++++++++--- spec/ParseGraphQLServerRelay.spec.js | 11 ++- src/GraphQL/ParseGraphQLServer.js | 2 +- 3 files changed, 106 insertions(+), 11 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 5d62b658fc..067c943af2 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -232,6 +232,7 @@ describe('ParseGraphQLServer', () => { describe('Auto API', () => { let httpServer; + let parseLiveQueryServer; const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-Javascript-Key': 'test', @@ -384,7 +385,7 @@ describe('ParseGraphQLServer', () => { const expressApp = express(); httpServer = http.createServer(expressApp); expressApp.use('/parse', parseServer.app); - ParseServer.createLiveQueryServer(httpServer, { + parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, { port: 1338, }); parseGraphQLServer.applyGraphQL(expressApp); @@ -427,6 +428,7 @@ describe('ParseGraphQLServer', () => { }); afterAll(async () => { + await parseLiveQueryServer.server.close(); await httpServer.close(); }); @@ -512,6 +514,13 @@ describe('ParseGraphQLServer', () => { }); describe('Schema', () => { + const resetGraphQLCache = async () => { + await Promise.all([ + parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), + parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), + ]); + }; + describe('Default Types', () => { it('should have Object scalar type', async () => { const objectType = (await apolloClient.query({ @@ -782,14 +791,93 @@ describe('ParseGraphQLServer', () => { }); }); - describe('Configuration', function() { - const resetGraphQLCache = async () => { - await Promise.all([ - parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), - parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), - ]); - }; + describe('Relay Specific Types', () => { + afterEach(() => { + parseGraphQLServer.setRelayStyle(false); + }); + + it('should have relay specific types when relay style is enabled', async () => { + await parseGraphQLServer.setRelayStyle(true); + await resetGraphQLCache(); + + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } + } + } + `, + })).data['__schema'].types.map(type => type.name); + expect(schemaTypes).toContain('Node'); + + const userFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "_UserClass") { + fields { + name + } + } + } + `, + })).data['__type'].fields.map(field => field.name); + + expect(userFields).toContain('id'); + }); + + it('should not have relay specific types when relay style is disabled', async () => { + await parseGraphQLServer.setRelayStyle(false); + await resetGraphQLCache(); + + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } + } + } + `, + })).data['__schema'].types.map(type => type.name); + + expect(schemaTypes).not.toContain('Node'); + + const queryFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "Query") { + fields { + name + } + } + } + `, + })).data['__type'].fields.map(field => field.name); + + expect(queryFields).not.toContain('node'); + + const userFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "_UserClass") { + fields { + name + } + } + } + `, + })).data['__type'].fields.map(field => field.name); + + expect(userFields).not.toContain('id'); + }); + }); + + describe('Configuration', function() { beforeEach(async () => { await parseGraphQLServer.setGraphQLConfig({}); await resetGraphQLCache(); diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js index 8282c3e59e..2586dadb92 100644 --- a/spec/ParseGraphQLServerRelay.spec.js +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -14,6 +14,8 @@ const { InMemoryCache } = require('apollo-cache-inmemory'); const gql = require('graphql-tag'); describe('ParseGraphQLServer - Relay Style', () => { + let httpServer; + let parseLiveQueryServer; const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-Javascript-Key': 'test', @@ -30,9 +32,9 @@ describe('ParseGraphQLServer - Relay Style', () => { relayStyle: true, }); const expressApp = express(); - const httpServer = http.createServer(expressApp); + httpServer = http.createServer(expressApp); expressApp.use('/parse', parseServer.app); - ParseServer.createLiveQueryServer(httpServer, { + parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, { port: 1338, }); parseGraphQLServer.applyGraphQL(expressApp); @@ -72,6 +74,11 @@ describe('ParseGraphQLServer - Relay Style', () => { }); }); + afterAll(async () => { + await parseLiveQueryServer.server.close(); + await httpServer.close(); + }); + describe('Object Identification', () => { it('Class get custom method should return valid gobal id', async () => { const obj = new Parse.Object('SomeClass'); diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 599ea2fa5b..524209cca8 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -126,7 +126,7 @@ class ParseGraphQLServer { return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); } - setRelaySyle(relayStyle) { + setRelayStyle(relayStyle) { this._config.relayStyle = relayStyle; this.parseGraphQLSchema.relayStyle = relayStyle; } From 23123c93f72189566edd6430c2a4439f3d99b32d Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Wed, 7 Aug 2019 23:11:06 -0700 Subject: [PATCH 13/34] Create file mutation --- src/GraphQL/loaders/filesMutations.js | 139 +++++++++++++++----------- 1 file changed, 80 insertions(+), 59 deletions(-) diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 68ad4e8628..bdc147e954 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -1,5 +1,6 @@ import { GraphQLObjectType, GraphQLNonNull } from 'graphql'; import { GraphQLUpload } from 'graphql-upload'; +import { mutationWithClientMutationId } from 'graphql-relay'; import Parse from 'parse/node'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import logger from '../../logger'; @@ -7,75 +8,95 @@ import logger from '../../logger'; const load = parseGraphQLSchema => { const fields = {}; - fields.create = { - description: - 'The create mutation can be used to create and upload a new file.', - args: { - file: { - description: 'This is the new file to be created and uploaded', - type: new GraphQLNonNull(GraphQLUpload), - }, + const description = + 'The create mutation can be used to create and upload a new file.'; + const args = { + file: { + description: 'This is the new file to be created and uploaded', + type: new GraphQLNonNull(GraphQLUpload), }, - type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO), - async resolve(_source, args, context) { - try { - const { file } = args; - const { config } = context; + }; + const type = new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO); + const resolve = async (_source, args, context) => { + try { + const { file } = args; + const { config } = context; - const { createReadStream, filename, mimetype } = await file; - let data = null; - if (createReadStream) { - const stream = createReadStream(); - data = await new Promise((resolve, reject) => { - let data = ''; - stream - .on('error', reject) - .on('data', chunk => (data += chunk)) - .on('end', () => resolve(data)); - }); - } + const { createReadStream, filename, mimetype } = await file; + let data = null; + if (createReadStream) { + const stream = createReadStream(); + data = await new Promise((resolve, reject) => { + let data = ''; + stream + .on('error', reject) + .on('data', chunk => (data += chunk)) + .on('end', () => resolve(data)); + }); + } - if (!data || !data.length) { - throw new Parse.Error( - Parse.Error.FILE_SAVE_ERROR, - 'Invalid file upload.' - ); - } + if (!data || !data.length) { + throw new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + 'Invalid file upload.' + ); + } - if (filename.length > 128) { - throw new Parse.Error( - Parse.Error.INVALID_FILE_NAME, - 'Filename too long.' - ); - } + if (filename.length > 128) { + throw new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'Filename too long.' + ); + } - if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { - throw new Parse.Error( - Parse.Error.INVALID_FILE_NAME, - 'Filename contains invalid characters.' - ); - } + if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { + throw new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'Filename contains invalid characters.' + ); + } - try { - return await config.filesController.createFile( - config, - filename, - data, - mimetype - ); - } catch (e) { - logger.error('Error creating a file: ', e); - throw new Parse.Error( - Parse.Error.FILE_SAVE_ERROR, - `Could not store file: ${filename}.` - ); - } + try { + return await config.filesController.createFile( + config, + filename, + data, + mimetype + ); } catch (e) { - parseGraphQLSchema.handleError(e); + logger.error('Error creating a file: ', e); + throw new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + `Could not store file: ${filename}.` + ); } - }, + } catch (e) { + parseGraphQLSchema.handleError(e); + } }; + let createField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + createField = mutationWithClientMutationId({ + name: 'CreateFile', + inputFields: args, + outputFields: { + fileInfo: { type }, + }, + mutateAndGetPayload: (object, ctx) => resolve(undefined, object, ctx), + }); + parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(createField.type); + } else { + createField = { + description, + args, + type, + resolve, + }; + } + fields.create = createField; + const filesMutation = new GraphQLObjectType({ name: 'FilesMutation', description: 'FilesMutation is the top level type for files mutations.', From d119b5fe4b751d9658b75faaefccbb2bcbcedadc Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 00:14:57 -0700 Subject: [PATCH 14/34] Create file mutation test --- spec/ParseGraphQLServerRelay.spec.js | 68 ++++++++++++++++++++++++++- src/GraphQL/loaders/filesMutations.js | 4 +- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js index 2586dadb92..1f27c8db37 100644 --- a/spec/ParseGraphQLServerRelay.spec.js +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -1,7 +1,9 @@ const http = require('http'); const fetch = require('node-fetch'); +const FormData = require('form-data'); const ws = require('ws'); const express = require('express'); +const uuidv4 = require('uuid/v4'); const { ParseServer } = require('../'); const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); const { SubscriptionClient } = require('subscriptions-transport-ws'); @@ -14,6 +16,7 @@ const { InMemoryCache } = require('apollo-cache-inmemory'); const gql = require('graphql-tag'); describe('ParseGraphQLServer - Relay Style', () => { + let parseServer; let httpServer; let parseLiveQueryServer; const headers = { @@ -24,7 +27,7 @@ describe('ParseGraphQLServer - Relay Style', () => { let apolloClient; beforeAll(async () => { - const parseServer = await global.reconfigureServer({}); + parseServer = await global.reconfigureServer({}); const parseGraphQLServer = new ParseGraphQLServer(parseServer, { graphQLPath: '/graphql', playgroundPath: '/playground', @@ -195,4 +198,67 @@ describe('ParseGraphQLServer - Relay Style', () => { expect(nodeResult.data.node2.someField).toBe('some value 2'); }); }); + + describe('Mutations', () => { + it('should create file with clientMutationId', async () => { + parseServer = await global.reconfigureServer({ + publicServerURL: 'http://localhost:13377/parse', + }); + + const clientMutationId = uuidv4(); + const body = new FormData(); + body.append( + 'operations', + JSON.stringify({ + query: ` + mutation CreateFile($file: Upload!, $clientMutationId: String) { + files { + create(input: { file: $file, clientMutationId: $clientMutationId }) { + fileInfo { + name, + url + }, + clientMutationId + } + } + } + `, + variables: { + file: null, + clientMutationId, + }, + }) + ); + body.append('map', JSON.stringify({ 1: ['variables.file'] })); + body.append('1', 'My File Content', { + filename: 'myFileName.txt', + contentType: 'text/plain', + }); + + let res = await fetch('http://localhost:13377/graphql', { + method: 'POST', + headers, + body, + }); + + expect(res.status).toEqual(200); + + const result = JSON.parse(await res.text()); + + expect(result.data.files.create.fileInfo.name).toEqual( + jasmine.stringMatching(/_myFileName.txt$/) + ); + expect(result.data.files.create.fileInfo.url).toEqual( + jasmine.stringMatching(/_myFileName.txt$/) + ); + expect(result.data.files.create.clientMutationId).toEqual( + clientMutationId + ); + + res = await fetch(result.data.files.create.fileInfo.url); + + expect(res.status).toEqual(200); + expect(await res.text()).toEqual('My File Content'); + }); + }); }); diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index bdc147e954..4f55cdaa47 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -83,7 +83,9 @@ const load = parseGraphQLSchema => { outputFields: { fileInfo: { type }, }, - mutateAndGetPayload: (object, ctx) => resolve(undefined, object, ctx), + mutateAndGetPayload: async (object, ctx) => ({ + fileInfo: resolve(undefined, object, ctx), + }), }); parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); parseGraphQLSchema.graphQLTypes.push(createField.type); From 68b29a1b6e8c2875fef7bd25f511cfc65ed72602 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 00:35:28 -0700 Subject: [PATCH 15/34] Relay creaFile mutation schema tests --- spec/ParseGraphQLServer.spec.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 067c943af2..9b95b41db5 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -827,6 +827,22 @@ describe('ParseGraphQLServer', () => { })).data['__type'].fields.map(field => field.name); expect(userFields).toContain('id'); + + const createFileInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFileInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(createFileInputFields).toEqual(['clientMutationId', 'file']); }); it('should not have relay specific types when relay style is disabled', async () => { @@ -874,6 +890,20 @@ describe('ParseGraphQLServer', () => { })).data['__type'].fields.map(field => field.name); expect(userFields).not.toContain('id'); + + const createFileInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFileInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(createFileInputType).toBeNull(); }); }); From bde9a588a44fe49385192c2529710917545f81b9 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 13:56:19 -0700 Subject: [PATCH 16/34] Relay style for functions mutations --- src/GraphQL/loaders/functionsMutations.js | 81 ++++++++++++++--------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index 6a7b9a3de9..f7ddd0d4f4 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -1,43 +1,64 @@ import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import { FunctionsRouter } from '../../Routers/FunctionsRouter'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; const load = parseGraphQLSchema => { const fields = {}; - fields.call = { - description: - 'The call mutation can be used to invoke a cloud code function.', - args: { - functionName: { - description: 'This is the name of the function to be called.', - type: new GraphQLNonNull(GraphQLString), - }, - params: { - description: 'These are the params to be passed to the function.', - type: defaultGraphQLTypes.OBJECT, - }, + const description = + 'The call mutation can be used to invoke a cloud code function.'; + const args = { + functionName: { + description: 'This is the name of the function to be called.', + type: new GraphQLNonNull(GraphQLString), }, - type: defaultGraphQLTypes.ANY, - async resolve(_source, args, context) { - try { - const { functionName, params } = args; - const { config, auth, info } = context; - - return (await FunctionsRouter.handleCloudFunction({ - params: { - functionName, - }, - config, - auth, - info, - body: params, - })).response.result; - } catch (e) { - parseGraphQLSchema.handleError(e); - } + params: { + description: 'These are the params to be passed to the function.', + type: defaultGraphQLTypes.OBJECT, }, }; + const type = defaultGraphQLTypes.ANY; + const resolve = async (_source, args, context) => { + try { + const { functionName, params } = args; + const { config, auth, info } = context; + + return (await FunctionsRouter.handleCloudFunction({ + params: { + functionName, + }, + config, + auth, + info, + body: params, + })).response.result; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }; + + let callField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + callField = mutationWithClientMutationId({ + name: 'CallFunction', + inputFields: args, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (object, ctx) => ({ + fileInfo: resolve(undefined, object, ctx), + }), + }); + } else { + callField = { + description, + args, + type, + resolve, + }; + } + fields.call = callField; const functionsMutation = new GraphQLObjectType({ name: 'FunctionsMutation', From 485976f68abbb751693228b2c0d6de28409765c8 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 14:04:52 -0700 Subject: [PATCH 17/34] Fix result bug --- src/GraphQL/loaders/functionsMutations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index f7ddd0d4f4..effde0e459 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -47,7 +47,7 @@ const load = parseGraphQLSchema => { result: { type }, }, mutateAndGetPayload: async (object, ctx) => ({ - fileInfo: resolve(undefined, object, ctx), + result: resolve(undefined, object, ctx), }), }); } else { From 8e895c49983e56e308d73c773ac99b2698229d46 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 14:32:41 -0700 Subject: [PATCH 18/34] Tests for clientMutationId in function calls --- spec/ParseGraphQLServer.spec.js | 100 +++++++++++++++++++++++++++ spec/ParseGraphQLServerRelay.spec.js | 35 ++++++++++ 2 files changed, 135 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 9b95b41db5..2b2e3286f4 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -843,6 +843,64 @@ describe('ParseGraphQLServer', () => { .sort(); expect(createFileInputFields).toEqual(['clientMutationId', 'file']); + + const createFilePayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFilePayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(createFilePayloadFields).toEqual([ + 'clientMutationId', + 'fileInfo', + ]); + + const callFunctionInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(callFunctionInputFields).toEqual([ + 'clientMutationId', + 'functionName', + 'params', + ]); + + const callFunctionPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(callFunctionPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); }); it('should not have relay specific types when relay style is disabled', async () => { @@ -904,6 +962,48 @@ describe('ParseGraphQLServer', () => { })).data['__type']; expect(createFileInputType).toBeNull(); + + const createFilePayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFilePayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(createFilePayloadType).toBeNull(); + + const callFunctionInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(callFunctionInputType).toBeNull(); + + const callFunctionPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(callFunctionPayloadType).toBeNull(); }); }); diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js index 1f27c8db37..682fe297f2 100644 --- a/spec/ParseGraphQLServerRelay.spec.js +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -260,5 +260,40 @@ describe('ParseGraphQLServer - Relay Style', () => { expect(res.status).toEqual(200); expect(await res.text()).toEqual('My File Content'); }); + + it('should call function with clientMutationId', async () => { + const clientMutationId = uuidv4(); + + Parse.Cloud.define('hello', req => { + return `Hello, ${req.params.name}!!!`; + }); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation CallFunction($input: CallFunctionInput!) { + functions { + call(input: $input) { + result + clientMutationId + } + } + } + `, + variables: { + input: { + functionName: 'hello', + params: { + name: 'dude', + }, + clientMutationId, + }, + }, + }); + + expect(result.data.functions.call.result).toEqual('Hello, dude!!!'); + expect(result.data.functions.call.clientMutationId).toEqual( + clientMutationId + ); + }); }); }); From 51970fc0816d9f23e23e0849904bab5f00440247 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 14:50:00 -0700 Subject: [PATCH 19/34] Relay style for create object generic mutation --- src/GraphQL/loaders/filesMutations.js | 4 +- src/GraphQL/loaders/functionsMutations.js | 4 +- src/GraphQL/loaders/objectsMutations.js | 63 ++++++++++++++++------- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 4f55cdaa47..97e31b389e 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -83,8 +83,8 @@ const load = parseGraphQLSchema => { outputFields: { fileInfo: { type }, }, - mutateAndGetPayload: async (object, ctx) => ({ - fileInfo: resolve(undefined, object, ctx), + mutateAndGetPayload: async (args, context) => ({ + fileInfo: resolve(undefined, args, context), }), }); parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index effde0e459..34edfdf882 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -46,8 +46,8 @@ const load = parseGraphQLSchema => { outputFields: { result: { type }, }, - mutateAndGetPayload: async (object, ctx) => ({ - result: resolve(undefined, object, ctx), + mutateAndGetPayload: async (args, context) => ({ + result: resolve(undefined, args, context), }), }); } else { diff --git a/src/GraphQL/loaders/objectsMutations.js b/src/GraphQL/loaders/objectsMutations.js index 7623b6eeca..f9bce10282 100644 --- a/src/GraphQL/loaders/objectsMutations.js +++ b/src/GraphQL/loaders/objectsMutations.js @@ -1,4 +1,5 @@ import { GraphQLNonNull, GraphQLBoolean, GraphQLObjectType } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import rest from '../../rest'; @@ -62,27 +63,51 @@ const deleteObject = async (className, objectId, config, auth, info) => { return true; }; -const load = parseGraphQLSchema => { - parseGraphQLSchema.graphQLObjectsMutations.create = { - description: - 'The create mutation can be used to create a new object of a certain class.', - args: { - className: defaultGraphQLTypes.CLASS_NAME_ATT, - fields: defaultGraphQLTypes.FIELDS_ATT, - }, - type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT), - async resolve(_source, args, context) { - try { - const { className, fields } = args; - const { config, auth, info } = context; - - return await createObject(className, fields, config, auth, info); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, +const loadCreate = parseGraphQLSchema => { + const description = + 'The create mutation can be used to create a new object of a certain class.'; + const args = { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + fields: defaultGraphQLTypes.FIELDS_ATT, + }; + const type = new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT); + const resolve = async (_source, args, context) => { + try { + const { className, fields } = args; + const { config, auth, info } = context; + + return await createObject(className, fields, config, auth, info); + } catch (e) { + parseGraphQLSchema.handleError(e); + } }; + let createField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + createField = mutationWithClientMutationId({ + name: 'CreateObject', + inputFields: args, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (args, context) => ({ + result: resolve(undefined, args, context), + }), + }); + } else { + createField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations.create = createField; +}; + +const load = parseGraphQLSchema => { + loadCreate(parseGraphQLSchema); + parseGraphQLSchema.graphQLObjectsMutations.update = { description: 'The update mutation can be used to update an object of a certain class.', From 763848d256c792bec86e59d9461a8515d6dfcdbf Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 15:32:34 -0700 Subject: [PATCH 20/34] Relay style for update object generic mutation --- src/GraphQL/loaders/objectsMutations.js | 79 ++++++++++++++++--------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/GraphQL/loaders/objectsMutations.js b/src/GraphQL/loaders/objectsMutations.js index f9bce10282..f020f5e295 100644 --- a/src/GraphQL/loaders/objectsMutations.js +++ b/src/GraphQL/loaders/objectsMutations.js @@ -105,37 +105,60 @@ const loadCreate = parseGraphQLSchema => { parseGraphQLSchema.graphQLObjectsMutations.create = createField; }; -const load = parseGraphQLSchema => { - loadCreate(parseGraphQLSchema); - - parseGraphQLSchema.graphQLObjectsMutations.update = { - description: - 'The update mutation can be used to update an object of a certain class.', - args: { - className: defaultGraphQLTypes.CLASS_NAME_ATT, - objectId: defaultGraphQLTypes.OBJECT_ID_ATT, - fields: defaultGraphQLTypes.FIELDS_ATT, - }, - type: new GraphQLNonNull(defaultGraphQLTypes.UPDATE_RESULT), - async resolve(_source, args, context) { - try { - const { className, objectId, fields } = args; - const { config, auth, info } = context; +const loadUpdate = parseGraphQLSchema => { + const description = + 'The update mutation can be used to update an object of a certain class.'; + const args = { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + fields: defaultGraphQLTypes.FIELDS_ATT, + }; + const type = new GraphQLNonNull(defaultGraphQLTypes.UPDATE_RESULT); + const resolve = async (_source, args, context) => { + try { + const { className, objectId, fields } = args; + const { config, auth, info } = context; - return await updateObject( - className, - objectId, - fields, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + return await updateObject( + className, + objectId, + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } }; + let updateField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + updateField = mutationWithClientMutationId({ + name: 'UpdateObject', + inputFields: args, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (args, context) => ({ + result: resolve(undefined, args, context), + }), + }); + } else { + updateField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations.update = updateField; +}; + +const load = parseGraphQLSchema => { + loadCreate(parseGraphQLSchema); + loadUpdate(parseGraphQLSchema); + parseGraphQLSchema.graphQLObjectsMutations.delete = { description: 'The delete mutation can be used to delete an object of a certain class.', From bd5008d89a33e590aa1a5c7e501909c8911cd59d Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 15:43:36 -0700 Subject: [PATCH 21/34] Relay style for delete object generic mutation --- src/GraphQL/loaders/filesMutations.js | 2 +- src/GraphQL/loaders/functionsMutations.js | 2 +- src/GraphQL/loaders/objectsMutations.js | 67 +++++++++++++++-------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 97e31b389e..703b673833 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -84,7 +84,7 @@ const load = parseGraphQLSchema => { fileInfo: { type }, }, mutateAndGetPayload: async (args, context) => ({ - fileInfo: resolve(undefined, args, context), + fileInfo: await resolve(undefined, args, context), }), }); parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index 34edfdf882..5ed3d66f75 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -47,7 +47,7 @@ const load = parseGraphQLSchema => { result: { type }, }, mutateAndGetPayload: async (args, context) => ({ - result: resolve(undefined, args, context), + result: await resolve(undefined, args, context), }), }); } else { diff --git a/src/GraphQL/loaders/objectsMutations.js b/src/GraphQL/loaders/objectsMutations.js index f020f5e295..c3e890164c 100644 --- a/src/GraphQL/loaders/objectsMutations.js +++ b/src/GraphQL/loaders/objectsMutations.js @@ -91,7 +91,7 @@ const loadCreate = parseGraphQLSchema => { result: { type }, }, mutateAndGetPayload: async (args, context) => ({ - result: resolve(undefined, args, context), + result: await resolve(undefined, args, context), }), }); } else { @@ -141,7 +141,7 @@ const loadUpdate = parseGraphQLSchema => { result: { type }, }, mutateAndGetPayload: async (args, context) => ({ - result: resolve(undefined, args, context), + result: await resolve(undefined, args, context), }), }); } else { @@ -155,29 +155,52 @@ const loadUpdate = parseGraphQLSchema => { parseGraphQLSchema.graphQLObjectsMutations.update = updateField; }; +const loadDelete = parseGraphQLSchema => { + const description = + 'The delete mutation can be used to delete an object of a certain class.'; + const args = { + className: defaultGraphQLTypes.CLASS_NAME_ATT, + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + }; + const type = new GraphQLNonNull(GraphQLBoolean); + const resolve = async (_source, args, context) => { + try { + const { className, objectId } = args; + const { config, auth, info } = context; + + return await deleteObject(className, objectId, config, auth, info); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }; + + let deleteField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + deleteField = mutationWithClientMutationId({ + name: 'DeleteObject', + inputFields: args, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (args, context) => ({ + result: await resolve(undefined, args, context), + }), + }); + } else { + deleteField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations.delete = deleteField; +}; + const load = parseGraphQLSchema => { loadCreate(parseGraphQLSchema); loadUpdate(parseGraphQLSchema); - - parseGraphQLSchema.graphQLObjectsMutations.delete = { - description: - 'The delete mutation can be used to delete an object of a certain class.', - args: { - className: defaultGraphQLTypes.CLASS_NAME_ATT, - objectId: defaultGraphQLTypes.OBJECT_ID_ATT, - }, - type: new GraphQLNonNull(GraphQLBoolean), - async resolve(_source, args, context) { - try { - const { className, objectId } = args; - const { config, auth, info } = context; - - return await deleteObject(className, objectId, config, auth, info); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, - }; + loadDelete(parseGraphQLSchema); const objectsMutation = new GraphQLObjectType({ name: 'ObjectsMutation', From fad70107c3dbca917d1ba2d77d5eb3900544ebd9 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 15:58:59 -0700 Subject: [PATCH 22/34] Tests for objects generic mutations running on relay style --- spec/ParseGraphQLServerRelay.spec.js | 127 +++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js index 682fe297f2..a1b614e140 100644 --- a/spec/ParseGraphQLServerRelay.spec.js +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -295,5 +295,132 @@ describe('ParseGraphQLServer - Relay Style', () => { clientMutationId ); }); + + it('should create object with clientMutationId in the generic mutation', async () => { + const clientMutationId = uuidv4(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation CreateObject($input: CreateObjectInput!) { + objects { + create(input: $input) { + result { + objectId + createdAt + } + clientMutationId + } + } + } + `, + variables: { + input: { + className: 'SomeClass', + fields: { + someField: 'some value', + }, + clientMutationId, + }, + }, + }); + + expect(result.data.objects.create.result.objectId).toBeDefined(); + + const obj = await new Parse.Query('SomeClass').get( + result.data.objects.create.result.objectId + ); + + expect(obj.createdAt).toEqual( + new Date(result.data.objects.create.result.createdAt) + ); + expect(obj.get('someField')).toEqual('some value'); + + expect(result.data.objects.create.clientMutationId).toEqual( + clientMutationId + ); + }); + + it('should update object with clientMutationId in the generic mutation', async () => { + const clientMutationId = uuidv4(); + + const obj = new Parse.Object('SomeClass'); + obj.set('someField1', 'some field 1 value 1'); + obj.set('someField2', 'some field 2 value 1'); + await obj.save(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation UpdateObject($input: UpdateObjectInput!) { + objects { + update(input: $input) { + result { + updatedAt + } + clientMutationId + } + } + } + `, + variables: { + input: { + className: 'SomeClass', + objectId: obj.id, + fields: { + someField1: 'some field 1 value 2', + }, + clientMutationId, + }, + }, + }); + + await obj.fetch(); + + expect(obj.updatedAt).toEqual( + new Date(result.data.objects.update.result.updatedAt) + ); + expect(obj.get('someField1')).toEqual('some field 1 value 2'); + expect(obj.get('someField2')).toEqual('some field 2 value 1'); + + expect(result.data.objects.update.clientMutationId).toEqual( + clientMutationId + ); + }); + + it('should delete object with clientMutationId in the generic mutation', async () => { + const clientMutationId = uuidv4(); + + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation DeleteObject($input: DeleteObjectInput!) { + objects { + delete(input: $input) { + result + clientMutationId + } + } + } + `, + variables: { + input: { + className: 'SomeClass', + objectId: obj.id, + clientMutationId, + }, + }, + }); + + expect(result.data.objects.delete.result).toEqual(true); + + await expectAsync(obj.fetch({ useMasterKey: true })).toBeRejectedWith( + jasmine.stringMatching('Object not found') + ); + + expect(result.data.objects.delete.clientMutationId).toEqual( + clientMutationId + ); + }); }); }); From 8426b25ea478afd66be9b63d88c4a453d758dc23 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 16:51:33 -0700 Subject: [PATCH 23/34] Improving tests organization --- spec/ParseGraphQLServer.spec.js | 360 ++++++++++++++++++-------------- 1 file changed, 201 insertions(+), 159 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 2b2e3286f4..fb8c840ccf 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -792,218 +792,260 @@ describe('ParseGraphQLServer', () => { }); describe('Relay Specific Types', () => { - afterEach(() => { - parseGraphQLServer.setRelayStyle(false); - }); + describe('when relay style is enabled', () => { + beforeAll(async () => { + parseGraphQLServer.setRelayStyle(true); + await resetGraphQLCache(); + }); - it('should have relay specific types when relay style is enabled', async () => { - await parseGraphQLServer.setRelayStyle(true); - await resetGraphQLCache(); + afterAll(async () => { + parseGraphQLServer.setRelayStyle(false); + await resetGraphQLCache(); + }); - const schemaTypes = (await apolloClient.query({ - query: gql` - query SchemaTypes { - __schema { - types { - name + it('should have Node interface', async () => { + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } } } - } - `, - })).data['__schema'].types.map(type => type.name); + `, + })).data['__schema'].types.map(type => type.name); - expect(schemaTypes).toContain('Node'); + expect(schemaTypes).toContain('Node'); + }); - const userFields = (await apolloClient.query({ - query: gql` - query UserType { - __type(name: "_UserClass") { - fields { - name + it('should have node query', async () => { + const queryFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "Query") { + fields { + name + } } } - } - `, - })).data['__type'].fields.map(field => field.name); + `, + })).data['__type'].fields.map(field => field.name); - expect(userFields).toContain('id'); + expect(queryFields).toContain('node'); + }); - const createFileInputFields = (await apolloClient.query({ - query: gql` - query { - __type(name: "CreateFileInput") { - inputFields { - name + it('should return global id', async () => { + const userFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "_UserClass") { + fields { + name + } } } - } - `, - })).data['__type'].inputFields - .map(field => field.name) - .sort(); + `, + })).data['__type'].fields.map(field => field.name); - expect(createFileInputFields).toEqual(['clientMutationId', 'file']); + expect(userFields).toContain('id'); + }); - const createFilePayloadFields = (await apolloClient.query({ - query: gql` - query { - __type(name: "CreateFilePayload") { - fields { - name + it('should have clientMutationId in create file input', async () => { + const createFileInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFileInput") { + inputFields { + name + } } } - } - `, - })).data['__type'].fields - .map(field => field.name) - .sort(); + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); - expect(createFilePayloadFields).toEqual([ - 'clientMutationId', - 'fileInfo', - ]); + expect(createFileInputFields).toEqual(['clientMutationId', 'file']); + }); - const callFunctionInputFields = (await apolloClient.query({ - query: gql` - query { - __type(name: "CallFunctionInput") { - inputFields { - name + it('should have clientMutationId in create file payload', async () => { + const createFilePayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFilePayload") { + fields { + name + } } } - } - `, - })).data['__type'].inputFields - .map(field => field.name) - .sort(); - - expect(callFunctionInputFields).toEqual([ - 'clientMutationId', - 'functionName', - 'params', - ]); + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); - const callFunctionPayloadFields = (await apolloClient.query({ - query: gql` - query { - __type(name: "CallFunctionPayload") { - fields { - name + expect(createFilePayloadFields).toEqual([ + 'clientMutationId', + 'fileInfo', + ]); + }); + + it('should have clientMutationId in call function input', async () => { + const callFunctionInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionInput") { + inputFields { + name + } } } - } - `, - })).data['__type'].fields - .map(field => field.name) - .sort(); + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(callFunctionInputFields).toEqual([ + 'clientMutationId', + 'functionName', + 'params', + ]); + }); - expect(callFunctionPayloadFields).toEqual([ - 'clientMutationId', - 'result', - ]); - }); + it('should have clientMutationId in call function payload', async () => { + const callFunctionPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); - it('should not have relay specific types when relay style is disabled', async () => { - await parseGraphQLServer.setRelayStyle(false); - await resetGraphQLCache(); + expect(callFunctionPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); + }); - const schemaTypes = (await apolloClient.query({ - query: gql` - query SchemaTypes { - __schema { - types { - name + describe('when relay style is disabled', () => { + it('should not Node interface', async () => { + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } } } - } - `, - })).data['__schema'].types.map(type => type.name); + `, + })).data['__schema'].types.map(type => type.name); - expect(schemaTypes).not.toContain('Node'); + expect(schemaTypes).not.toContain('Node'); + }); - const queryFields = (await apolloClient.query({ - query: gql` - query UserType { - __type(name: "Query") { - fields { - name + it('should not have node query', async () => { + const queryFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "Query") { + fields { + name + } } } - } - `, - })).data['__type'].fields.map(field => field.name); + `, + })).data['__type'].fields.map(field => field.name); - expect(queryFields).not.toContain('node'); + expect(queryFields).not.toContain('node'); + }); - const userFields = (await apolloClient.query({ - query: gql` - query UserType { - __type(name: "_UserClass") { - fields { - name + it('should not return global id', async () => { + const userFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "_UserClass") { + fields { + name + } } } - } - `, - })).data['__type'].fields.map(field => field.name); + `, + })).data['__type'].fields.map(field => field.name); - expect(userFields).not.toContain('id'); + expect(userFields).not.toContain('id'); + }); - const createFileInputType = (await apolloClient.query({ - query: gql` - query { - __type(name: "CreateFileInput") { - inputFields { - name + it('should not have create file input', async () => { + const createFileInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFileInput") { + inputFields { + name + } } } - } - `, - })).data['__type']; + `, + })).data['__type']; - expect(createFileInputType).toBeNull(); + expect(createFileInputType).toBeNull(); + }); - const createFilePayloadType = (await apolloClient.query({ - query: gql` - query { - __type(name: "CreateFilePayload") { - fields { - name + it('should not have create file payload', async () => { + const createFilePayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFilePayload") { + fields { + name + } } } - } - `, - })).data['__type']; + `, + })).data['__type']; - expect(createFilePayloadType).toBeNull(); + expect(createFilePayloadType).toBeNull(); + }); - const callFunctionInputType = (await apolloClient.query({ - query: gql` - query { - __type(name: "CallFunctionInput") { - inputFields { - name + it('should not have call function input', async () => { + const callFunctionInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionInput") { + inputFields { + name + } } } - } - `, - })).data['__type']; + `, + })).data['__type']; - expect(callFunctionInputType).toBeNull(); + expect(callFunctionInputType).toBeNull(); + }); - const callFunctionPayloadType = (await apolloClient.query({ - query: gql` - query { - __type(name: "CallFunctionPayload") { - fields { - name + it('should not have call function payload', async () => { + const callFunctionPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionPayload") { + fields { + name + } } } - } - `, - })).data['__type']; + `, + })).data['__type']; - expect(callFunctionPayloadType).toBeNull(); + expect(callFunctionPayloadType).toBeNull(); + }); }); }); From c84a2c85f3d64adad676732012575322eb0a719a Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 17:02:35 -0700 Subject: [PATCH 24/34] relay style tests for generic object mutations --- spec/ParseGraphQLServer.spec.js | 226 ++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index fb8c840ccf..303a3bc793 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -932,6 +932,136 @@ describe('ParseGraphQLServer', () => { 'result', ]); }); + + it('should have clientMutationId in generic create object mutation input', async () => { + const createObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(createObjectInputFields).toEqual([ + 'className', + 'clientMutationId', + 'fields', + ]); + }); + + it('should have clientMutationId in generic create object mutation payload', async () => { + const createObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(createObjectPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); + + it('should have clientMutationId in generic update object mutation input', async () => { + const updateObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(updateObjectInputFields).toEqual([ + 'className', + 'clientMutationId', + 'fields', + 'objectId', + ]); + }); + + it('should have clientMutationId in generic update object mutation payload', async () => { + const updateObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(updateObjectPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); + + it('should have clientMutationId in generic delete object mutation input', async () => { + const deleteObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(deleteObjectInputFields).toEqual([ + 'className', + 'clientMutationId', + 'objectId', + ]); + }); + + it('should have clientMutationId in generic delete object mutation payload', async () => { + const deleteObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(deleteObjectPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); }); describe('when relay style is disabled', () => { @@ -1046,6 +1176,102 @@ describe('ParseGraphQLServer', () => { expect(callFunctionPayloadType).toBeNull(); }); + + it('should not have create object input', async () => { + const createObjectInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(createObjectInputType).toBeNull(); + }); + + it('should not have create object payload', async () => { + const createObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(createObjectPayloadType).toBeNull(); + }); + + it('should not have update object input', async () => { + const updateObjectInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(updateObjectInputType).toBeNull(); + }); + + it('should not have update object payload', async () => { + const updateObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(updateObjectPayloadType).toBeNull(); + }); + + it('should not have delete object input', async () => { + const deleteObjectInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(deleteObjectInputType).toBeNull(); + }); + + it('should not have delete object payload', async () => { + const deleteObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(deleteObjectPayloadType).toBeNull(); + }); }); }); From 65785907599abae7f9a4e3135b017f78135467a9 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 17:37:45 -0700 Subject: [PATCH 25/34] Relay style option for users mutations --- src/GraphQL/loaders/usersMutations.js | 223 +++++++++++++++++--------- 1 file changed, 147 insertions(+), 76 deletions(-) diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 71c0c46670..4cbab52cda 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -4,98 +4,169 @@ import { GraphQLObjectType, GraphQLString, } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import UsersRouter from '../../Routers/UsersRouter'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsMutations from './objectsMutations'; const usersRouter = new UsersRouter(); -const load = parseGraphQLSchema => { - if (parseGraphQLSchema.isUsersClassDisabled) { - return; - } - const fields = {}; - - fields.signUp = { - description: 'The signUp mutation can be used to sign the user up.', - args: { - fields: { - descriptions: 'These are the fields of the user.', - type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType, - }, - }, - type: new GraphQLNonNull(defaultGraphQLTypes.SIGN_UP_RESULT), - async resolve(_source, args, context) { - try { - const { fields } = args; - const { config, auth, info } = context; - - return await objectsMutations.createObject( - '_User', - fields, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } +const loadSignUp = (parseGraphQLSchema, fields) => { + const description = 'The signUp mutation can be used to sign the user up.'; + const args = { + fields: { + descriptions: 'These are the fields of the user.', + type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType, }, }; + const type = new GraphQLNonNull(defaultGraphQLTypes.SIGN_UP_RESULT); + const resolve = async (_source, args, context) => { + try { + const { fields } = args; + const { config, auth, info } = context; - fields.logIn = { - description: 'The logIn mutation can be used to log the user in.', - args: { - username: { - description: 'This is the username used to log the user in.', - type: new GraphQLNonNull(GraphQLString), - }, - password: { - description: 'This is the password used to log the user in.', - type: new GraphQLNonNull(GraphQLString), + return await objectsMutations.createObject( + '_User', + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }; + + let signUpField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + signUpField = mutationWithClientMutationId({ + name: 'SignUp', + inputFields: args, + outputFields: { + result: { type }, }, + mutateAndGetPayload: async (args, context) => ({ + result: await resolve(undefined, args, context), + }), + }); + } else { + signUpField = { + description, + args, + type, + resolve, + }; + } + fields.signUp = signUpField; +}; + +const loadLogIn = (parseGraphQLSchema, fields) => { + const description = 'The logIn mutation can be used to log the user in.'; + const args = { + username: { + description: 'This is the username used to log the user in.', + type: new GraphQLNonNull(GraphQLString), }, - type: new GraphQLNonNull(parseGraphQLSchema.meType), - async resolve(_source, args, context) { - try { - const { username, password } = args; - const { config, auth, info } = context; - - return (await usersRouter.handleLogIn({ - body: { - username, - password, - }, - query: {}, - config, - auth, - info, - })).response; - } catch (e) { - parseGraphQLSchema.handleError(e); - } + password: { + description: 'This is the password used to log the user in.', + type: new GraphQLNonNull(GraphQLString), }, }; + const type = new GraphQLNonNull(parseGraphQLSchema.meType); + const resolve = async (_source, args, context) => { + try { + const { username, password } = args; + const { config, auth, info } = context; - fields.logOut = { - description: 'The logOut mutation can be used to log the user out.', - type: new GraphQLNonNull(GraphQLBoolean), - async resolve(_source, _args, context) { - try { - const { config, auth, info } = context; - - await usersRouter.handleLogOut({ - config, - auth, - info, - }); - return true; - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + return (await usersRouter.handleLogIn({ + body: { + username, + password, + }, + query: {}, + config, + auth, + info, + })).response; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }; + + let logInField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + logInField = mutationWithClientMutationId({ + name: 'LogIn', + inputFields: args, + outputFields: { + me: { type }, + }, + mutateAndGetPayload: async (args, context) => ({ + me: await resolve(undefined, args, context), + }), + }); + } else { + logInField = { + description, + args, + type, + resolve, + }; + } + fields.logIn = logInField; +}; + +const loadLogOut = (parseGraphQLSchema, fields) => { + const description = 'The logOut mutation can be used to log the user out.'; + const type = new GraphQLNonNull(GraphQLBoolean); + const resolve = async (_source, _args, context) => { + try { + const { config, auth, info } = context; + + await usersRouter.handleLogOut({ + config, + auth, + info, + }); + return true; + } catch (e) { + parseGraphQLSchema.handleError(e); + } }; + let logOutField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + logOutField = mutationWithClientMutationId({ + name: 'LogOut', + inputFields: {}, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (_args, context) => ({ + result: await resolve(undefined, undefined, context), + }), + }); + } else { + logOutField = { + description, + type, + resolve, + }; + } + fields.logOut = logOutField; +}; + +const load = parseGraphQLSchema => { + if (parseGraphQLSchema.isUsersClassDisabled) { + return; + } + + const fields = {}; + + loadSignUp(parseGraphQLSchema, fields); + loadLogIn(parseGraphQLSchema, fields); + loadLogOut(parseGraphQLSchema, fields); + const usersMutation = new GraphQLObjectType({ name: 'UsersMutation', description: 'UsersMutation is the top level type for files mutations.', From 7b15f88c1320ea9cfcb31380638dd7a01c4fb046 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 18:05:09 -0700 Subject: [PATCH 26/34] relay style tests for users mutations --- spec/ParseGraphQLServerRelay.spec.js | 117 +++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js index a1b614e140..cd64cd0cc2 100644 --- a/spec/ParseGraphQLServerRelay.spec.js +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -422,5 +422,122 @@ describe('ParseGraphQLServer - Relay Style', () => { clientMutationId ); }); + + it('should sign up with clientMutationId', async () => { + const clientMutationId = uuidv4(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation SignUp($input: SignUpInput!) { + users { + signUp(input: $input) { + result { + sessionToken + } + clientMutationId + } + } + } + `, + variables: { + input: { + fields: { + username: 'user1', + password: 'user1', + }, + clientMutationId, + }, + }, + }); + + expect(result.data.users.signUp.result.sessionToken).toBeDefined(); + expect(typeof result.data.users.signUp.result.sessionToken).toBe( + 'string' + ); + + expect(result.data.users.signUp.clientMutationId).toEqual( + clientMutationId + ); + }); + + it('should log in with clientMutationId', async () => { + const clientMutationId = uuidv4(); + + const user = new Parse.User(); + user.setUsername('user1'); + user.setPassword('user1'); + await user.signUp(); + await Parse.User.logOut(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation LogIn($input: LogInInput!) { + users { + logIn(input: $input) { + me { + sessionToken + } + clientMutationId + } + } + } + `, + variables: { + input: { + username: 'user1', + password: 'user1', + clientMutationId, + }, + }, + }); + + expect(result.data.users.logIn.me.sessionToken).toBeDefined(); + expect(typeof result.data.users.logIn.me.sessionToken).toBe('string'); + + expect(result.data.users.logIn.clientMutationId).toEqual( + clientMutationId + ); + }); + + it('should log out with clientMutationId', async () => { + const clientMutationId = uuidv4(); + + const user = new Parse.User(); + user.setUsername('user1'); + user.setPassword('user1'); + await user.signUp(); + const sessionToken = user.getSessionToken(); + + const logOut = await apolloClient.mutate({ + mutation: gql` + mutation LogOutUser($input: LogOutInput!) { + users { + logOut(input: $input) { + result + clientMutationId + } + } + } + `, + variables: { + input: { + clientMutationId, + }, + }, + context: { + headers: { + 'X-Parse-Session-Token': sessionToken, + }, + }, + }); + + expect(logOut.data.users.logOut.result).toBeTruthy(); + expect(logOut.data.users.logOut.clientMutationId).toEqual( + clientMutationId + ); + await expectAsync(Parse.User.me({ sessionToken })).toBeRejectedWith( + new Error('Invalid session token') + ); + }); }); }); From 48aa14ec6ae5b54003b4f295929830c777238293 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 18:10:45 -0700 Subject: [PATCH 27/34] Testing relay style schema for users mutations --- spec/ParseGraphQLServer.spec.js | 112 ++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 303a3bc793..1d943b6acc 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1062,6 +1062,118 @@ describe('ParseGraphQLServer', () => { 'result', ]); }); + + it('should have clientMutationId in sign up mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "SignUpInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual(['clientMutationId', 'fields']); + }); + + it('should have clientMutationId in sign up mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "SignUpPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['clientMutationId', 'result']); + }); + + it('should have clientMutationId in log in mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogInInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual([ + 'clientMutationId', + 'password', + 'username', + ]); + }); + + it('should have clientMutationId in log in mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogInPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['clientMutationId', 'me']); + }); + + it('should have clientMutationId in log out mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogOutInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual(['clientMutationId']); + }); + + it('should have clientMutationId in log out mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogOutPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['clientMutationId', 'result']); + }); }); describe('when relay style is disabled', () => { From fbf92e4e1388575417cee8ef7c7d233a5af38354 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 18:14:22 -0700 Subject: [PATCH 28/34] Testing non relay style schema for users mutations --- spec/ParseGraphQLServer.spec.js | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 1d943b6acc..f77ec60f91 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1384,6 +1384,102 @@ describe('ParseGraphQLServer', () => { expect(deleteObjectPayloadType).toBeNull(); }); + + it('should not have sign up input', async () => { + const inputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "SignUpInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(inputType).toBeNull(); + }); + + it('should not have sign up payload', async () => { + const payloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "SignUpPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(payloadType).toBeNull(); + }); + + it('should not have log in input', async () => { + const inputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogInInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(inputType).toBeNull(); + }); + + it('should not have log in payload', async () => { + const payloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogInPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(payloadType).toBeNull(); + }); + + it('should not have log out input', async () => { + const inputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogOutInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(inputType).toBeNull(); + }); + + it('should not have log out payload', async () => { + const payloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogOutPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(payloadType).toBeNull(); + }); }); }); From e45bfca007e59bdea055251c9826a8cd49132336 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 19:33:23 -0700 Subject: [PATCH 29/34] Relay version for custom create object mutation --- src/GraphQL/loaders/parseClassMutations.js | 67 +++++++++++++++------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index c1c75ba127..0ec654f27c 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,4 +1,5 @@ import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsMutations from './objectsMutations'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; @@ -73,31 +74,53 @@ const load = function( 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; + const description = `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${className} class.`; + const args = { + fields: createFields, + }; + const type = new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT); + const resolve = async (_source, args, context) => { + try { + const { fields } = args; + const { config, auth, info } = context; - transformTypes('create', 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); + } }; + + let createField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + createField = mutationWithClientMutationId({ + name: `Create${className}Object`, + inputFields: args, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (args, context) => ({ + result: await resolve(undefined, args, context), + }), + }); + } else { + createField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations[ + createGraphQLMutationName + ] = createField; } if (isUpdateEnabled) { From e2d0f95d4c38ce1529984650b6f104407fb96548 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 19:41:03 -0700 Subject: [PATCH 30/34] Pushing relay auto generated types for mutation --- src/GraphQL/loaders/functionsMutations.js | 2 ++ src/GraphQL/loaders/objectsMutations.js | 6 ++++++ src/GraphQL/loaders/parseClassMutations.js | 2 ++ src/GraphQL/loaders/usersMutations.js | 6 ++++++ 4 files changed, 16 insertions(+) diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index 5ed3d66f75..43aff98df4 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -50,6 +50,8 @@ const load = parseGraphQLSchema => { result: await resolve(undefined, args, context), }), }); + parseGraphQLSchema.graphQLTypes.push(callField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(callField.type); } else { callField = { description, diff --git a/src/GraphQL/loaders/objectsMutations.js b/src/GraphQL/loaders/objectsMutations.js index c3e890164c..a8e668da78 100644 --- a/src/GraphQL/loaders/objectsMutations.js +++ b/src/GraphQL/loaders/objectsMutations.js @@ -94,6 +94,8 @@ const loadCreate = parseGraphQLSchema => { result: await resolve(undefined, args, context), }), }); + parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(createField.type); } else { createField = { description, @@ -144,6 +146,8 @@ const loadUpdate = parseGraphQLSchema => { result: await resolve(undefined, args, context), }), }); + parseGraphQLSchema.graphQLTypes.push(updateField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(updateField.type); } else { updateField = { description, @@ -186,6 +190,8 @@ const loadDelete = parseGraphQLSchema => { result: await resolve(undefined, args, context), }), }); + parseGraphQLSchema.graphQLTypes.push(deleteField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(deleteField.type); } else { deleteField = { description, diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 0ec654f27c..ba640484f4 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -110,6 +110,8 @@ const load = function( result: await resolve(undefined, args, context), }), }); + parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(createField.type); } else { createField = { description, diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 4cbab52cda..688456adb2 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -49,6 +49,8 @@ const loadSignUp = (parseGraphQLSchema, fields) => { result: await resolve(undefined, args, context), }), }); + parseGraphQLSchema.graphQLTypes.push(signUpField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(signUpField.type); } else { signUpField = { description, @@ -105,6 +107,8 @@ const loadLogIn = (parseGraphQLSchema, fields) => { me: await resolve(undefined, args, context), }), }); + parseGraphQLSchema.graphQLTypes.push(logInField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(logInField.type); } else { logInField = { description, @@ -146,6 +150,8 @@ const loadLogOut = (parseGraphQLSchema, fields) => { result: await resolve(undefined, undefined, context), }), }); + parseGraphQLSchema.graphQLTypes.push(logOutField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(logOutField.type); } else { logOutField = { description, From 8d1ba340ef2fb8999fb83640da04259a05ebe154 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 8 Aug 2019 23:35:10 -0700 Subject: [PATCH 31/34] Parse class mutations on relay style --- src/GraphQL/loaders/parseClassMutations.js | 144 ++++++++++++++------- 1 file changed, 96 insertions(+), 48 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index ba640484f4..f48281366a 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -127,60 +127,108 @@ const load = function( 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('update', fields); - - return await objectsMutations.updateObject( - className, - objectId, - fields, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + const description = `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${className} class.`; + const args = { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, + fields: updateFields, + }; + const type = defaultGraphQLTypes.UPDATE_RESULT; + const resolve = async (_source, args, context) => { + try { + const { objectId, fields } = args; + const { config, auth, info } = context; + + transformTypes('update', fields); + + return await objectsMutations.updateObject( + className, + objectId, + fields, + config, + auth, + info + ); + } catch (e) { + parseGraphQLSchema.handleError(e); + } }; + + let updateField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + updateField = mutationWithClientMutationId({ + name: `Update${className}Object`, + inputFields: args, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (args, context) => ({ + result: await resolve(undefined, args, context), + }), + }); + parseGraphQLSchema.graphQLTypes.push(updateField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(updateField.type); + } else { + updateField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations[ + updateGraphQLMutationName + ] = updateField; } 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); - } - }, + const description = `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`; + const args = { + objectId: defaultGraphQLTypes.OBJECT_ID_ATT, }; + const type = new GraphQLNonNull(GraphQLBoolean); + const resolve = async (_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); + } + }; + + let deleteField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + deleteField = mutationWithClientMutationId({ + name: `Delete${className}Object`, + inputFields: args, + outputFields: { + result: { type }, + }, + mutateAndGetPayload: async (args, context) => ({ + result: await resolve(undefined, args, context), + }), + }); + parseGraphQLSchema.graphQLTypes.push(deleteField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(deleteField.type); + } else { + deleteField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations[ + deleteGraphQLMutationName + ] = deleteField; } }; From 1ec7d2fa6985f30d9c462959446004e4d25b3e53 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 9 Aug 2019 00:34:46 -0700 Subject: [PATCH 32/34] Tests for relay spec on parse class mutations --- spec/ParseGraphQLServerRelay.spec.js | 137 ++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js index cd64cd0cc2..2bd01be375 100644 --- a/spec/ParseGraphQLServerRelay.spec.js +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -19,6 +19,7 @@ describe('ParseGraphQLServer - Relay Style', () => { let parseServer; let httpServer; let parseLiveQueryServer; + let parseGraphQLServer; const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-Javascript-Key': 'test', @@ -28,7 +29,7 @@ describe('ParseGraphQLServer - Relay Style', () => { beforeAll(async () => { parseServer = await global.reconfigureServer({}); - const parseGraphQLServer = new ParseGraphQLServer(parseServer, { + parseGraphQLServer = new ParseGraphQLServer(parseServer, { graphQLPath: '/graphql', playgroundPath: '/playground', subscriptionsPath: '/subscriptions', @@ -340,6 +341,55 @@ describe('ParseGraphQLServer - Relay Style', () => { ); }); + it('should create object with clientMutationId in the custom mutation', async () => { + const clientMutationId = uuidv4(); + + const firstobj = new Parse.Object('SomeClass'); + firstobj.set('someField', 'some value'); + await firstobj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation CreateSomeClassObject($input: CreateSomeClassObjectInput!) { + objects { + createSomeClass(input: $input) { + result { + objectId + createdAt + } + clientMutationId + } + } + } + `, + variables: { + input: { + fields: { + someField: 'some other value', + }, + clientMutationId, + }, + }, + }); + + expect(result.data.objects.createSomeClass.result.objectId).toBeDefined(); + + const obj = await new Parse.Query('SomeClass').get( + result.data.objects.createSomeClass.result.objectId + ); + + expect(obj.createdAt).toEqual( + new Date(result.data.objects.createSomeClass.result.createdAt) + ); + expect(obj.get('someField')).toEqual('some other value'); + + expect(result.data.objects.createSomeClass.clientMutationId).toEqual( + clientMutationId + ); + }); + it('should update object with clientMutationId in the generic mutation', async () => { const clientMutationId = uuidv4(); @@ -386,6 +436,53 @@ describe('ParseGraphQLServer - Relay Style', () => { ); }); + it('should update object with clientMutationId in the custom mutation', async () => { + const clientMutationId = uuidv4(); + + const obj = new Parse.Object('SomeClass'); + obj.set('someField1', 'some field 1 value 1'); + obj.set('someField2', 'some field 2 value 1'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation UpdateSomeClassObject($input: UpdateSomeClassObjectInput!) { + objects { + updateSomeClass(input: $input) { + result { + updatedAt + } + clientMutationId + } + } + } + `, + variables: { + input: { + objectId: obj.id, + fields: { + someField1: 'some field 1 value 2', + }, + clientMutationId, + }, + }, + }); + + await obj.fetch(); + + expect(obj.updatedAt).toEqual( + new Date(result.data.objects.updateSomeClass.result.updatedAt) + ); + expect(obj.get('someField1')).toEqual('some field 1 value 2'); + expect(obj.get('someField2')).toEqual('some field 2 value 1'); + + expect(result.data.objects.updateSomeClass.clientMutationId).toEqual( + clientMutationId + ); + }); + it('should delete object with clientMutationId in the generic mutation', async () => { const clientMutationId = uuidv4(); @@ -423,6 +520,44 @@ describe('ParseGraphQLServer - Relay Style', () => { ); }); + it('should delete object with clientMutationId in the custom mutation', async () => { + const clientMutationId = uuidv4(); + + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = await apolloClient.mutate({ + mutation: gql` + mutation DeleteSomeClassObject($input: DeleteSomeClassObjectInput!) { + objects { + deleteSomeClass(input: $input) { + result + clientMutationId + } + } + } + `, + variables: { + input: { + objectId: obj.id, + clientMutationId, + }, + }, + }); + + expect(result.data.objects.deleteSomeClass.result).toEqual(true); + + await expectAsync(obj.fetch({ useMasterKey: true })).toBeRejectedWith( + jasmine.stringMatching('Object not found') + ); + + expect(result.data.objects.deleteSomeClass.clientMutationId).toEqual( + clientMutationId + ); + }); + it('should sign up with clientMutationId', async () => { const clientMutationId = uuidv4(); From e7a26db2d9be20876809b7c855e2ac19c6a6f084 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 9 Aug 2019 00:50:30 -0700 Subject: [PATCH 33/34] Relay style schema for parse class mutations --- spec/ParseGraphQLServer.spec.js | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index f77ec60f91..cb3fce328c 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -976,6 +976,58 @@ describe('ParseGraphQLServer', () => { ]); }); + it('should have clientMutationId in custom create object mutation input', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateSomeClassObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(createObjectInputFields).toEqual([ + 'clientMutationId', + 'fields', + ]); + }); + + it('should have clientMutationId in custom create object mutation payload', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateSomeClassObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(createObjectPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); + it('should have clientMutationId in generic update object mutation input', async () => { const updateObjectInputFields = (await apolloClient.query({ query: gql` @@ -1020,6 +1072,59 @@ describe('ParseGraphQLServer', () => { ]); }); + it('should have clientMutationId in custom update object mutation input', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const updateObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateSomeClassObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(updateObjectInputFields).toEqual([ + 'clientMutationId', + 'fields', + 'objectId', + ]); + }); + + it('should have clientMutationId in custom update object mutation payload', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const updateObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateSomeClassObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(updateObjectPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); + it('should have clientMutationId in generic delete object mutation input', async () => { const deleteObjectInputFields = (await apolloClient.query({ query: gql` @@ -1063,6 +1168,58 @@ describe('ParseGraphQLServer', () => { ]); }); + it('should have clientMutationId in custom delete object mutation input', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const deleteObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteSomeClassObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(deleteObjectInputFields).toEqual([ + 'clientMutationId', + 'objectId', + ]); + }); + + it('should have clientMutationId in custom delete object mutation payload', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const deleteObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteSomeClassObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(deleteObjectPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); + it('should have clientMutationId in sign up mutation input', async () => { const inputFields = (await apolloClient.query({ query: gql` From 6e9e9f3a012505dd9bb38fc24bd9d025765fc914 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Fri, 9 Aug 2019 00:59:50 -0700 Subject: [PATCH 34/34] Test non relay style schema for parse class mutation --- spec/ParseGraphQLServer.spec.js | 138 ++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 6 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index cb3fce328c..0862e20544 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1446,7 +1446,7 @@ describe('ParseGraphQLServer', () => { expect(callFunctionPayloadType).toBeNull(); }); - it('should not have create object input', async () => { + it('should not have create object input for generic mutation', async () => { const createObjectInputType = (await apolloClient.query({ query: gql` query { @@ -1462,7 +1462,7 @@ describe('ParseGraphQLServer', () => { expect(createObjectInputType).toBeNull(); }); - it('should not have create object payload', async () => { + it('should not have create object payload for generic mutation', async () => { const createObjectPayloadType = (await apolloClient.query({ query: gql` query { @@ -1478,7 +1478,49 @@ describe('ParseGraphQLServer', () => { expect(createObjectPayloadType).toBeNull(); }); - it('should not have update object input', async () => { + it('should not have create object input for custom mutation', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateSomeClassObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(createObjectInputType).toBeNull(); + }); + + it('should not have create object payload for custom mutation', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateSomeClassObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(createObjectPayloadType).toBeNull(); + }); + + it('should not have update object input for generic mutation', async () => { const updateObjectInputType = (await apolloClient.query({ query: gql` query { @@ -1494,7 +1536,7 @@ describe('ParseGraphQLServer', () => { expect(updateObjectInputType).toBeNull(); }); - it('should not have update object payload', async () => { + it('should not have update object payload for generic mutation', async () => { const updateObjectPayloadType = (await apolloClient.query({ query: gql` query { @@ -1510,7 +1552,49 @@ describe('ParseGraphQLServer', () => { expect(updateObjectPayloadType).toBeNull(); }); - it('should not have delete object input', async () => { + it('should not have update object input for custom mutation', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const updateObjectInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateSomeClassObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(updateObjectInputType).toBeNull(); + }); + + it('should not have update object payload for custom mutation', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const updateObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateSomeClassObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(updateObjectPayloadType).toBeNull(); + }); + + it('should not have delete object input for generic mutation', async () => { const deleteObjectInputType = (await apolloClient.query({ query: gql` query { @@ -1526,7 +1610,7 @@ describe('ParseGraphQLServer', () => { expect(deleteObjectInputType).toBeNull(); }); - it('should not have delete object payload', async () => { + it('should not have delete object payload for generic mutation', async () => { const deleteObjectPayloadType = (await apolloClient.query({ query: gql` query { @@ -1542,6 +1626,48 @@ describe('ParseGraphQLServer', () => { expect(deleteObjectPayloadType).toBeNull(); }); + it('should not have delete object input for custom mutation', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const deleteObjectInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteSomeClassObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(deleteObjectInputType).toBeNull(); + }); + + it('should not have delete object payload for custom mutation', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const deleteObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteSomeClassObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(deleteObjectPayloadType).toBeNull(); + }); + it('should not have sign up input', async () => { const inputType = (await apolloClient.query({ query: gql`