GraphQL: Inline Fragment on Array Fields (#5908)

* Inline Fragment Spec

* Inline Fragment on Arrays

* Fix Test

* Only select the root field

* Requested Changes

* Lazy Loaded ArrayResult
This commit is contained in:
Antoine Cormouls
2019-08-14 21:25:28 +02:00
committed by Antonio Davi Macedo Coelho de Castro
parent 45dabbbcda
commit 4bffdce047
7 changed files with 247 additions and 61 deletions

View File

@@ -539,6 +539,19 @@ describe('ParseGraphQLServer', () => {
expect(dateType.kind).toEqual('SCALAR'); 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 () => { it('should have File object type', async () => {
const fileType = (await apolloClient.query({ const fileType = (await apolloClient.query({
query: gql` query: gql`
@@ -746,6 +759,25 @@ describe('ParseGraphQLServer', () => {
).toBeTruthy(JSON.stringify(schemaTypes)); ).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 () => { it('should update schema when it changes', async () => {
const schemaController = await parseServer.config.databaseController.loadSchema(); const schemaController = await parseServer.config.databaseController.loadSchema();
await schemaController.updateClass('_User', { await schemaController.updateClass('_User', {
@@ -1661,6 +1693,84 @@ describe('ParseGraphQLServer', () => {
expect(new Date(result.updatedAt)).toEqual(obj.updatedAt); 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 () => { it('should respect level permissions', async () => {
await prepareData(); await prepareData();
@@ -5609,7 +5719,11 @@ describe('ParseGraphQLServer', () => {
findSomeClass(where: { someField: { _exists: true } }) { findSomeClass(where: { someField: { _exists: true } }) {
results { results {
objectId objectId
someField someField {
... on Element {
value
}
}
} }
} }
} }

View File

@@ -81,7 +81,7 @@ class ParseGraphQLSchema {
parseClassMutations.load(this, parseClass, parseClassConfig); parseClassMutations.load(this, parseClass, parseClassConfig);
} }
); );
defaultGraphQLTypes.loadArrayResult(this, parseClasses);
defaultGraphQLQueries.load(this); defaultGraphQLQueries.load(this);
defaultGraphQLMutations.load(this); defaultGraphQLMutations.load(this);

View File

@@ -12,6 +12,7 @@ import {
GraphQLList, GraphQLList,
GraphQLInputObjectType, GraphQLInputObjectType,
GraphQLBoolean, GraphQLBoolean,
GraphQLUnionType,
} from 'graphql'; } from 'graphql';
import { GraphQLUpload } from 'graphql-upload'; 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 => { const load = parseGraphQLSchema => {
parseGraphQLSchema.graphQLTypes.push(GraphQLUpload); parseGraphQLSchema.graphQLTypes.push(GraphQLUpload);
parseGraphQLSchema.graphQLTypes.push(ANY); parseGraphQLSchema.graphQLTypes.push(ANY);
@@ -1056,6 +1106,7 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.graphQLTypes.push(POLYGON_CONSTRAINT); parseGraphQLSchema.graphQLTypes.push(POLYGON_CONSTRAINT);
parseGraphQLSchema.graphQLTypes.push(FIND_RESULT); parseGraphQLSchema.graphQLTypes.push(FIND_RESULT);
parseGraphQLSchema.graphQLTypes.push(SIGN_UP_RESULT); parseGraphQLSchema.graphQLTypes.push(SIGN_UP_RESULT);
parseGraphQLSchema.graphQLTypes.push(ELEMENT);
}; };
export { export {
@@ -1140,5 +1191,8 @@ export {
POLYGON_CONSTRAINT, POLYGON_CONSTRAINT,
FIND_RESULT, FIND_RESULT,
SIGN_UP_RESULT, SIGN_UP_RESULT,
ARRAY_RESULT,
ELEMENT,
load, load,
loadArrayResult,
}; };

View File

@@ -1,7 +1,7 @@
import { GraphQLNonNull } from 'graphql'; import { GraphQLNonNull } from 'graphql';
import getFieldNames from 'graphql-list-fields'; import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as parseClassTypes from './parseClassTypes'; import { extractKeysAndInclude } from '../parseGraphQLUtils';
import * as objectsMutations from './objectsMutations'; import * as objectsMutations from './objectsMutations';
import * as objectsQueries from './objectsQueries'; import * as objectsQueries from './objectsQueries';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
@@ -119,9 +119,7 @@ const load = function(
info info
); );
const selectedFields = getFieldNames(mutationInfo); const selectedFields = getFieldNames(mutationInfo);
const { keys, include } = parseClassTypes.extractKeysAndInclude( const { keys, include } = extractKeysAndInclude(selectedFields);
selectedFields
);
const { keys: requiredKeys, needGet } = getOnlyRequiredFields( const { keys: requiredKeys, needGet } = getOnlyRequiredFields(
fields, fields,
keys, keys,
@@ -180,9 +178,7 @@ const load = function(
info info
); );
const selectedFields = getFieldNames(mutationInfo); const selectedFields = getFieldNames(mutationInfo);
const { keys, include } = parseClassTypes.extractKeysAndInclude( const { keys, include } = extractKeysAndInclude(selectedFields);
selectedFields
);
const { keys: requiredKeys, needGet } = getOnlyRequiredFields( const { keys: requiredKeys, needGet } = getOnlyRequiredFields(
fields, fields,
@@ -225,9 +221,7 @@ const load = function(
const { objectId } = args; const { objectId } = args;
const { config, auth, info } = context; const { config, auth, info } = context;
const selectedFields = getFieldNames(mutationInfo); const selectedFields = getFieldNames(mutationInfo);
const { keys, include } = parseClassTypes.extractKeysAndInclude( const { keys, include } = extractKeysAndInclude(selectedFields);
selectedFields
);
let optimizedObject = {}; let optimizedObject = {};
const splitedKeys = keys.split(','); const splitedKeys = keys.split(',');

View File

@@ -2,8 +2,8 @@ import { GraphQLNonNull } from 'graphql';
import getFieldNames from 'graphql-list-fields'; import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsQueries from './objectsQueries'; import * as objectsQueries from './objectsQueries';
import * as parseClassTypes from './parseClassTypes';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
import { extractKeysAndInclude } from '../parseGraphQLUtils';
const getParseClassQueryConfig = function( const getParseClassQueryConfig = function(
parseClassConfig: ?ParseGraphQLClassConfig parseClassConfig: ?ParseGraphQLClassConfig
@@ -11,6 +11,26 @@ const getParseClassQueryConfig = function(
return (parseClassConfig && parseClassConfig.query) || {}; 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( const load = function(
parseGraphQLSchema, parseGraphQLSchema,
parseClass, parseClass,
@@ -40,25 +60,7 @@ const load = function(
type: new GraphQLNonNull(classGraphQLOutputType), type: new GraphQLNonNull(classGraphQLOutputType),
async resolve(_source, args, context, queryInfo) { async resolve(_source, args, context, queryInfo) {
try { try {
const { objectId, readPreference, includeReadPreference } = args; return await getQuery(className, _source, args, context, queryInfo);
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
);
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(e); parseGraphQLSchema.handleError(e);
} }
@@ -86,7 +88,7 @@ const load = function(
const { config, auth, info } = context; const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo); const selectedFields = getFieldNames(queryInfo);
const { keys, include } = parseClassTypes.extractKeysAndInclude( const { keys, include } = extractKeysAndInclude(
selectedFields selectedFields
.filter(field => field.includes('.')) .filter(field => field.includes('.'))
.map(field => field.slice(field.indexOf('.') + 1)) .map(field => field.slice(field.indexOf('.') + 1))

View File

@@ -14,6 +14,7 @@ import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsQueries from './objectsQueries'; import * as objectsQueries from './objectsQueries';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
import { extractKeysAndInclude } from '../parseGraphQLUtils';
const mapInputType = (parseType, targetClass, parseClassTypes) => { const mapInputType = (parseType, targetClass, parseClassTypes) => {
switch (parseType) { switch (parseType) {
@@ -65,7 +66,7 @@ const mapOutputType = (parseType, targetClass, parseClassTypes) => {
case 'Boolean': case 'Boolean':
return GraphQLBoolean; return GraphQLBoolean;
case 'Array': case 'Array':
return new GraphQLList(defaultGraphQLTypes.ANY); return new GraphQLList(defaultGraphQLTypes.ARRAY_RESULT);
case 'Object': case 'Object':
return defaultGraphQLTypes.OBJECT; return defaultGraphQLTypes.OBJECT;
case 'Date': 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( const getParseClassTypeConfig = function(
parseClassConfig: ?ParseGraphQLClassConfig 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) { } else if (type) {
return { return {
...fields, ...fields,

View File

@@ -12,3 +12,30 @@ export function toGraphQLError(error) {
} }
return new ApolloError(message, code); 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 };
};