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/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 5d62b658fc..0862e20544 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,982 @@ 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', () => { + describe('when relay style is enabled', () => { + beforeAll(async () => { + parseGraphQLServer.setRelayStyle(true); + await resetGraphQLCache(); + }); + + afterAll(async () => { + parseGraphQLServer.setRelayStyle(false); + await resetGraphQLCache(); + }); + + 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); + + expect(schemaTypes).toContain('Node'); + }); + + 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); + + expect(queryFields).toContain('node'); + }); + + it('should 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); + + expect(userFields).toContain('id'); + }); + + it('should have clientMutationId in create file input', async () => { + 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 have clientMutationId in create file payload', async () => { + 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', + ]); + }); + + it('should have clientMutationId in call function input', async () => { + 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', + ]); + }); + + 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(); + + expect(callFunctionPayloadFields).toEqual([ + 'clientMutationId', + '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 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` + 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 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` + 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', + ]); + }); + + 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` + 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', () => { + 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); + + expect(schemaTypes).not.toContain('Node'); + }); + + 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); + + expect(queryFields).not.toContain('node'); + }); + + 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); + + expect(userFields).not.toContain('id'); + }); + + it('should not have create file input', async () => { + const createFileInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFileInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(createFileInputType).toBeNull(); + }); + + it('should not have create file payload', async () => { + const createFilePayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFilePayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(createFilePayloadType).toBeNull(); + }); + + it('should not have call function input', async () => { + const callFunctionInputType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(callFunctionInputType).toBeNull(); + }); + + it('should not have call function payload', async () => { + const callFunctionPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallFunctionPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(callFunctionPayloadType).toBeNull(); + }); + + it('should not have create object input for generic mutation', 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 for generic mutation', async () => { + const createObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(createObjectPayloadType).toBeNull(); + }); + + 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 { + __type(name: "UpdateObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(updateObjectInputType).toBeNull(); + }); + + it('should not have update object payload for generic mutation', async () => { + const updateObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + expect(updateObjectPayloadType).toBeNull(); + }); + + 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 { + __type(name: "DeleteObjectInput") { + inputFields { + name + } + } + } + `, + })).data['__type']; + + expect(deleteObjectInputType).toBeNull(); + }); + + it('should not have delete object payload for generic mutation', async () => { + const deleteObjectPayloadType = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteObjectPayload") { + fields { + name + } + } + } + `, + })).data['__type']; + + 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` + 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(); + }); + }); + }); + + describe('Configuration', function() { beforeEach(async () => { await parseGraphQLServer.setGraphQLConfig({}); await resetGraphQLCache(); diff --git a/spec/ParseGraphQLServerRelay.spec.js b/spec/ParseGraphQLServerRelay.spec.js new file mode 100644 index 0000000000..2bd01be375 --- /dev/null +++ b/spec/ParseGraphQLServerRelay.spec.js @@ -0,0 +1,678 @@ +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'); +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', () => { + let parseServer; + let httpServer; + let parseLiveQueryServer; + let parseGraphQLServer; + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }; + + let apolloClient; + + beforeAll(async () => { + parseServer = await global.reconfigureServer({}); + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + playgroundPath: '/playground', + subscriptionsPath: '/subscriptions', + relayStyle: true, + }); + const expressApp = express(); + httpServer = http.createServer(expressApp); + expressApp.use('/parse', parseServer.app); + parseLiveQueryServer = 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', + }, + }, + }); + }); + + 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'); + 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'); + }); + + 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'); + }); + }); + + 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'); + }); + + 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 + ); + }); + + 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 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(); + + 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 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(); + + 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 + ); + }); + + 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(); + + 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') + ); + }); + }); +}); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 261045fe81..16f6d83774 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'; @@ -14,18 +19,32 @@ 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; 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 +56,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() { @@ -63,6 +83,7 @@ class ParseGraphQLSchema { this.meType = null; this.graphQLAutoSchema = null; this.graphQLSchema = null; + this.graphQLSchemaIsRelayStyle = this.relayStyle; this.graphQLTypes = []; this.graphQLObjectsQueries = {}; this.graphQLQueries = {}; @@ -71,9 +92,14 @@ class ParseGraphQLSchema { this.graphQLSubscriptions = {}; this.graphQLSchemaDirectivesDefinitions = null; this.graphQLSchemaDirectives = {}; + this.relayNodeInterface = null; defaultGraphQLTypes.load(this); + if (this.graphQLSchemaIsRelayStyle) { + defaultRelaySchema.load(this); + } + this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach( ([parseClass, parseClassConfig]) => { parseClassTypes.load(this, parseClass, parseClassConfig); @@ -263,9 +289,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; } diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 59c5fe61e1..524209cca8 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,10 +85,11 @@ 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-Client-Key': this.parseServer.config.clientKey, 'X-Parse-Master-Key': this.parseServer.config.masterKey, }, }) @@ -112,7 +114,7 @@ class ParseGraphQLServer { { server, path: - this.config.subscriptionsPath || + this._config.subscriptionsPath || requiredParameter( 'You must provide a config.subscriptionsPath to createSubscriptions!' ), @@ -123,6 +125,11 @@ class ParseGraphQLServer { setGraphQLConfig(graphQLConfig: ParseGraphQLConfig): Promise { return this.parseGraphQLController.updateGraphQLConfig(graphQLConfig); } + + setRelayStyle(relayStyle) { + this._config.relayStyle = relayStyle; + this.parseGraphQLSchema.relayStyle = relayStyle; + } } export { ParseGraphQLServer }; diff --git a/src/GraphQL/loaders/defaultRelaySchema.js b/src/GraphQL/loaders/defaultRelaySchema.js new file mode 100644 index 0000000000..ebcb4b45a2 --- /dev/null +++ b/src/GraphQL/loaders/defaultRelaySchema.js @@ -0,0 +1,47 @@ +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.parseClassTypes[obj.className] + .classGraphQLOutputType; + } + ); + + parseGraphQLSchema.relayNodeInterface = nodeInterface; + parseGraphQLSchema.graphQLTypes.push(nodeInterface); + parseGraphQLSchema.graphQLQueries.node = nodeField; +}; + +export { load }; diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 68ad4e8628..703b673833 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,97 @@ 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: async (args, context) => ({ + fileInfo: await resolve(undefined, args, context), + }), + }); + 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.', diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index 6a7b9a3de9..43aff98df4 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -1,43 +1,66 @@ 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 (args, context) => ({ + result: await resolve(undefined, args, context), + }), + }); + parseGraphQLSchema.graphQLTypes.push(callField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(callField.type); + } else { + callField = { + description, + args, + type, + resolve, + }; + } + fields.call = callField; const functionsMutation = new GraphQLObjectType({ name: 'FunctionsMutation', diff --git a/src/GraphQL/loaders/objectsMutations.js b/src/GraphQL/loaders/objectsMutations.js index 7623b6eeca..a8e668da78 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,74 +63,150 @@ 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); + } }; - 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; - - return await updateObject( - className, - objectId, - 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: await resolve(undefined, args, context), + }), + }); + parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(createField.type); + } else { + createField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations.create = createField; +}; + +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); + } }; - 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); - } - }, + let updateField; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + updateField = mutationWithClientMutationId({ + name: 'UpdateObject', + 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.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), + }), + }); + parseGraphQLSchema.graphQLTypes.push(deleteField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(deleteField.type); + } else { + deleteField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations.delete = deleteField; +}; + +const load = parseGraphQLSchema => { + loadCreate(parseGraphQLSchema); + loadUpdate(parseGraphQLSchema); + loadDelete(parseGraphQLSchema); const objectsMutation = new GraphQLObjectType({ name: 'ObjectsMutation', diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index c1c75ba127..f48281366a 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,89 +74,161 @@ 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; - - transformTypes('create', fields); - - return await objectsMutations.createObject( - className, - fields, - config, - auth, - info - ); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + 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); + + 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), + }), + }); + parseGraphQLSchema.graphQLTypes.push(createField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(createField.type); + } else { + createField = { + description, + args, + type, + resolve, + }; + } + parseGraphQLSchema.graphQLObjectsMutations[ + createGraphQLMutationName + ] = createField; } 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; } }; diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index d32adc845b..4a0b6ceaa3 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'; @@ -534,7 +535,18 @@ const load = ( }; const classGraphQLOutputTypeName = `${className}Class`; + const interfaces = [defaultGraphQLTypes.CLASS]; + if (parseGraphQLSchema.graphQLSchemaIsRelayStyle) { + interfaces.push(parseGraphQLSchema.relayNodeInterface); + } const outputFields = () => { + 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, @@ -637,12 +649,12 @@ const load = ( } else { return fields; } - }, defaultGraphQLTypes.CLASS_FIELDS); + }, classFields); }; 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); @@ -679,7 +691,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, diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 71c0c46670..688456adb2 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -4,98 +4,175 @@ 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), + }), + }); + parseGraphQLSchema.graphQLTypes.push(signUpField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(signUpField.type); + } 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), + }), + }); + parseGraphQLSchema.graphQLTypes.push(logInField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(logInField.type); + } 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), + }), + }); + parseGraphQLSchema.graphQLTypes.push(logOutField.args.input.type); + parseGraphQLSchema.graphQLTypes.push(logOutField.type); + } 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.', 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) {