diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index cc44aaa8f5..e9cbc64dc2 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -17,6 +17,14 @@ const { SubscriptionClient } = require('subscriptions-transport-ws'); const { WebSocketLink } = require('apollo-link-ws'); const ApolloClient = require('apollo-client').default; const gql = require('graphql-tag'); +const { + GraphQLObjectType, + GraphQLString, + GraphQLNonNull, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLSchema, +} = require('graphql'); const { ParseServer } = require('../'); const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); const ReadPreference = require('mongodb').ReadPreference; @@ -10594,130 +10602,296 @@ describe('ParseGraphQLServer', () => { }); describe('Custom API', () => { - let httpServer; - const headers = { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test', - }; - let apolloClient; - - beforeAll(async () => { - const expressApp = express(); - httpServer = http.createServer(expressApp); - parseGraphQLServer = new ParseGraphQLServer(parseServer, { - graphQLPath: '/graphql', - graphQLCustomTypeDefs: gql` - extend type Query { - hello: String @resolve - hello2: String @resolve(to: "hello") - userEcho(user: CreateUserFieldsInput!): User! @resolve - hello3: String! @mock(with: "Hello world!") - hello4: User! @mock(with: { username: "somefolk" }) - } - `, - }); - parseGraphQLServer.applyGraphQL(expressApp); - await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); - const httpLink = createUploadLink({ - uri: 'http://localhost:13377/graphql', - fetch, - headers, - }); - apolloClient = new ApolloClient({ - link: httpLink, - cache: new InMemoryCache(), - defaultOptions: { - query: { - fetchPolicy: 'no-cache', + describe('GraphQL Schema Based', () => { + let httpServer; + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }; + let apolloClient; + beforeAll(async () => { + const expressApp = express(); + httpServer = http.createServer(expressApp); + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + graphQLCustomTypeDefs: gql` + extend type Query { + hello: String @resolve + hello2: String @resolve(to: "hello") + userEcho(user: CreateUserFieldsInput!): User! @resolve + hello3: String! @mock(with: "Hello world!") + hello4: User! @mock(with: { username: "somefolk" }) + } + `, + }); + parseGraphQLServer.applyGraphQL(expressApp); + await new Promise(resolve => + httpServer.listen({ port: 13377 }, resolve) + ); + const httpLink = createUploadLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers, + }); + apolloClient = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + }, }, - }, + }); }); - }); - afterAll(async () => { - await httpServer.close(); - }); - - it('can resolve a custom query using default function name', async () => { - Parse.Cloud.define('hello', async () => { - return 'Hello world!'; + afterAll(async () => { + await httpServer.close(); }); - const result = await apolloClient.query({ - query: gql` - query Hello { - hello - } - `, - }); + it('can resolve a custom query using default function name', async () => { + Parse.Cloud.define('hello', async () => { + return 'Hello world!'; + }); - expect(result.data.hello).toEqual('Hello world!'); - }); + const result = await apolloClient.query({ + query: gql` + query Hello { + hello + } + `, + }); - it('can resolve a custom query using function name set by "to" argument', async () => { - Parse.Cloud.define('hello', async () => { - return 'Hello world!'; + expect(result.data.hello).toEqual('Hello world!'); }); - const result = await apolloClient.query({ - query: gql` - query Hello { - hello2 - } - `, - }); + it('can resolve a custom query using function name set by "to" argument', async () => { + Parse.Cloud.define('hello', async () => { + return 'Hello world!'; + }); + + const result = await apolloClient.query({ + query: gql` + query Hello { + hello2 + } + `, + }); - expect(result.data.hello2).toEqual('Hello world!'); + expect(result.data.hello2).toEqual('Hello world!'); + }); }); - it('should resolve auto types', async () => { - Parse.Cloud.define('userEcho', async req => { - return req.params.user; + describe('SDL Based', () => { + let httpServer; + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }; + let apolloClient; + + beforeAll(async () => { + const expressApp = express(); + httpServer = http.createServer(expressApp); + const TypeEnum = new GraphQLEnumType({ + name: 'TypeEnum', + values: { + human: { value: 'human' }, + robot: { value: 'robot' }, + }, + }); + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + graphQLCustomTypeDefs: new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + customQuery: { + type: new GraphQLNonNull(GraphQLString), + args: { + message: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: (p, { message }) => message, + }, + }, + }), + types: [ + new GraphQLInputObjectType({ + name: 'CreateSomeClassFieldsInput', + fields: { + type: { type: TypeEnum }, + }, + }), + new GraphQLInputObjectType({ + name: 'UpdateSomeClassFieldsInput', + fields: { + type: { type: TypeEnum }, + }, + }), + new GraphQLObjectType({ + name: 'SomeClass', + fields: { + nameUpperCase: { + type: new GraphQLNonNull(GraphQLString), + resolve: p => p.name.toUpperCase(), + }, + type: { type: TypeEnum }, + language: { + type: new GraphQLEnumType({ + name: 'LanguageEnum', + values: { + fr: { value: 'fr' }, + en: { value: 'en' }, + }, + }), + resolve: () => 'fr', + }, + }, + }), + ], + }), + }); + + parseGraphQLServer.applyGraphQL(expressApp); + await new Promise(resolve => + httpServer.listen({ port: 13377 }, resolve) + ); + const httpLink = createUploadLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers, + }); + apolloClient = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + }, + }, + }); + }); + + afterAll(async () => { + await httpServer.close(); }); - const result = await apolloClient.query({ - query: gql` - query UserEcho($user: CreateUserFieldsInput!) { - userEcho(user: $user) { - username + it('can resolve a custom query', async () => { + const result = await apolloClient.query({ + variables: { message: 'hello' }, + query: gql` + query CustomQuery($message: String!) { + customQuery(message: $message) } - } - `, - variables: { - user: { - username: 'somefolk', - password: 'somepassword', - }, - }, + `, + }); + expect(result.data.customQuery).toEqual('hello'); }); - expect(result.data.userEcho.username).toEqual('somefolk'); + it('can resolve a custom extend type', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save({ name: 'aname', type: 'robot' }); + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + const result = await apolloClient.query({ + variables: { id: obj.id }, + query: gql` + query someClass($id: ID!) { + someClass(id: $id) { + nameUpperCase + language + type + } + } + `, + }); + expect(result.data.someClass.nameUpperCase).toEqual('ANAME'); + expect(result.data.someClass.language).toEqual('fr'); + expect(result.data.someClass.type).toEqual('robot'); + + const result2 = await apolloClient.query({ + variables: { id: obj.id }, + query: gql` + query someClass($id: ID!) { + someClass(id: $id) { + name + language + } + } + `, + }); + expect(result2.data.someClass.name).toEqual('aname'); + expect(result.data.someClass.language).toEqual('fr'); + const result3 = await apolloClient.mutate({ + variables: { id: obj.id, name: 'anewname' }, + mutation: gql` + mutation someClass($id: ID!, $name: String!) { + updateSomeClass( + input: { id: $id, fields: { name: $name, type: human } } + ) { + someClass { + nameUpperCase + type + } + } + } + `, + }); + expect(result3.data.updateSomeClass.someClass.nameUpperCase).toEqual( + 'ANEWNAME' + ); + expect(result3.data.updateSomeClass.someClass.type).toEqual('human'); + }); }); + describe('Async Function Based Merge', () => { + let httpServer; + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }; + let apolloClient; - it('can mock a custom query with string', async () => { - const result = await apolloClient.query({ - query: gql` - query Hello { - hello3 - } - `, + beforeAll(async () => { + const expressApp = express(); + httpServer = http.createServer(expressApp); + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + graphQLCustomTypeDefs: ({ autoSchema, mergeSchemas }) => + mergeSchemas({ schemas: [autoSchema] }), + }); + + parseGraphQLServer.applyGraphQL(expressApp); + await new Promise(resolve => + httpServer.listen({ port: 13377 }, resolve) + ); + const httpLink = createUploadLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers, + }); + apolloClient = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + }, + }, + }); }); - expect(result.data.hello3).toEqual('Hello world!'); - }); + afterAll(async () => { + await httpServer.close(); + }); - it('can mock a custom query with auto type', async () => { - const result = await apolloClient.query({ - query: gql` - query Hello { - hello4 { - username + it('can resolve a query', async () => { + const result = await apolloClient.query({ + query: gql` + query Health { + health } - } - `, + `, + }); + expect(result.data.health).toEqual(true); }); - - expect(result.data.hello4.username).toEqual('somefolk'); }); }); }); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index d01f5eee0a..d596eae5b7 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -197,19 +197,60 @@ class ParseGraphQLSchema { if (this.graphQLCustomTypeDefs) { schemaDirectives.load(this); - this.graphQLSchema = mergeSchemas({ - schemas: [ - this.graphQLSchemaDirectivesDefinitions, - this.graphQLAutoSchema, - this.graphQLCustomTypeDefs, - ], - mergeDirectives: true, - }); + if (typeof this.graphQLCustomTypeDefs.getTypeMap === 'function') { + const customGraphQLSchemaTypeMap = this.graphQLCustomTypeDefs.getTypeMap(); + Object.values(customGraphQLSchemaTypeMap).forEach( + customGraphQLSchemaType => { + if ( + !customGraphQLSchemaType || + !customGraphQLSchemaType.name || + customGraphQLSchemaType.name.startsWith('__') + ) { + return; + } + const autoGraphQLSchemaType = this.graphQLAutoSchema.getType( + customGraphQLSchemaType.name + ); + if (autoGraphQLSchemaType) { + autoGraphQLSchemaType._fields = { + ...autoGraphQLSchemaType._fields, + ...customGraphQLSchemaType._fields, + }; + } + } + ); + this.graphQLSchema = mergeSchemas({ + schemas: [ + this.graphQLSchemaDirectivesDefinitions, + this.graphQLCustomTypeDefs, + this.graphQLAutoSchema, + ], + mergeDirectives: true, + }); + } else if (typeof this.graphQLCustomTypeDefs === 'function') { + this.graphQLSchema = await this.graphQLCustomTypeDefs({ + directivesDefinitionsSchema: this.graphQLSchemaDirectivesDefinitions, + autoSchema: this.graphQLAutoSchema, + mergeSchemas, + }); + } else { + this.graphQLSchema = mergeSchemas({ + schemas: [ + this.graphQLSchemaDirectivesDefinitions, + this.graphQLAutoSchema, + this.graphQLCustomTypeDefs, + ], + mergeDirectives: true, + }); + } const graphQLSchemaTypeMap = this.graphQLSchema.getTypeMap(); Object.keys(graphQLSchemaTypeMap).forEach(graphQLSchemaTypeName => { const graphQLSchemaType = graphQLSchemaTypeMap[graphQLSchemaTypeName]; - if (typeof graphQLSchemaType.getFields === 'function') { + if ( + typeof graphQLSchemaType.getFields === 'function' && + this.graphQLCustomTypeDefs.definitions + ) { const graphQLCustomTypeDef = this.graphQLCustomTypeDefs.definitions.find( definition => definition.name.value === graphQLSchemaTypeName ); diff --git a/src/GraphQL/helpers/objectsQueries.js b/src/GraphQL/helpers/objectsQueries.js index 3e918c98b6..c1237deaea 100644 --- a/src/GraphQL/helpers/objectsQueries.js +++ b/src/GraphQL/helpers/objectsQueries.js @@ -3,6 +3,11 @@ import { offsetToCursor, cursorToOffset } from 'graphql-relay'; import rest from '../../rest'; import { transformQueryInputToParse } from '../transformers/query'; +const needToGetAllKeys = (fields, keys) => + keys + ? !!keys.split(',').find(keyName => !fields[keyName.split('.')[0]]) + : true; + const getObject = async ( className, objectId, @@ -12,10 +17,11 @@ const getObject = async ( includeReadPreference, config, auth, - info + info, + parseClass ) => { const options = {}; - if (keys) { + if (!needToGetAllKeys(parseClass.fields, keys)) { options.keys = keys; } if (include) { @@ -133,7 +139,14 @@ const findObjects = async ( // Silently replace the limit on the query with the max configured options.limit = config.maxLimit; } - if (keys) { + if ( + !needToGetAllKeys( + parseClasses.find( + ({ className: parseClassName }) => className === parseClassName + ).fields, + keys + ) + ) { options.keys = keys; } if (includeAll === true) { @@ -313,4 +326,4 @@ const calculateSkipAndLimit = ( }; }; -export { getObject, findObjects, calculateSkipAndLimit }; +export { getObject, findObjects, calculateSkipAndLimit, needToGetAllKeys }; diff --git a/src/GraphQL/loaders/defaultRelaySchema.js b/src/GraphQL/loaders/defaultRelaySchema.js index 3837bd5b9f..c3c2d9ccce 100644 --- a/src/GraphQL/loaders/defaultRelaySchema.js +++ b/src/GraphQL/loaders/defaultRelaySchema.js @@ -30,7 +30,10 @@ const load = parseGraphQLSchema => { undefined, config, auth, - info + info, + parseGraphQLSchema.parseClasses.find( + ({ className }) => type === className + ) )), }; } catch (e) { diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 3ca333a0db..f41cccf5ed 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -112,8 +112,12 @@ const load = function( include, ['id', 'objectId', 'createdAt', 'updatedAt'] ); + const needToGetAllKeys = objectsQueries.needToGetAllKeys( + parseClass.fields, + keys + ); let optimizedObject = {}; - if (needGet) { + if (needGet && !needToGetAllKeys) { optimizedObject = await objectsQueries.getObject( className, createdObject.objectId, @@ -123,7 +127,21 @@ const load = function( undefined, config, auth, - info + info, + parseClass + ); + } else if (needToGetAllKeys) { + optimizedObject = await objectsQueries.getObject( + className, + createdObject.objectId, + undefined, + include, + undefined, + undefined, + config, + auth, + info, + parseClass ); } return { @@ -212,9 +230,12 @@ const load = function( include, ['id', 'objectId', 'updatedAt'] ); - + const needToGetAllKeys = objectsQueries.needToGetAllKeys( + parseClass.fields, + keys + ); let optimizedObject = {}; - if (needGet) { + if (needGet && !needToGetAllKeys) { optimizedObject = await objectsQueries.getObject( className, id, @@ -224,7 +245,21 @@ const load = function( undefined, config, auth, - info + info, + parseClass + ); + } else if (needToGetAllKeys) { + optimizedObject = await objectsQueries.getObject( + className, + id, + undefined, + include, + undefined, + undefined, + config, + auth, + info, + parseClass ); } return { @@ -301,7 +336,8 @@ const load = function( undefined, config, auth, - info + info, + parseClass ); } await objectsMutations.deleteObject( diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 80667836d0..cd1e71d922 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -14,7 +14,7 @@ const getParseClassQueryConfig = function( return (parseClassConfig && parseClassConfig.query) || {}; }; -const getQuery = async (className, _source, args, context, queryInfo) => { +const getQuery = async (parseClass, _source, args, context, queryInfo) => { let { id } = args; const { options } = args; const { readPreference, includeReadPreference } = options || {}; @@ -23,14 +23,14 @@ const getQuery = async (className, _source, args, context, queryInfo) => { const globalIdObject = fromGlobalId(id); - if (globalIdObject.type === className) { + if (globalIdObject.type === parseClass.className) { id = globalIdObject.id; } const { keys, include } = extractKeysAndInclude(selectedFields); return await objectsQueries.getObject( - className, + parseClass.className, id, keys, include, @@ -38,7 +38,8 @@ const getQuery = async (className, _source, args, context, queryInfo) => { includeReadPreference, config, auth, - info + info, + parseClass ); }; @@ -79,7 +80,7 @@ const load = function( ), async resolve(_source, args, context, queryInfo) { try { - return await getQuery(className, _source, args, context, queryInfo); + return await getQuery(parseClass, _source, args, context, queryInfo); } catch (e) { parseGraphQLSchema.handleError(e); } diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 4909c3e992..b0272c4de6 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -436,7 +436,7 @@ const load = ( ); const parseOrder = order && order.join(','); - return await objectsQueries.findObjects( + return objectsQueries.findObjects( source[field].className, { $relatedTo: { diff --git a/src/ParseServer.js b/src/ParseServer.js index 5611c50491..1fd279b4c7 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -262,10 +262,12 @@ class ParseServer { if (options.mountGraphQL === true || options.mountPlayground === true) { let graphQLCustomTypeDefs = undefined; - if (options.graphQLSchema) { + if (typeof options.graphQLSchema === 'string') { graphQLCustomTypeDefs = parse( fs.readFileSync(options.graphQLSchema, 'utf8') ); + } else if (typeof options.graphQLSchema === 'object') { + graphQLCustomTypeDefs = options.graphQLSchema; } const parseGraphQLServer = new ParseGraphQLServer(this, {