diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index cbd16300d9..7dc1346f83 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -539,6 +539,19 @@ describe('ParseGraphQLServer', () => { expect(dateType.kind).toEqual('SCALAR'); }); + it('should have ArrayResult type', async () => { + const arrayResultType = (await apolloClient.query({ + query: gql` + query ArrayResultType { + __type(name: "ArrayResult") { + kind + } + } + `, + })).data['__type']; + expect(arrayResultType.kind).toEqual('UNION'); + }); + it('should have File object type', async () => { const fileType = (await apolloClient.query({ query: gql` @@ -746,6 +759,25 @@ describe('ParseGraphQLServer', () => { ).toBeTruthy(JSON.stringify(schemaTypes)); }); + it('should ArrayResult contains all types', async () => { + const objectType = (await apolloClient.query({ + query: gql` + query ObjectType { + __type(name: "ArrayResult") { + kind + possibleTypes { + name + } + } + } + `, + })).data['__type']; + const possibleTypes = objectType.possibleTypes.map(o => o.name); + expect(possibleTypes).toContain('_UserClass'); + expect(possibleTypes).toContain('_RoleClass'); + expect(possibleTypes).toContain('Element'); + }); + it('should update schema when it changes', async () => { const schemaController = await parseServer.config.databaseController.loadSchema(); await schemaController.updateClass('_User', { @@ -1661,6 +1693,84 @@ describe('ParseGraphQLServer', () => { expect(new Date(result.updatedAt)).toEqual(obj.updatedAt); }); + it_only_db('mongo')( + 'should return child objects in array fields', + async () => { + const obj1 = new Parse.Object('Customer'); + const obj2 = new Parse.Object('SomeClass'); + const obj3 = new Parse.Object('Customer'); + + obj1.set('someCustomerField', 'imCustomerOne'); + const arrayField = [42.42, 42, 'string', true]; + obj1.set('arrayField', arrayField); + await obj1.save(); + + obj2.set('someClassField', 'imSomeClassTwo'); + await obj2.save(); + + //const obj3Relation = obj3.relation('manyRelations') + obj3.set('manyRelations', [obj1, obj2]); + await obj3.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const result = (await apolloClient.query({ + query: gql` + query GetCustomer($objectId: ID!) { + objects { + getCustomer(objectId: $objectId) { + objectId + manyRelations { + ... on CustomerClass { + objectId + someCustomerField + arrayField { + ... on Element { + value + } + } + } + ... on SomeClassClass { + objectId + someClassField + } + } + createdAt + updatedAt + } + } + } + `, + variables: { + objectId: obj3.id, + }, + })).data.objects.getCustomer; + + expect(result.objectId).toEqual(obj3.id); + expect(result.manyRelations.length).toEqual(2); + + const customerSubObject = result.manyRelations.find( + o => o.objectId === obj1.id + ); + const someClassSubObject = result.manyRelations.find( + o => o.objectId === obj2.id + ); + + expect(customerSubObject).toBeDefined(); + expect(someClassSubObject).toBeDefined(); + expect(customerSubObject.someCustomerField).toEqual( + 'imCustomerOne' + ); + const formatedArrayField = customerSubObject.arrayField.map( + elem => elem.value + ); + expect(formatedArrayField).toEqual(arrayField); + expect(someClassSubObject.someClassField).toEqual( + 'imSomeClassTwo' + ); + } + ); + it('should respect level permissions', async () => { await prepareData(); @@ -5601,7 +5711,11 @@ describe('ParseGraphQLServer', () => { findSomeClass(where: { someField: { _exists: true } }) { results { objectId - someField + someField { + ... on Element { + value + } + } } } } diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 261045fe81..a0811b2fe1 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -81,7 +81,7 @@ class ParseGraphQLSchema { parseClassMutations.load(this, parseClass, parseClassConfig); } ); - + defaultGraphQLTypes.loadArrayResult(this, parseClasses); defaultGraphQLQueries.load(this); defaultGraphQLMutations.load(this); diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index 7961132fa8..58115b9198 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -12,6 +12,7 @@ import { GraphQLList, GraphQLInputObjectType, GraphQLBoolean, + GraphQLUnionType, } from 'graphql'; import { GraphQLUpload } from 'graphql-upload'; @@ -1020,6 +1021,55 @@ const SIGN_UP_RESULT = new GraphQLObjectType({ }, }); +const ELEMENT = new GraphQLObjectType({ + name: 'Element', + description: + 'The SignUpResult object type is used in the users sign up mutation to return the data of the recent created user.', + fields: { + value: { + description: 'Return the value of the element in the array', + type: new GraphQLNonNull(ANY), + }, + }, +}); + +// Default static union type, we update types and resolveType function later +let ARRAY_RESULT; + +const loadArrayResult = (parseGraphQLSchema, parseClasses) => { + const classTypes = parseClasses + .filter(parseClass => + parseGraphQLSchema.parseClassTypes[parseClass.className] + .classGraphQLOutputType + ? true + : false + ) + .map( + parseClass => + parseGraphQLSchema.parseClassTypes[parseClass.className] + .classGraphQLOutputType + ); + ARRAY_RESULT = new GraphQLUnionType({ + name: 'ArrayResult', + description: + 'Use Inline Fragment on Array to get results: https://graphql.org/learn/queries/#inline-fragments', + types: () => [ELEMENT, ...classTypes], + resolveType: value => { + if (value.__type === 'Object' && value.className && value.objectId) { + if (parseGraphQLSchema.parseClassTypes[value.className]) { + return parseGraphQLSchema.parseClassTypes[value.className] + .classGraphQLOutputType; + } else { + return ELEMENT; + } + } else { + return ELEMENT; + } + }, + }); + parseGraphQLSchema.graphQLTypes.push(ARRAY_RESULT); +}; + const load = parseGraphQLSchema => { parseGraphQLSchema.graphQLTypes.push(GraphQLUpload); parseGraphQLSchema.graphQLTypes.push(ANY); @@ -1056,6 +1106,7 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.graphQLTypes.push(POLYGON_CONSTRAINT); parseGraphQLSchema.graphQLTypes.push(FIND_RESULT); parseGraphQLSchema.graphQLTypes.push(SIGN_UP_RESULT); + parseGraphQLSchema.graphQLTypes.push(ELEMENT); }; export { @@ -1140,5 +1191,8 @@ export { POLYGON_CONSTRAINT, FIND_RESULT, SIGN_UP_RESULT, + ARRAY_RESULT, + ELEMENT, load, + loadArrayResult, }; diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index eed18d39da..d493df2d5f 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,7 +1,7 @@ import { GraphQLNonNull } from 'graphql'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; -import * as parseClassTypes from './parseClassTypes'; +import { extractKeysAndInclude } from '../parseGraphQLUtils'; import * as objectsMutations from './objectsMutations'; import * as objectsQueries from './objectsQueries'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; @@ -119,9 +119,7 @@ const load = function( info ); const selectedFields = getFieldNames(mutationInfo); - const { keys, include } = parseClassTypes.extractKeysAndInclude( - selectedFields - ); + const { keys, include } = extractKeysAndInclude(selectedFields); const { keys: requiredKeys, needGet } = getOnlyRequiredFields( fields, keys, @@ -180,9 +178,7 @@ const load = function( info ); const selectedFields = getFieldNames(mutationInfo); - const { keys, include } = parseClassTypes.extractKeysAndInclude( - selectedFields - ); + const { keys, include } = extractKeysAndInclude(selectedFields); const { keys: requiredKeys, needGet } = getOnlyRequiredFields( fields, @@ -225,9 +221,7 @@ const load = function( const { objectId } = args; const { config, auth, info } = context; const selectedFields = getFieldNames(mutationInfo); - const { keys, include } = parseClassTypes.extractKeysAndInclude( - selectedFields - ); + const { keys, include } = extractKeysAndInclude(selectedFields); let optimizedObject = {}; const splitedKeys = keys.split(','); diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 1f02f962fb..5d0bc02015 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -2,8 +2,8 @@ import { GraphQLNonNull } from 'graphql'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; -import * as parseClassTypes from './parseClassTypes'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; +import { extractKeysAndInclude } from '../parseGraphQLUtils'; const getParseClassQueryConfig = function( parseClassConfig: ?ParseGraphQLClassConfig @@ -11,6 +11,26 @@ const getParseClassQueryConfig = function( return (parseClassConfig && parseClassConfig.query) || {}; }; +const getQuery = async (className, _source, args, context, queryInfo) => { + const { objectId, readPreference, includeReadPreference } = args; + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); + + const { keys, include } = extractKeysAndInclude(selectedFields); + + return await objectsQueries.getObject( + className, + objectId, + keys, + include, + readPreference, + includeReadPreference, + config, + auth, + info + ); +}; + const load = function( parseGraphQLSchema, parseClass, @@ -40,25 +60,7 @@ const load = function( type: new GraphQLNonNull(classGraphQLOutputType), async resolve(_source, args, context, queryInfo) { try { - const { objectId, readPreference, includeReadPreference } = args; - const { config, auth, info } = context; - const selectedFields = getFieldNames(queryInfo); - - const { keys, include } = parseClassTypes.extractKeysAndInclude( - selectedFields - ); - - return await objectsQueries.getObject( - className, - objectId, - keys, - include, - readPreference, - includeReadPreference, - config, - auth, - info - ); + return await getQuery(className, _source, args, context, queryInfo); } catch (e) { parseGraphQLSchema.handleError(e); } @@ -86,7 +88,7 @@ const load = function( const { config, auth, info } = context; const selectedFields = getFieldNames(queryInfo); - const { keys, include } = parseClassTypes.extractKeysAndInclude( + const { keys, include } = extractKeysAndInclude( selectedFields .filter(field => field.includes('.')) .map(field => field.slice(field.indexOf('.') + 1)) diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 893c786f0c..8c23bf1222 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -14,6 +14,7 @@ import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from './objectsQueries'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; +import { extractKeysAndInclude } from '../parseGraphQLUtils'; const mapInputType = (parseType, targetClass, parseClassTypes) => { switch (parseType) { @@ -65,7 +66,7 @@ const mapOutputType = (parseType, targetClass, parseClassTypes) => { case 'Boolean': return GraphQLBoolean; case 'Array': - return new GraphQLList(defaultGraphQLTypes.ANY); + return new GraphQLList(defaultGraphQLTypes.ARRAY_RESULT); case 'Object': return defaultGraphQLTypes.OBJECT; case 'Date': @@ -135,33 +136,6 @@ const mapConstraintType = (parseType, targetClass, parseClassTypes) => { } }; -const extractKeysAndInclude = selectedFields => { - selectedFields = selectedFields.filter( - field => !field.includes('__typename') - ); - let keys = undefined; - let include = undefined; - if (selectedFields && selectedFields.length > 0) { - keys = selectedFields.join(','); - include = selectedFields - .reduce((fields, field) => { - fields = fields.slice(); - let pointIndex = field.lastIndexOf('.'); - while (pointIndex > 0) { - const lastField = field.slice(pointIndex + 1); - field = field.slice(0, pointIndex); - if (!fields.includes(field) && lastField !== 'objectId') { - fields.push(field); - } - pointIndex = field.lastIndexOf('.'); - } - return fields; - }, []) - .join(','); - } - return { keys, include }; -}; - const getParseClassTypeConfig = function( parseClassConfig: ?ParseGraphQLClassConfig ) { @@ -626,6 +600,27 @@ const load = ( }, }, }; + } else if (parseClass.fields[field].type === 'Array') { + return { + ...fields, + [field]: { + description: `Use Inline Fragment on Array to get results: https://graphql.org/learn/queries/#inline-fragments`, + type, + async resolve(source) { + return source[field].map(async elem => { + if ( + elem.className && + elem.objectId && + elem.__type === 'Object' + ) { + return elem; + } else { + return { value: elem }; + } + }); + }, + }, + }; } else if (type) { return { ...fields, diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index 79f7192e53..5f03e057d4 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -12,3 +12,30 @@ export function toGraphQLError(error) { } return new ApolloError(message, code); } + +export const extractKeysAndInclude = selectedFields => { + selectedFields = selectedFields.filter( + field => !field.includes('__typename') + ); + let keys = undefined; + let include = undefined; + if (selectedFields && selectedFields.length > 0) { + keys = selectedFields.join(','); + include = selectedFields + .reduce((fields, field) => { + fields = fields.slice(); + let pointIndex = field.lastIndexOf('.'); + while (pointIndex > 0) { + const lastField = field.slice(pointIndex + 1); + field = field.slice(0, pointIndex); + if (!fields.includes(field) && lastField !== 'objectId') { + fields.push(field); + } + pointIndex = field.lastIndexOf('.'); + } + return fields; + }, []) + .join(','); + } + return { keys, include }; +};