GraphQL DX: Relation/Pointer (#5946)

* Add a test on deep complex GraphQL Query

* Relation/Pointer new DX + deep nested mutations

* Fix lint

* Review

* Remove unnecessary code

* Fix objectId on update
This commit is contained in:
Antoine Cormouls
2019-08-21 23:55:34 +02:00
committed by Antonio Davi Macedo Coelho de Castro
parent 89e8868a85
commit 5b3a492965
7 changed files with 956 additions and 327 deletions

View File

@@ -395,14 +395,14 @@ const POLYGON_INPUT = new GraphQLList(new GraphQLNonNull(GEO_POINT_INPUT));
const POLYGON = new GraphQLList(new GraphQLNonNull(GEO_POINT));
const RELATION_OP = new GraphQLEnumType({
name: 'RelationOp',
description:
'The RelationOp enum type is used to specify which kind of operation should be executed to a relation.',
values: {
Batch: { value: 'Batch' },
AddRelation: { value: 'AddRelation' },
RemoveRelation: { value: 'RemoveRelation' },
const RELATION_INPUT = new GraphQLInputObjectType({
name: 'RelationInput',
description: 'Object involved into a relation',
fields: {
objectId: {
description: 'Id of the object involved.',
type: new GraphQLNonNull(GraphQLID),
},
},
});
@@ -491,6 +491,17 @@ const INCLUDE_ATT = {
type: GraphQLString,
};
const POINTER_INPUT = new GraphQLInputObjectType({
name: 'PointerInput',
description: 'Allow to link an object to another object',
fields: {
objectId: {
description: 'Id of the object involved.',
type: new GraphQLNonNull(GraphQLID),
},
},
});
const READ_PREFERENCE = new GraphQLEnumType({
name: 'ReadPreference',
description:
@@ -1080,7 +1091,6 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLType(FILE_INFO, true);
parseGraphQLSchema.addGraphQLType(GEO_POINT_INPUT, true);
parseGraphQLSchema.addGraphQLType(GEO_POINT, true);
parseGraphQLSchema.addGraphQLType(RELATION_OP, true);
parseGraphQLSchema.addGraphQLType(CREATE_RESULT, true);
parseGraphQLSchema.addGraphQLType(UPDATE_RESULT, true);
parseGraphQLSchema.addGraphQLType(CLASS, true);
@@ -1108,6 +1118,8 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLType(FIND_RESULT, true);
parseGraphQLSchema.addGraphQLType(SIGN_UP_RESULT, true);
parseGraphQLSchema.addGraphQLType(ELEMENT, true);
parseGraphQLSchema.addGraphQLType(RELATION_INPUT, true);
parseGraphQLSchema.addGraphQLType(POINTER_INPUT, true);
};
export {
@@ -1133,7 +1145,6 @@ export {
GEO_POINT,
POLYGON_INPUT,
POLYGON,
RELATION_OP,
CLASS_NAME_ATT,
FIELDS_ATT,
OBJECT_ID_ATT,
@@ -1195,6 +1206,8 @@ export {
SIGN_UP_RESULT,
ARRAY_RESULT,
ELEMENT,
POINTER_INPUT,
RELATION_INPUT,
load,
loadArrayResult,
};

View File

@@ -1,15 +1,12 @@
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import rest from '../../rest';
import { transformMutationInputToParse } from '../transformers/mutation';
const createObject = async (className, fields, config, auth, info) => {
if (!fields) {
fields = {};
}
transformMutationInputToParse(fields);
return (await rest.create(config, auth, className, fields, info.clientSDK))
.response;
};
@@ -26,8 +23,6 @@ const updateObject = async (
fields = {};
}
transformMutationInputToParse(fields);
return (await rest.update(
config,
auth,

View File

@@ -1,17 +1,15 @@
import { GraphQLNonNull } from 'graphql';
import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import { extractKeysAndInclude } from '../parseGraphQLUtils';
import {
extractKeysAndInclude,
getParseClassMutationConfig,
} from '../parseGraphQLUtils';
import * as objectsMutations from './objectsMutations';
import * as objectsQueries from './objectsQueries';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
import { transformClassNameToGraphQL } from '../transformers/className';
const getParseClassMutationConfig = function(
parseClassConfig: ?ParseGraphQLClassConfig
) {
return (parseClassConfig && parseClassConfig.mutation) || {};
};
import { transformTypes } from '../transformers/mutation';
const getOnlyRequiredFields = (
updatedFields,
@@ -55,43 +53,6 @@ const load = function(
classGraphQLOutputType,
} = parseGraphQLSchema.parseClassTypes[className];
const transformTypes = (inputType: 'create' | 'update', fields) => {
if (fields) {
const classGraphQLCreateTypeFields =
isCreateEnabled && classGraphQLCreateType
? classGraphQLCreateType.getFields()
: null;
const classGraphQLUpdateTypeFields =
isUpdateEnabled && classGraphQLUpdateType
? classGraphQLUpdateType.getFields()
: null;
Object.keys(fields).forEach(field => {
let inputTypeField;
if (inputType === 'create' && classGraphQLCreateTypeFields) {
inputTypeField = classGraphQLCreateTypeFields[field];
} else if (classGraphQLUpdateTypeFields) {
inputTypeField = classGraphQLUpdateTypeFields[field];
}
if (inputTypeField) {
switch (inputTypeField.type) {
case defaultGraphQLTypes.GEO_POINT_INPUT:
fields[field].__type = 'GeoPoint';
break;
case defaultGraphQLTypes.POLYGON_INPUT:
fields[field] = {
__type: 'Polygon',
coordinates: fields[field].map(geoPoint => [
geoPoint.latitude,
geoPoint.longitude,
]),
};
break;
}
}
});
}
};
if (isCreateEnabled) {
const createGraphQLMutationName = `create${graphQLClassName}`;
parseGraphQLSchema.addGraphQLMutation(createGraphQLMutationName, {
@@ -110,10 +71,16 @@ const load = function(
let { fields } = args;
if (!fields) fields = {};
const { config, auth, info } = context;
transformTypes('create', fields);
const parseFields = await transformTypes('create', fields, {
className,
parseGraphQLSchema,
req: { config, auth, info },
});
const createdObject = await objectsMutations.createObject(
className,
fields,
parseFields,
config,
auth,
info
@@ -172,12 +139,16 @@ const load = function(
const { objectId, fields } = args;
const { config, auth, info } = context;
transformTypes('update', fields);
const parseFields = await transformTypes('update', fields, {
className,
parseGraphQLSchema,
req: { config, auth, info },
});
const updatedObject = await objectsMutations.updateObject(
className,
objectId,
fields,
parseFields,
config,
auth,
info
@@ -205,7 +176,12 @@ const load = function(
info
);
}
return { ...updatedObject, ...fields, ...optimizedObject };
return {
objectId: objectId,
...updatedObject,
...fields,
...optimizedObject,
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}

View File

@@ -15,7 +15,10 @@ import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsQueries from './objectsQueries';
import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController';
import { transformClassNameToGraphQL } from '../transformers/className';
import { extractKeysAndInclude } from '../parseGraphQLUtils';
import {
extractKeysAndInclude,
getParseClassMutationConfig,
} from '../parseGraphQLUtils';
const mapInputType = (parseType, targetClass, parseClassTypes) => {
switch (parseType) {
@@ -34,18 +37,18 @@ const mapInputType = (parseType, targetClass, parseClassTypes) => {
case 'Pointer':
if (
parseClassTypes[targetClass] &&
parseClassTypes[targetClass].classGraphQLScalarType
parseClassTypes[targetClass].classGraphQLPointerType
) {
return parseClassTypes[targetClass].classGraphQLScalarType;
return parseClassTypes[targetClass].classGraphQLPointerType;
} else {
return defaultGraphQLTypes.OBJECT;
}
case 'Relation':
if (
parseClassTypes[targetClass] &&
parseClassTypes[targetClass].classGraphQLRelationOpType
parseClassTypes[targetClass].classGraphQLRelationType
) {
return parseClassTypes[targetClass].classGraphQLRelationOpType;
return parseClassTypes[targetClass].classGraphQLRelationType;
} else {
return defaultGraphQLTypes.OBJECT;
}
@@ -259,6 +262,11 @@ const load = (
classSortFields,
} = getInputFieldsAndConstraints(parseClass, parseClassConfig);
const {
create: isCreateEnabled = true,
update: isUpdateEnabled = true,
} = getParseClassMutationConfig(parseClassConfig);
const classGraphQLScalarTypeName = `${graphQLClassName}Pointer`;
const parseScalarValue = value => {
if (typeof value === 'string') {
@@ -339,31 +347,6 @@ const load = (
parseGraphQLSchema.addGraphQLType(classGraphQLScalarType) ||
defaultGraphQLTypes.OBJECT;
const classGraphQLRelationOpTypeName = `${graphQLClassName}RelationOpInput`;
let classGraphQLRelationOpType = new GraphQLInputObjectType({
name: classGraphQLRelationOpTypeName,
description: `The ${classGraphQLRelationOpTypeName} type is used in operations that involve relations with the ${graphQLClassName} class.`,
fields: () => ({
_op: {
description: 'This is the operation to be executed.',
type: new GraphQLNonNull(defaultGraphQLTypes.RELATION_OP),
},
ops: {
description:
'In the case of a Batch operation, this is the list of operations to be executed.',
type: new GraphQLList(new GraphQLNonNull(classGraphQLRelationOpType)),
},
objects: {
description:
'In the case of a AddRelation or RemoveRelation operation, this is the list of objects to be added/removed.',
type: new GraphQLList(new GraphQLNonNull(classGraphQLScalarType)),
},
}),
});
classGraphQLRelationOpType =
parseGraphQLSchema.addGraphQLType(classGraphQLRelationOpType) ||
defaultGraphQLTypes.OBJECT;
const classGraphQLCreateTypeName = `Create${graphQLClassName}FieldsInput`;
let classGraphQLCreateType = new GraphQLInputObjectType({
name: classGraphQLCreateTypeName,
@@ -430,6 +413,62 @@ const load = (
classGraphQLUpdateType
);
const classGraphQLPointerTypeName = `${graphQLClassName}PointerInput`;
let classGraphQLPointerType = new GraphQLInputObjectType({
name: classGraphQLPointerTypeName,
description: `Allow to link OR add and link an object of the ${graphQLClassName} class.`,
fields: () => {
const fields = {
link: {
description: `Link an existing object from ${graphQLClassName} class.`,
type: defaultGraphQLTypes.POINTER_INPUT,
},
};
if (isCreateEnabled) {
fields['createAndLink'] = {
description: `Create and link an object from ${graphQLClassName} class.`,
type: classGraphQLCreateType,
};
}
return fields;
},
});
classGraphQLPointerType =
parseGraphQLSchema.addGraphQLType(classGraphQLPointerType) ||
defaultGraphQLTypes.OBJECT;
const classGraphQLRelationTypeName = `${graphQLClassName}RelationInput`;
let classGraphQLRelationType = new GraphQLInputObjectType({
name: classGraphQLRelationTypeName,
description: `Allow to add, remove, createAndAdd objects of the ${graphQLClassName} class into a relation field.`,
fields: () => {
const fields = {
add: {
description: `Add an existing object from the ${graphQLClassName} class into the relation.`,
type: new GraphQLList(
new GraphQLNonNull(defaultGraphQLTypes.RELATION_INPUT)
),
},
remove: {
description: `Remove an existing object from the ${graphQLClassName} class out of the relation.`,
type: new GraphQLList(
new GraphQLNonNull(defaultGraphQLTypes.RELATION_INPUT)
),
},
};
if (isCreateEnabled) {
fields['createAndAdd'] = {
description: `Create and add an object of the ${graphQLClassName} class into the relation.`,
type: new GraphQLList(new GraphQLNonNull(classGraphQLCreateType)),
};
}
return fields;
},
});
classGraphQLRelationType =
parseGraphQLSchema.addGraphQLType(classGraphQLRelationType) ||
defaultGraphQLTypes.OBJECT;
const classGraphQLConstraintTypeName = `${graphQLClassName}PointerWhereInput`;
let classGraphQLConstraintType = new GraphQLInputObjectType({
name: classGraphQLConstraintTypeName,
@@ -700,8 +739,9 @@ const load = (
);
parseGraphQLSchema.parseClassTypes[className] = {
classGraphQLPointerType,
classGraphQLRelationType,
classGraphQLScalarType,
classGraphQLRelationOpType,
classGraphQLCreateType,
classGraphQLUpdateType,
classGraphQLConstraintType,
@@ -709,6 +749,11 @@ const load = (
classGraphQLFindArgs,
classGraphQLOutputType,
classGraphQLFindResultType,
config: {
parseClassConfig,
isCreateEnabled,
isUpdateEnabled,
},
};
if (className === '_User') {

View File

@@ -39,3 +39,7 @@ export const extractKeysAndInclude = selectedFields => {
}
return { keys, include };
};
export const getParseClassMutationConfig = function(parseClassConfig) {
return (parseClassConfig && parseClassConfig.mutation) || {};
};

View File

@@ -1,21 +1,184 @@
const parseMap = {
_op: '__op',
};
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
import * as objectsMutations from '../loaders/objectsMutations';
const transformMutationInputToParse = fields => {
if (!fields || typeof fields !== 'object') {
return;
const transformTypes = async (
inputType: 'create' | 'update',
fields,
{ className, parseGraphQLSchema, req }
) => {
const {
classGraphQLCreateType,
classGraphQLUpdateType,
config: { isCreateEnabled, isUpdateEnabled },
} = parseGraphQLSchema.parseClassTypes[className];
const parseClass = parseGraphQLSchema.parseClasses.find(
clazz => clazz.className === className
);
if (fields) {
const classGraphQLCreateTypeFields =
isCreateEnabled && classGraphQLCreateType
? classGraphQLCreateType.getFields()
: null;
const classGraphQLUpdateTypeFields =
isUpdateEnabled && classGraphQLUpdateType
? classGraphQLUpdateType.getFields()
: null;
const promises = Object.keys(fields).map(async field => {
let inputTypeField;
if (inputType === 'create' && classGraphQLCreateTypeFields) {
inputTypeField = classGraphQLCreateTypeFields[field];
} else if (classGraphQLUpdateTypeFields) {
inputTypeField = classGraphQLUpdateTypeFields[field];
}
if (inputTypeField) {
switch (true) {
case inputTypeField.type === defaultGraphQLTypes.GEO_POINT_INPUT:
fields[field] = transformers.geoPoint(fields[field]);
break;
case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT:
fields[field] = transformers.polygon(fields[field]);
break;
case parseClass.fields[field].type === 'Relation':
fields[field] = await transformers.relation(
parseClass.fields[field].targetClass,
field,
fields[field],
parseGraphQLSchema,
req
);
break;
case parseClass.fields[field].type === 'Pointer':
fields[field] = await transformers.pointer(
parseClass.fields[field].targetClass,
field,
fields[field],
parseGraphQLSchema,
req
);
break;
}
}
});
await Promise.all(promises);
}
Object.keys(fields).forEach(fieldName => {
const fieldValue = fields[fieldName];
if (parseMap[fieldName]) {
delete fields[fieldName];
fields[parseMap[fieldName]] = fieldValue;
}
if (typeof fieldValue === 'object') {
transformMutationInputToParse(fieldValue);
}
});
return fields;
};
export { transformMutationInputToParse };
const transformers = {
polygon: value => ({
__type: 'Polygon',
coordinates: value.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]),
}),
geoPoint: value => ({
...value,
__type: 'GeoPoint',
}),
relation: async (
targetClass,
field,
value,
parseGraphQLSchema,
{ config, auth, info }
) => {
if (Object.keys(value) === 0)
throw new Error(
`You need to provide atleast one operation on the relation mutation of field ${field}`
);
const op = {
__op: 'Batch',
ops: [],
};
let nestedObjectsToAdd = [];
if (value.createAndAdd) {
nestedObjectsToAdd = (await Promise.all(
value.createAndAdd.map(async input => {
const parseFields = await transformTypes('create', input, {
className: targetClass,
parseGraphQLSchema,
req: { config, auth, info },
});
return objectsMutations.createObject(
targetClass,
parseFields,
config,
auth,
info
);
})
)).map(object => ({
__type: 'Pointer',
className: targetClass,
objectId: object.objectId,
}));
}
if (value.add || nestedObjectsToAdd.length > 0) {
if (!value.add) value.add = [];
value.add = value.add.map(input => ({
__type: 'Pointer',
className: targetClass,
objectId: input.objectId,
}));
op.ops.push({
__op: 'AddRelation',
objects: [...value.add, ...nestedObjectsToAdd],
});
}
if (value.remove) {
op.ops.push({
__op: 'RemoveRelation',
objects: value.remove.map(input => ({
__type: 'Pointer',
className: targetClass,
objectId: input.objectId,
})),
});
}
return op;
},
pointer: async (
targetClass,
field,
value,
parseGraphQLSchema,
{ config, auth, info }
) => {
if (Object.keys(value) > 1 || Object.keys(value) === 0)
throw new Error(
`You need to provide link OR createLink on the pointer mutation of field ${field}`
);
let nestedObjectToAdd;
if (value.createAndLink) {
const parseFields = await transformTypes('create', value.createAndLink, {
className: targetClass,
parseGraphQLSchema,
req: { config, auth, info },
});
nestedObjectToAdd = await objectsMutations.createObject(
targetClass,
parseFields,
config,
auth,
info
);
return {
__type: 'Pointer',
className: targetClass,
objectId: nestedObjectToAdd.objectId,
};
}
if (value.link && value.link.objectId) {
return {
__type: 'Pointer',
className: targetClass,
objectId: value.link.objectId,
};
}
},
};
export { transformTypes };