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

@@ -1643,7 +1643,6 @@ describe('ParseGraphQLServer', () => {
obj2.set('someClassField', 'imSomeClassTwo');
await obj2.save();
//const obj3Relation = obj3.relation('manyRelations')
obj3.set('manyRelations', [obj1, obj2]);
await obj3.save();
@@ -1704,6 +1703,119 @@ describe('ParseGraphQLServer', () => {
}
);
it_only_db('mongo')(
'should return many child objects in allow cyclic query',
async () => {
const obj1 = new Parse.Object('Employee');
const obj2 = new Parse.Object('Team');
const obj3 = new Parse.Object('Company');
const obj4 = new Parse.Object('Country');
obj1.set('name', 'imAnEmployee');
await obj1.save();
obj2.set('name', 'imATeam');
obj2.set('employees', [obj1]);
await obj2.save();
obj3.set('name', 'imACompany');
obj3.set('teams', [obj2]);
obj3.set('employees', [obj1]);
await obj3.save();
obj4.set('name', 'imACountry');
obj4.set('companies', [obj3]);
await obj4.save();
obj1.set('country', obj4);
await obj1.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const result = (await apolloClient.query({
query: gql`
query DeepComplexGraphQLQuery($objectId: ID!) {
country(objectId: $objectId) {
objectId
name
companies {
... on Company {
objectId
name
employees {
... on Employee {
objectId
name
}
}
teams {
... on Team {
objectId
name
employees {
... on Employee {
objectId
name
country {
objectId
name
}
}
}
}
}
}
}
}
}
`,
variables: {
objectId: obj4.id,
},
})).data.country;
const expectedResult = {
objectId: obj4.id,
name: 'imACountry',
__typename: 'Country',
companies: [
{
objectId: obj3.id,
name: 'imACompany',
__typename: 'Company',
employees: [
{
objectId: obj1.id,
name: 'imAnEmployee',
__typename: 'Employee',
},
],
teams: [
{
objectId: obj2.id,
name: 'imATeam',
__typename: 'Team',
employees: [
{
objectId: obj1.id,
name: 'imAnEmployee',
__typename: 'Employee',
country: {
objectId: obj4.id,
name: 'imACountry',
__typename: 'Country',
},
},
],
},
],
},
],
};
expect(result).toEqual(expectedResult);
}
);
it('should respect level permissions', async () => {
await prepareData();
@@ -3321,6 +3433,41 @@ describe('ParseGraphQLServer', () => {
expect(obj.get('someField2')).toEqual('someField2Value1');
});
it('should return only objectId using class specific mutation', async () => {
const obj = new Parse.Object('Customer');
obj.set('someField1', 'someField1Value1');
obj.set('someField2', 'someField2Value1');
await obj.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const result = await apolloClient.mutate({
mutation: gql`
mutation UpdateCustomer(
$objectId: ID!
$fields: UpdateCustomerFieldsInput
) {
updateCustomer(objectId: $objectId, fields: $fields) {
objectId
}
}
`,
variables: {
objectId: obj.id,
fields: {
someField1: 'someField1Value2',
},
},
});
expect(result.data.updateCustomer.objectId).toEqual(obj.id);
await obj.fetch();
expect(obj.get('someField1')).toEqual('someField1Value2');
expect(obj.get('someField2')).toEqual('someField2Value1');
});
it('should respect level permissions', async () => {
await prepareData();
@@ -4759,255 +4906,541 @@ describe('ParseGraphQLServer', () => {
expect(Date.parse(getResult.data.get.updatedAt)).not.toEqual(NaN);
});
it('should support pointer values', async () => {
const parent = new Parse.Object('ParentClass');
await parent.save();
it('should support pointer on create', async () => {
const company = new Parse.Object('Company');
company.set('name', 'imACompany1');
await company.save();
const pointerFieldValue = {
__type: 'Pointer',
className: 'ParentClass',
objectId: parent.id,
};
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.set('company', company);
await country.save();
const createResult = await apolloClient.mutate({
const company2 = new Parse.Object('Company');
company2.set('name', 'imACompany2');
await company2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: { createCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation CreateChildObject($fields: Object) {
create(className: "ChildClass", fields: $fields) {
mutation Create($fields: CreateCountryFieldsInput) {
createCountry(fields: $fields) {
objectId
company {
objectId
name
}
}
}
`,
variables: {
fields: {
pointerField: pointerFieldValue,
name: 'imCountry2',
company: { link: { objectId: company2.id } },
},
},
});
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const schema = await new Parse.Schema('ChildClass').get();
expect(schema.fields.pointerField.type).toEqual('Pointer');
expect(schema.fields.pointerField.targetClass).toEqual('ParentClass');
await apolloClient.mutate({
mutation: gql`
mutation CreateChildObject(
$fields1: CreateChildClassFieldsInput
$fields2: CreateChildClassFieldsInput
) {
createChildClass1: createChildClass(fields: $fields1) {
objectId
}
createChildClass2: createChildClass(fields: $fields2) {
objectId
}
}
`,
variables: {
fields1: {
pointerField: pointerFieldValue,
},
fields2: {
pointerField: pointerFieldValue.objectId,
},
},
});
const getResult = await apolloClient.query({
query: gql`
query GetChildObject(
$objectId: ID!
$pointerFieldValue1: ParentClassPointer
$pointerFieldValue2: ParentClassPointer
) {
get(className: "ChildClass", objectId: $objectId)
findChildClass1: childClasses(
where: { pointerField: { _eq: $pointerFieldValue1 } }
) {
results {
pointerField {
objectId
createdAt
}
}
}
findChildClass2: childClasses(
where: { pointerField: { _eq: $pointerFieldValue2 } }
) {
results {
pointerField {
objectId
createdAt
}
}
}
}
`,
variables: {
objectId: createResult.data.create.objectId,
pointerFieldValue1: pointerFieldValue,
pointerFieldValue2: pointerFieldValue.objectId,
},
});
expect(typeof getResult.data.get.pointerField).toEqual('object');
expect(getResult.data.get.pointerField).toEqual(pointerFieldValue);
expect(getResult.data.findChildClass1.results.length).toEqual(3);
expect(getResult.data.findChildClass2.results.length).toEqual(3);
expect(result.objectId).toBeDefined();
expect(result.company.objectId).toEqual(company2.id);
expect(result.company.name).toEqual('imACompany2');
});
it_only_db('mongo')('should support relation', async () => {
const someObject1 = new Parse.Object('SomeClass');
await someObject1.save();
const someObject2 = new Parse.Object('SomeClass');
await someObject2.save();
it('should support nested pointer on create', async () => {
const company = new Parse.Object('Company');
company.set('name', 'imACompany1');
await company.save();
const pointerValue1 = {
__type: 'Pointer',
className: 'SomeClass',
objectId: someObject1.id,
};
const pointerValue2 = {
__type: 'Pointer',
className: 'SomeClass',
objectId: someObject2.id,
};
const createResult = await apolloClient.mutate({
mutation: gql`
mutation CreateMainObject($fields: Object) {
create(className: "MainClass", fields: $fields) {
objectId
}
}
`,
variables: {
fields: {
relationField: {
__op: 'Batch',
ops: [
{
__op: 'AddRelation',
objects: [pointerValue1],
},
{
__op: 'AddRelation',
objects: [pointerValue2],
},
],
},
},
},
});
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.set('company', company);
await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const schema = await new Parse.Schema('MainClass').get();
expect(schema.fields.relationField.type).toEqual('Relation');
expect(schema.fields.relationField.targetClass).toEqual('SomeClass');
await apolloClient.mutate({
const {
data: { createCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation CreateMainObject($fields: CreateMainClassFieldsInput) {
createMainClass(fields: $fields) {
mutation Create($fields: CreateCountryFieldsInput) {
createCountry(fields: $fields) {
objectId
}
}
`,
variables: {
fields: {
relationField: {
_op: 'Batch',
ops: [
{
_op: 'AddRelation',
objects: [pointerValue1],
},
{
_op: 'RemoveRelation',
objects: [pointerValue1],
},
{
_op: 'AddRelation',
objects: [pointerValue2],
},
],
},
},
},
});
const getResult = await apolloClient.query({
query: gql`
query GetMainObject($objectId: ID!) {
get(className: "MainClass", objectId: $objectId)
mainClass(objectId: $objectId) {
relationField {
results {
objectId
createdAt
}
count
company {
objectId
name
}
}
}
`,
variables: {
objectId: createResult.data.create.objectId,
},
});
expect(typeof getResult.data.get.relationField).toEqual('object');
expect(getResult.data.get.relationField).toEqual({
__type: 'Relation',
className: 'SomeClass',
});
expect(getResult.data.mainClass.relationField.results.length).toEqual(
2
);
expect(getResult.data.mainClass.relationField.count).toEqual(2);
const findResult = await apolloClient.query({
query: gql`
query FindSomeObjects($where: Object) {
find(className: "SomeClass", where: $where) {
results
}
}
`,
variables: {
where: {
$relatedTo: {
object: {
__type: 'Pointer',
className: 'MainClass',
objectId: createResult.data.create.objectId,
fields: {
name: 'imCountry2',
company: {
createAndLink: {
name: 'imACompany2',
},
key: 'relationField',
},
},
},
});
const compare = (obj1, obj2) =>
obj1.createdAt > obj2.createdAt ? 1 : -1;
expect(result.objectId).toBeDefined();
expect(result.company.objectId).toBeDefined();
expect(result.company.name).toEqual('imACompany2');
});
expect(findResult.data.find.results).toEqual(jasmine.any(Array));
expect(findResult.data.find.results.sort(compare)).toEqual(
[
{
objectId: someObject1.id,
createdAt: someObject1.createdAt.toISOString(),
updatedAt: someObject1.updatedAt.toISOString(),
it('should support pointer on update', async () => {
const company = new Parse.Object('Company');
company.set('name', 'imACompany1');
await company.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.set('company', company);
await country.save();
const company2 = new Parse.Object('Company');
company2.set('name', 'imACompany2');
await company2.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: { updateCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation Update(
$objectId: ID!
$fields: UpdateCountryFieldsInput
) {
updateCountry(objectId: $objectId, fields: $fields) {
objectId
company {
objectId
name
}
}
}
`,
variables: {
objectId: country.id,
fields: {
company: { link: { objectId: company2.id } },
},
{
objectId: someObject2.id,
createdAt: someObject2.createdAt.toISOString(),
updatedAt: someObject2.updatedAt.toISOString(),
},
});
expect(result.objectId).toBeDefined();
expect(result.company.objectId).toEqual(company2.id);
expect(result.company.name).toEqual('imACompany2');
});
it('should support nested pointer on update', async () => {
const company = new Parse.Object('Company');
company.set('name', 'imACompany1');
await company.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.set('company', company);
await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: { updateCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation Update(
$objectId: ID!
$fields: UpdateCountryFieldsInput
) {
updateCountry(objectId: $objectId, fields: $fields) {
objectId
company {
objectId
name
}
}
}
`,
variables: {
objectId: country.id,
fields: {
company: {
createAndLink: {
name: 'imACompany2',
},
},
},
].sort(compare)
);
},
});
expect(result.objectId).toBeDefined();
expect(result.company.objectId).toBeDefined();
expect(result.company.name).toEqual('imACompany2');
});
it_only_db('mongo')(
'should support relation and nested relation on create',
async () => {
const company = new Parse.Object('Company');
company.set('name', 'imACompany1');
await company.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.relation('companies').add(company);
await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: { createCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation CreateCountry($fields: CreateCountryFieldsInput) {
createCountry(fields: $fields) {
objectId
name
companies {
results {
objectId
name
}
}
}
}
`,
variables: {
fields: {
name: 'imACountry2',
companies: {
add: [{ objectId: company.id }],
createAndAdd: [
{
name: 'imACompany2',
},
{
name: 'imACompany3',
},
],
},
},
},
});
expect(result.objectId).toBeDefined();
expect(result.name).toEqual('imACountry2');
expect(result.companies.results.length).toEqual(3);
expect(
result.companies.results.some(o => o.objectId === company.id)
).toBeTruthy();
expect(
result.companies.results.some(o => o.name === 'imACompany2')
).toBeTruthy();
expect(
result.companies.results.some(o => o.name === 'imACompany3')
).toBeTruthy();
}
);
it_only_db('mongo')('should support deep nested creation', async () => {
const team = new Parse.Object('Team');
team.set('name', 'imATeam1');
await team.save();
const company = new Parse.Object('Company');
company.set('name', 'imACompany1');
company.relation('teams').add(team);
await company.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.relation('companies').add(company);
await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: { createCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation CreateCountry($fields: CreateCountryFieldsInput) {
createCountry(fields: $fields) {
objectId
name
companies {
results {
objectId
name
teams {
results {
objectId
name
}
}
}
}
}
}
`,
variables: {
fields: {
name: 'imACountry2',
companies: {
createAndAdd: [
{
name: 'imACompany2',
teams: {
createAndAdd: {
name: 'imATeam2',
},
},
},
{
name: 'imACompany3',
teams: {
createAndAdd: {
name: 'imATeam3',
},
},
},
],
},
},
},
});
expect(result.objectId).toBeDefined();
expect(result.name).toEqual('imACountry2');
expect(result.companies.results.length).toEqual(2);
expect(
result.companies.results.some(
c =>
c.name === 'imACompany2' &&
c.teams.results.some(t => t.name === 'imATeam2')
)
).toBeTruthy();
expect(
result.companies.results.some(
c =>
c.name === 'imACompany3' &&
c.teams.results.some(t => t.name === 'imATeam3')
)
).toBeTruthy();
});
it_only_db('mongo')(
'should support relation and nested relation on update',
async () => {
const company1 = new Parse.Object('Company');
company1.set('name', 'imACompany1');
await company1.save();
const company2 = new Parse.Object('Company');
company2.set('name', 'imACompany2');
await company2.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.relation('companies').add(company1);
await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: { updateCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation UpdateCountry(
$objectId: ID!
$fields: UpdateCountryFieldsInput
) {
updateCountry(objectId: $objectId, fields: $fields) {
objectId
companies {
results {
objectId
name
}
}
}
}
`,
variables: {
objectId: country.id,
fields: {
companies: {
add: [{ objectId: company2.id }],
remove: [{ objectId: company1.id }],
createAndAdd: [
{
name: 'imACompany3',
},
],
},
},
},
});
expect(result.objectId).toEqual(country.id);
expect(result.companies.results.length).toEqual(2);
expect(
result.companies.results.some(o => o.objectId === company2.id)
).toBeTruthy();
expect(
result.companies.results.some(o => o.name === 'imACompany3')
).toBeTruthy();
expect(
result.companies.results.some(o => o.objectId === company1.id)
).toBeFalsy();
}
);
it_only_db('mongo')(
'should support nested relation on create with filter',
async () => {
const company = new Parse.Object('Company');
company.set('name', 'imACompany1');
await company.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.relation('companies').add(company);
await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const {
data: { createCountry: result },
} = await apolloClient.mutate({
mutation: gql`
mutation CreateCountry(
$fields: CreateCountryFieldsInput
$where: CompanyWhereInput
) {
createCountry(fields: $fields) {
objectId
name
companies(where: $where) {
results {
objectId
name
}
}
}
}
`,
variables: {
where: {
name: {
_eq: 'imACompany2',
},
},
fields: {
name: 'imACountry2',
companies: {
add: [{ objectId: company.id }],
createAndAdd: [
{
name: 'imACompany2',
},
{
name: 'imACompany3',
},
],
},
},
},
});
expect(result.objectId).toBeDefined();
expect(result.name).toEqual('imACountry2');
expect(result.companies.results.length).toEqual(1);
expect(
result.companies.results.some(o => o.name === 'imACompany2')
).toBeTruthy();
}
);
it_only_db('mongo')('should support relation on query', async () => {
const company1 = new Parse.Object('Company');
company1.set('name', 'imACompany1');
await company1.save();
const company2 = new Parse.Object('Company');
company2.set('name', 'imACompany2');
await company2.save();
const country = new Parse.Object('Country');
country.set('name', 'imACountry');
country.relation('companies').add([company1, company2]);
await country.save();
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
// Without where
const {
data: { country: result1 },
} = await apolloClient.query({
query: gql`
query getCountry($objectId: ID!) {
country(objectId: $objectId) {
objectId
companies {
results {
objectId
name
}
}
}
}
`,
variables: {
objectId: country.id,
},
});
expect(result1.objectId).toEqual(country.id);
expect(result1.companies.results.length).toEqual(2);
expect(
result1.companies.results.some(o => o.objectId === company1.id)
).toBeTruthy();
expect(
result1.companies.results.some(o => o.objectId === company2.id)
).toBeTruthy();
// With where
const {
data: { country: result2 },
} = await apolloClient.query({
query: gql`
query getCountry($objectId: ID!, $where: CompanyWhereInput) {
country(objectId: $objectId) {
objectId
companies(where: $where) {
results {
objectId
name
}
}
}
}
`,
variables: {
objectId: country.id,
where: {
name: { _eq: 'imACompany1' },
},
},
});
expect(result2.objectId).toEqual(country.id);
expect(result2.companies.results.length).toEqual(1);
expect(result2.companies.results[0].objectId).toEqual(company1.id);
});
it('should support files', async () => {

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 };