From 0e8c2319fdcf61b826524bb80059c6624481b976 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 15 Aug 2019 14:12:34 +0200 Subject: [PATCH 1/6] Add a test on deep complex GraphQL Query --- spec/ParseGraphQLServer.spec.js | 115 ++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 13cb825803..427c926c9d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1771,6 +1771,121 @@ 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!) { + objects { + getCountry(objectId: $objectId) { + objectId + name + companies { + ... on CompanyClass { + objectId + name + employees { + ... on EmployeeClass { + objectId + name + } + } + teams { + ... on TeamClass { + objectId + name + employees { + ... on EmployeeClass { + objectId + name + country { + objectId + name + } + } + } + } + } + } + } + } + } + } + `, + variables: { + objectId: obj4.id, + }, + })).data.objects.getCountry; + + const expectedResult = { + objectId: obj4.id, + name: 'imACountry', + __typename: 'CountryClass', + companies: [ + { + objectId: obj3.id, + name: 'imACompany', + __typename: 'CompanyClass', + employees: [ + { + objectId: obj1.id, + name: 'imAnEmployee', + __typename: 'EmployeeClass', + }, + ], + teams: [ + { + objectId: obj2.id, + name: 'imATeam', + __typename: 'TeamClass', + employees: [ + { + objectId: obj1.id, + name: 'imAnEmployee', + __typename: 'EmployeeClass', + country: { + objectId: obj4.id, + name: 'imACountry', + __typename: 'CountryClass', + }, + }, + ], + }, + ], + }, + ], + }; + expect(result).toEqual(expectedResult); + } + ); + it('should respect level permissions', async () => { await prepareData(); From 3b84b7c6b86cfe3b5c30818e12f093ee0f9be4eb Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 18 Aug 2019 21:07:41 +0200 Subject: [PATCH 2/6] Relation/Pointer new DX + deep nested mutations --- spec/ParseGraphQLServer.spec.js | 659 +++++++++++++++------ src/GraphQL/loaders/defaultGraphQLTypes.js | 33 +- src/GraphQL/loaders/objectsMutations.js | 5 - src/GraphQL/loaders/parseClassMutations.js | 79 +-- src/GraphQL/loaders/parseClassTypes.js | 107 +++- src/GraphQL/parseGraphQLUtils.js | 6 + src/GraphQL/transformers/mutation.js | 203 ++++++- 7 files changed, 794 insertions(+), 298 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index c8965f6093..5a4918f3b4 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1629,7 +1629,6 @@ describe('ParseGraphQLServer', () => { obj2.set('someClassField', 'imSomeClassTwo'); await obj2.save(); - //const obj3Relation = obj3.relation('manyRelations') obj3.set('manyRelations', [obj1, obj2]); await obj3.save(); @@ -1722,32 +1721,30 @@ describe('ParseGraphQLServer', () => { const result = (await apolloClient.query({ query: gql` query DeepComplexGraphQLQuery($objectId: ID!) { - objects { - getCountry(objectId: $objectId) { - objectId - name - companies { - ... on CompanyClass { - objectId - name - employees { - ... on EmployeeClass { - objectId - name - } + country(objectId: $objectId) { + objectId + name + companies { + ... on Company { + objectId + name + employees { + ... on Employee { + objectId + name } - teams { - ... on TeamClass { - objectId - name - employees { - ... on EmployeeClass { + } + teams { + ... on Team { + objectId + name + employees { + ... on Employee { + objectId + name + country { objectId name - country { - objectId - name - } } } } @@ -1761,38 +1758,38 @@ describe('ParseGraphQLServer', () => { variables: { objectId: obj4.id, }, - })).data.objects.getCountry; + })).data.country; const expectedResult = { objectId: obj4.id, name: 'imACountry', - __typename: 'CountryClass', + __typename: 'Country', companies: [ { objectId: obj3.id, name: 'imACompany', - __typename: 'CompanyClass', + __typename: 'Company', employees: [ { objectId: obj1.id, name: 'imAnEmployee', - __typename: 'EmployeeClass', + __typename: 'Employee', }, ], teams: [ { objectId: obj2.id, name: 'imATeam', - __typename: 'TeamClass', + __typename: 'Team', employees: [ { objectId: obj1.id, name: 'imAnEmployee', - __typename: 'EmployeeClass', + __typename: 'Employee', country: { objectId: obj4.id, name: 'imACountry', - __typename: 'CountryClass', + __typename: 'Country', }, }, ], @@ -4860,178 +4857,308 @@ 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(); + expect(result.objectId).toBeDefined(); + expect(result.company.objectId).toEqual(company2.id); + expect(result.company.name).toEqual('imACompany2'); + }); - const schema = await new Parse.Schema('ChildClass').get(); - expect(schema.fields.pointerField.type).toEqual('Pointer'); - expect(schema.fields.pointerField.targetClass).toEqual('ParentClass'); + it('should support nested pointer on create', async () => { + const company = new Parse.Object('Company'); + company.set('name', 'imACompany1'); + await company.save(); - await apolloClient.mutate({ + 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: { createCountry: result }, + } = await apolloClient.mutate({ mutation: gql` - mutation CreateChildObject( - $fields1: CreateChildClassFieldsInput - $fields2: CreateChildClassFieldsInput - ) { - createChildClass1: createChildClass(fields: $fields1) { - objectId - } - createChildClass2: createChildClass(fields: $fields2) { + mutation Create($fields: CreateCountryFieldsInput) { + createCountry(fields: $fields) { objectId + company { + objectId + name + } } } `, variables: { - fields1: { - pointerField: pointerFieldValue, - }, - fields2: { - pointerField: pointerFieldValue.objectId, + fields: { + name: 'imCountry2', + company: { + createAndLink: { + name: 'imACompany2', + }, + }, }, }, }); - const getResult = await apolloClient.query({ - query: gql` - query GetChildObject( + expect(result.objectId).toBeDefined(); + expect(result.company.objectId).toBeDefined(); + expect(result.company.name).toEqual('imACompany2'); + }); + + 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! - $pointerFieldValue1: ParentClassPointer - $pointerFieldValue2: ParentClassPointer + $fields: UpdateCountryFieldsInput ) { - 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 - } + updateCountry(objectId: $objectId, fields: $fields) { + objectId + company { + objectId + name } } } `, variables: { - objectId: createResult.data.create.objectId, - pointerFieldValue1: pointerFieldValue, - pointerFieldValue2: pointerFieldValue.objectId, + objectId: country.id, + fields: { + company: { link: { objectId: company2.id } }, + }, }, }); - 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 update', 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 country = new Parse.Object('Country'); + country.set('name', 'imACountry'); + country.set('company', company); + await country.save(); - const createResult = await apolloClient.mutate({ + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const { + data: { updateCountry: result }, + } = await apolloClient.mutate({ mutation: gql` - mutation CreateMainObject($fields: Object) { - create(className: "MainClass", fields: $fields) { + mutation Update( + $objectId: ID! + $fields: UpdateCountryFieldsInput + ) { + updateCountry(objectId: $objectId, fields: $fields) { objectId + company { + objectId + name + } } } `, variables: { + objectId: country.id, fields: { - relationField: { - __op: 'Batch', - ops: [ - { - __op: 'AddRelation', - objects: [pointerValue1], - }, - { - __op: 'AddRelation', - objects: [pointerValue2], - }, - ], + company: { + createAndLink: { + name: 'imACompany2', + }, }, }, }, }); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + expect(result.objectId).toBeDefined(); + expect(result.company.objectId).toBeDefined(); + expect(result.company.name).toEqual('imACompany2'); + }); - const schema = await new Parse.Schema('MainClass').get(); - expect(schema.fields.relationField.type).toEqual('Relation'); - expect(schema.fields.relationField.targetClass).toEqual('SomeClass'); + 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(); - await apolloClient.mutate({ + 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 CreateMainObject($fields: CreateMainClassFieldsInput) { - createMainClass(fields: $fields) { + mutation CreateCountry($fields: CreateCountryFieldsInput) { + createCountry(fields: $fields) { objectId + name + companies { + results { + objectId + name + teams { + results { + objectId + name + } + } + } + } } } `, variables: { fields: { - relationField: { - _op: 'Batch', - ops: [ - { - _op: 'AddRelation', - objects: [pointerValue1], - }, + name: 'imACountry2', + companies: { + createAndAdd: [ { - _op: 'RemoveRelation', - objects: [pointerValue1], + name: 'imACompany2', + teams: { + createAndAdd: { + name: 'imATeam2', + }, + }, }, { - _op: 'AddRelation', - objects: [pointerValue2], + name: 'imACompany3', + teams: { + createAndAdd: { + name: 'imATeam3', + }, + }, }, ], }, @@ -5039,76 +5166,232 @@ describe('ParseGraphQLServer', () => { }, }); - const getResult = await apolloClient.query({ + 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 GetMainObject($objectId: ID!) { - get(className: "MainClass", objectId: $objectId) - mainClass(objectId: $objectId) { - relationField { + query getCountry($objectId: ID!) { + country(objectId: $objectId) { + objectId + companies { results { objectId - createdAt + name } - count } } } `, variables: { - objectId: createResult.data.create.objectId, + objectId: country.id, }, }); - 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); + 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(); - const findResult = await apolloClient.query({ + // With where + const { + data: { country: result2 }, + } = await apolloClient.query({ query: gql` - query FindSomeObjects($where: Object) { - find(className: "SomeClass", where: $where) { - results + query getCountry($objectId: ID!, $where: CompanyWhereInput) { + country(objectId: $objectId) { + objectId + companies(where: $where) { + results { + objectId + name + } + } } } `, variables: { + objectId: country.id, where: { - $relatedTo: { - object: { - __type: 'Pointer', - className: 'MainClass', - objectId: createResult.data.create.objectId, - }, - key: 'relationField', - }, + name: { _eq: 'imACompany1' }, }, }, }); - - const compare = (obj1, obj2) => - obj1.createdAt > obj2.createdAt ? 1 : -1; - - 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(), - }, - { - objectId: someObject2.id, - createdAt: someObject2.createdAt.toISOString(), - updatedAt: someObject2.updatedAt.toISOString(), - }, - ].sort(compare) - ); + 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 () => { diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index a7f7f78806..fa917d24fa 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -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, }; diff --git a/src/GraphQL/loaders/objectsMutations.js b/src/GraphQL/loaders/objectsMutations.js index 9250ed4c53..208420c212 100644 --- a/src/GraphQL/loaders/objectsMutations.js +++ b/src/GraphQL/loaders/objectsMutations.js @@ -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, diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 32e31c1e72..7f95d9e538 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,18 +1,17 @@ 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'; +import { transformTypes } from '../transformers/mutation'; -const getParseClassMutationConfig = function( - parseClassConfig: ?ParseGraphQLClassConfig -) { - return (parseClassConfig && parseClassConfig.mutation) || {}; -}; - +// TODO: Check if include can contain ".", should always get const getOnlyRequiredFields = ( updatedFields, selectedFieldsString, @@ -55,43 +54,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 +72,20 @@ const load = function( let { fields } = args; if (!fields) fields = {}; const { config, auth, info } = context; - transformTypes('create', fields); + let parseFields; + try { + parseFields = await transformTypes('create', fields, { + className, + parseGraphQLSchema, + req: { config, auth, info }, + }); + } catch (e) { + console.log(e); + throw e; + } const createdObject = await objectsMutations.createObject( className, - fields, + parseFields, config, auth, info @@ -171,13 +143,22 @@ const load = function( try { const { objectId, fields } = args; const { config, auth, info } = context; - - transformTypes('update', fields); + let parseFields; + try { + parseFields = await transformTypes('update', fields, { + className, + parseGraphQLSchema, + req: { config, auth, info }, + }); + } catch (e) { + console.log(e); + throw e; + } const updatedObject = await objectsMutations.updateObject( className, objectId, - fields, + parseFields, config, auth, info diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index fb1de5bf30..ca7bd7da34 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -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') { diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index 5f03e057d4..f228c7488d 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -39,3 +39,9 @@ export const extractKeysAndInclude = selectedFields => { } return { keys, include }; }; + +export const getParseClassMutationConfig = function( + parseClassConfig: ?ParseGraphQLClassConfig +) { + return (parseClassConfig && parseClassConfig.mutation) || {}; +}; diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 16b306e0f6..998205d9c1 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -1,21 +1,194 @@ -const parseMap = { - _op: '__op', -}; +import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; +import * as objectsMutations from '../loaders/objectsMutations'; + +// TODO: +// * Handle Add/Remove on Relation +// * Handle Link on Pointer +// * Handle deep nestedMutation on Pointer and link -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; + return fields; +}; + +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 = { + // Always batch, simplify code, no performance drawbacks + __op: 'Batch', + ops: [], + }; + let nestedObjectsToAdd = []; + + if (Object.keys(value).length > 1) { + op['__op'] = 'Batch'; + } + + 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 (typeof fieldValue === 'object') { - transformMutationInputToParse(fieldValue); + if (value.link && value.link.objectId) { + return { + __type: 'Pointer', + className: targetClass, + objectId: value.link.objectId, + }; } - }); + }, }; -export { transformMutationInputToParse }; +export { transformTypes }; From 64031d3990e8a68b4b4c8ca736268e5a5c3e92c0 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Tue, 20 Aug 2019 20:56:26 +0200 Subject: [PATCH 3/6] Fix lint --- src/GraphQL/parseGraphQLUtils.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index f228c7488d..9d75fb3e7a 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -40,8 +40,6 @@ export const extractKeysAndInclude = selectedFields => { return { keys, include }; }; -export const getParseClassMutationConfig = function( - parseClassConfig: ?ParseGraphQLClassConfig -) { +export const getParseClassMutationConfig = function(parseClassConfig) { return (parseClassConfig && parseClassConfig.mutation) || {}; }; From bb64965fb31b96be468b4514ceb820b43ae143c9 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 21 Aug 2019 10:06:37 +0200 Subject: [PATCH 4/6] Review --- src/GraphQL/loaders/parseClassMutations.js | 36 ++++++++-------------- src/GraphQL/transformers/mutation.js | 6 ---- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 7f95d9e538..78ccccd05b 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -11,7 +11,6 @@ import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLControlle import { transformClassNameToGraphQL } from '../transformers/className'; import { transformTypes } from '../transformers/mutation'; -// TODO: Check if include can contain ".", should always get const getOnlyRequiredFields = ( updatedFields, selectedFieldsString, @@ -72,17 +71,13 @@ const load = function( let { fields } = args; if (!fields) fields = {}; const { config, auth, info } = context; - let parseFields; - try { - parseFields = await transformTypes('create', fields, { - className, - parseGraphQLSchema, - req: { config, auth, info }, - }); - } catch (e) { - console.log(e); - throw e; - } + + const parseFields = await transformTypes('create', fields, { + className, + parseGraphQLSchema, + req: { config, auth, info }, + }); + const createdObject = await objectsMutations.createObject( className, parseFields, @@ -143,17 +138,12 @@ const load = function( try { const { objectId, fields } = args; const { config, auth, info } = context; - let parseFields; - try { - parseFields = await transformTypes('update', fields, { - className, - parseGraphQLSchema, - req: { config, auth, info }, - }); - } catch (e) { - console.log(e); - throw e; - } + + const parseFields = await transformTypes('update', fields, { + className, + parseGraphQLSchema, + req: { config, auth, info }, + }); const updatedObject = await objectsMutations.updateObject( className, diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 998205d9c1..9182b018d2 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -1,11 +1,6 @@ import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; import * as objectsMutations from '../loaders/objectsMutations'; -// TODO: -// * Handle Add/Remove on Relation -// * Handle Link on Pointer -// * Handle deep nestedMutation on Pointer and link - const transformTypes = async ( inputType: 'create' | 'update', fields, @@ -91,7 +86,6 @@ const transformers = { ); const op = { - // Always batch, simplify code, no performance drawbacks __op: 'Batch', ops: [], }; From c2e7fad1d45e3013260088ce5d69df0f279486df Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 21 Aug 2019 10:13:29 +0200 Subject: [PATCH 5/6] Remove unnecessary code --- src/GraphQL/transformers/mutation.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 9182b018d2..0b015e18b1 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -91,10 +91,6 @@ const transformers = { }; let nestedObjectsToAdd = []; - if (Object.keys(value).length > 1) { - op['__op'] = 'Batch'; - } - if (value.createAndAdd) { nestedObjectsToAdd = (await Promise.all( value.createAndAdd.map(async input => { From b09f69ac569d1be814c54e6f87cf041f85b03c13 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 21 Aug 2019 22:45:48 +0200 Subject: [PATCH 6/6] Fix objectId on update --- spec/ParseGraphQLServer.spec.js | 35 ++++++++++++++++++++++ src/GraphQL/loaders/parseClassMutations.js | 7 ++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 5a4918f3b4..a8efe5f696 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -3419,6 +3419,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(); diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 78ccccd05b..39cee84d9e 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -176,7 +176,12 @@ const load = function( info ); } - return { ...updatedObject, ...fields, ...optimizedObject }; + return { + objectId: objectId, + ...updatedObject, + ...fields, + ...optimizedObject, + }; } catch (e) { parseGraphQLSchema.handleError(e); }