From 7cdc3c922c47df059e9249553c10150374bc4b90 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 14:37:24 -0700 Subject: [PATCH 01/27] Install graphql-relay --- package-lock.json | 11 +++++++++-- package.json | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b37751857..3758b0c326 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5892,6 +5892,14 @@ "resolved": "https://registry.npmjs.org/graphql-list-fields/-/graphql-list-fields-2.0.2.tgz", "integrity": "sha512-9TSAwcVA3KWw7JWYep5NCk2aw3wl1ayLtbMpmG7l26vh1FZ+gZexNPP+XJfUFyJa71UU0zcKSgtgpsrsA3Xv9Q==" }, + "graphql-relay": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/graphql-relay/-/graphql-relay-0.6.0.tgz", + "integrity": "sha512-OVDi6C9/qOT542Q3KxZdXja3NrDvqzbihn1B44PH8P/c5s0Q90RyQwT6guhGqXqbYEH6zbeLJWjQqiYvcg2vVw==", + "requires": { + "prettier": "^1.16.0" + } + }, "graphql-subscriptions": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz", @@ -9267,8 +9275,7 @@ "prettier": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", - "dev": true + "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==" }, "private": { "version": "0.1.8", diff --git a/package.json b/package.json index ae8bd410b3..f9d4e80701 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "follow-redirects": "1.9.0", "graphql": "14.5.7", "graphql-list-fields": "2.0.2", + "graphql-relay": "^0.6.0", "graphql-tools": "^4.0.5", "graphql-upload": "8.0.7", "intersect": "1.0.1", From 12839869e39a0752487beabfa209c04cee210cb2 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 14:50:51 -0700 Subject: [PATCH 02/27] Add relayNodeInterface to ParseGraphQLSchema --- src/GraphQL/ParseGraphQLSchema.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 5a12189f65..919f2d2948 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -1,5 +1,10 @@ import Parse from 'parse/node'; -import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { + GraphQLSchema, + GraphQLObjectType, + DocumentNode, + GraphQLNamedType, +} from 'graphql'; import { mergeSchemas, SchemaDirectiveVisitor } from 'graphql-tools'; import requiredParameter from '../requiredParameter'; import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes'; @@ -48,7 +53,14 @@ class ParseGraphQLSchema { databaseController: DatabaseController; parseGraphQLController: ParseGraphQLController; parseGraphQLConfig: ParseGraphQLConfig; - graphQLCustomTypeDefs: any; + log: any; + appId: string; + graphQLCustomTypeDefs: ?( + | string + | GraphQLSchema + | DocumentNode + | GraphQLNamedType[] + ); constructor( params: { @@ -56,6 +68,12 @@ class ParseGraphQLSchema { parseGraphQLController: ParseGraphQLController, log: any, appId: string, + graphQLCustomTypeDefs: ?( + | string + | GraphQLSchema + | DocumentNode + | GraphQLNamedType[] + ), } = {} ) { this.parseGraphQLController = @@ -105,6 +123,7 @@ class ParseGraphQLSchema { this.graphQLSubscriptions = {}; this.graphQLSchemaDirectivesDefinitions = null; this.graphQLSchemaDirectives = {}; + this.relayNodeInterface = null; defaultGraphQLTypes.load(this); schemaTypes.load(this); From d2e30e8265bef5f2b8dce14b1fd4674977fa856c Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 16:10:34 -0700 Subject: [PATCH 03/27] Add support to global id --- spec/ParseGraphQLServer.spec.js | 151 ++++++++++++--------- src/GraphQL/ParseGraphQLSchema.js | 2 + src/GraphQL/loaders/defaultGraphQLTypes.js | 11 +- src/GraphQL/loaders/defaultRelaySchema.js | 51 +++++++ src/GraphQL/loaders/parseClassMutations.js | 22 ++- src/GraphQL/loaders/parseClassQueries.js | 12 +- src/GraphQL/loaders/parseClassTypes.js | 24 ++-- 7 files changed, 193 insertions(+), 80 deletions(-) create mode 100644 src/GraphQL/loaders/defaultRelaySchema.js diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index ccf583bdd1..478deb2369 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -621,7 +621,7 @@ describe('ParseGraphQLServer', () => { expect(classType.fields.map(field => field.name).sort()).toEqual([ 'ACL', 'createdAt', - 'id', + 'objectId', 'updatedAt', ]); }); @@ -1265,6 +1265,7 @@ describe('ParseGraphQLServer', () => { query GetSuperCar($id: ID!) { superCar(id: $id) { id + objectId engine doors price @@ -1283,6 +1284,7 @@ describe('ParseGraphQLServer', () => { query GetSuperCar($id: ID!) { superCar(id: $id) { id + objectId engine doors price @@ -1327,6 +1329,7 @@ describe('ParseGraphQLServer', () => { query GetSuperCar($id: ID!) { superCar(id: $id) { id + objectId } } `, @@ -1334,7 +1337,7 @@ describe('ParseGraphQLServer', () => { id: superCar.id, }, })).data.superCar; - expect(getSuperCar.id).toBe(superCar.id); + expect(getSuperCar.objectId).toBe(superCar.id); }); it('should only allow the supplied constraint fields for a class', async () => { @@ -2644,6 +2647,7 @@ describe('ParseGraphQLServer', () => { query GetCustomer($id: ID!) { customer(id: $id) { id + objectId someField createdAt updatedAt @@ -2655,7 +2659,7 @@ describe('ParseGraphQLServer', () => { }, })).data.customer; - expect(result.id).toEqual(obj.id); + expect(result.objectId).toEqual(obj.id); expect(result.someField).toEqual('someValue'); expect(new Date(result.createdAt)).toEqual(obj.createdAt); expect(new Date(result.updatedAt)).toEqual(obj.updatedAt); @@ -2685,10 +2689,10 @@ describe('ParseGraphQLServer', () => { query: gql` query GetCustomer($id: ID!) { customer(id: $id) { - id + objectId manyRelations { ... on Customer { - id + objectId someCustomerField arrayField { ... on Element { @@ -2697,7 +2701,7 @@ describe('ParseGraphQLServer', () => { } } ... on SomeClass { - id + objectId someClassField } } @@ -2711,14 +2715,14 @@ describe('ParseGraphQLServer', () => { }, })).data.customer; - expect(result.id).toEqual(obj3.id); + expect(result.objectId).toEqual(obj3.id); expect(result.manyRelations.length).toEqual(2); const customerSubObject = result.manyRelations.find( - o => o.id === obj1.id + o => o.objectId === obj1.id ); const someClassSubObject = result.manyRelations.find( - o => o.id === obj2.id + o => o.objectId === obj2.id ); expect(customerSubObject).toBeDefined(); @@ -2769,28 +2773,28 @@ describe('ParseGraphQLServer', () => { query: gql` query DeepComplexGraphQLQuery($id: ID!) { country(id: $id) { - id + objectId name companies { ... on Company { - id + objectId name employees { ... on Employee { - id + objectId name } } teams { ... on Team { - id + objectId name employees { ... on Employee { - id + objectId name country { - id + objectId name } } @@ -2808,33 +2812,33 @@ describe('ParseGraphQLServer', () => { })).data.country; const expectedResult = { - id: obj4.id, + objectId: obj4.id, name: 'imACountry', __typename: 'Country', companies: [ { - id: obj3.id, + objectId: obj3.id, name: 'imACompany', __typename: 'Company', employees: [ { - id: obj1.id, + objectId: obj1.id, name: 'imAnEmployee', __typename: 'Employee', }, ], teams: [ { - id: obj2.id, + objectId: obj2.id, name: 'imATeam', __typename: 'Team', employees: [ { - id: obj1.id, + objectId: obj1.id, name: 'imAnEmployee', __typename: 'Employee', country: { - id: obj4.id, + objectId: obj4.id, name: 'imACountry', __typename: 'Country', }, @@ -3265,7 +3269,7 @@ describe('ParseGraphQLServer', () => { query FindCustomer { customers { results { - id + objectId someField createdAt updatedAt @@ -3278,8 +3282,8 @@ describe('ParseGraphQLServer', () => { expect(result.data.customers.results.length).toEqual(2); result.data.customers.results.forEach(resultObj => { - const obj = resultObj.id === obj1.id ? obj1 : obj2; - expect(resultObj.id).toEqual(obj.id); + const obj = resultObj.objectId === obj1.id ? obj1 : obj2; + expect(resultObj.objectId).toEqual(obj.id); expect(resultObj.someField).toEqual(obj.get('someField')); expect(new Date(resultObj.createdAt)).toEqual(obj.createdAt); expect(new Date(resultObj.updatedAt)).toEqual(obj.updatedAt); @@ -3508,7 +3512,7 @@ describe('ParseGraphQLServer', () => { ) { fullTextSearchTests(where: $where) { results { - id + objectId } } } @@ -3531,9 +3535,9 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.fullTextSearchTests.results[0].id).toEqual( - obj.id - ); + expect( + result.data.fullTextSearchTests.results[0].objectId + ).toEqual(obj.id); } catch (e) { handleError(e); } @@ -4104,6 +4108,7 @@ describe('ParseGraphQLServer', () => { mutation CreateCustomer($fields: CreateCustomerFieldsInput) { createCustomer(fields: $fields) { id + objectId createdAt someField } @@ -4120,7 +4125,7 @@ describe('ParseGraphQLServer', () => { expect(result.data.createCustomer.someField).toEqual('someValue'); const customer = await new Parse.Query('Customer').get( - result.data.createCustomer.id + result.data.createCustomer.objectId ); expect(customer.createdAt).toEqual( @@ -4265,6 +4270,7 @@ describe('ParseGraphQLServer', () => { ) { updateCustomer(id: $id, fields: $fields) { id + objectId } } `, @@ -4276,7 +4282,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.updateCustomer.id).toEqual(obj.id); + expect(result.data.updateCustomer.objectId).toEqual(obj.id); await obj.fetch(); @@ -4655,6 +4661,7 @@ describe('ParseGraphQLServer', () => { mutation DeleteCustomer($id: ID!) { deleteCustomer(id: $id) { id + objectId someField1 someField2 } @@ -4665,7 +4672,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.deleteCustomer.id).toEqual(obj.id); + expect(result.data.deleteCustomer.objectId).toEqual(obj.id); expect(result.data.deleteCustomer.someField1).toEqual( 'someField1Value1' ); @@ -4690,7 +4697,7 @@ describe('ParseGraphQLServer', () => { $id: ID! ) { delete: delete${className}(id: $id) { - id + objectId } } `, @@ -4727,7 +4734,7 @@ describe('ParseGraphQLServer', () => { ); expect( (await deleteObject(object4.className, object4.id)).data.delete - ).toEqual({ id: object4.id, __typename: 'PublicClass' }); + ).toEqual({ objectId: object4.id, __typename: 'PublicClass' }); await expectAsync( object4.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); @@ -4735,7 +4742,7 @@ describe('ParseGraphQLServer', () => { (await deleteObject(object1.className, object1.id, { 'X-Parse-Master-Key': 'test', })).data.delete - ).toEqual({ id: object1.id, __typename: 'GraphQLClass' }); + ).toEqual({ objectId: object1.id, __typename: 'GraphQLClass' }); await expectAsync( object1.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); @@ -4743,7 +4750,7 @@ describe('ParseGraphQLServer', () => { (await deleteObject(object2.className, object2.id, { 'X-Parse-Session-Token': user2.getSessionToken(), })).data.delete - ).toEqual({ id: object2.id, __typename: 'GraphQLClass' }); + ).toEqual({ objectId: object2.id, __typename: 'GraphQLClass' }); await expectAsync( object2.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); @@ -4751,7 +4758,7 @@ describe('ParseGraphQLServer', () => { (await deleteObject(object3.className, object3.id, { 'X-Parse-Session-Token': user5.getSessionToken(), })).data.delete - ).toEqual({ id: object3.id, __typename: 'GraphQLClass' }); + ).toEqual({ objectId: object3.id, __typename: 'GraphQLClass' }); await expectAsync( object3.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); @@ -4769,7 +4776,7 @@ describe('ParseGraphQLServer', () => { $id: ID! ) { delete${className}(id: $id) { - id + objectId } } `, @@ -4807,7 +4814,7 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object4.className, object4.id)).data[ `delete${object4.className}` - ].id + ].objectId ).toEqual(object4.id); await expectAsync( object4.fetch({ useMasterKey: true }) @@ -4815,7 +4822,7 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object1.className, object1.id, { 'X-Parse-Master-Key': 'test', - })).data[`delete${object1.className}`].id + })).data[`delete${object1.className}`].objectId ).toEqual(object1.id); await expectAsync( object1.fetch({ useMasterKey: true }) @@ -4823,7 +4830,7 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object2.className, object2.id, { 'X-Parse-Session-Token': user2.getSessionToken(), - })).data[`delete${object2.className}`].id + })).data[`delete${object2.className}`].objectId ).toEqual(object2.id); await expectAsync( object2.fetch({ useMasterKey: true }) @@ -4831,7 +4838,7 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object3.className, object3.id, { 'X-Parse-Session-Token': user5.getSessionToken(), - })).data[`delete${object3.className}`].id + })).data[`delete${object3.className}`].objectId ).toEqual(object3.id); await expectAsync( object3.fetch({ useMasterKey: true }) @@ -4958,6 +4965,7 @@ describe('ParseGraphQLServer', () => { query GetCurrentUser { viewer { id + objectId sessionToken userFoo { bar @@ -4972,8 +4980,12 @@ describe('ParseGraphQLServer', () => { }, }); - const { id, sessionToken, userFoo: resultFoo } = result.data.viewer; - expect(id).toEqual(user.id); + const { + objectId, + sessionToken, + userFoo: resultFoo, + } = result.data.viewer; + expect(objectId).toEqual(user.id); expect(sessionToken).toBeDefined(); expect(resultFoo).toBeDefined(); expect(resultFoo.bar).toEqual('hello'); @@ -5852,8 +5864,10 @@ describe('ParseGraphQLServer', () => { mutation Create($fields: CreateCountryFieldsInput) { createCountry(fields: $fields) { id + objectId company { id + objectId name } } @@ -5868,7 +5882,7 @@ describe('ParseGraphQLServer', () => { }); expect(result.id).toBeDefined(); - expect(result.company.id).toEqual(company2.id); + expect(result.company.objectId).toEqual(company2.id); expect(result.company.name).toEqual('imACompany2'); }); @@ -5938,8 +5952,10 @@ describe('ParseGraphQLServer', () => { mutation Update($id: ID!, $fields: UpdateCountryFieldsInput) { updateCountry(id: $id, fields: $fields) { id + objectId company { id + objectId name } } @@ -5954,7 +5970,7 @@ describe('ParseGraphQLServer', () => { }); expect(result.id).toBeDefined(); - expect(result.company.id).toEqual(company2.id); + expect(result.company.objectId).toEqual(company2.id); expect(result.company.name).toEqual('imACompany2'); }); @@ -6022,10 +6038,12 @@ describe('ParseGraphQLServer', () => { mutation CreateCountry($fields: CreateCountryFieldsInput) { createCountry(fields: $fields) { id + objectId name companies { results { id + objectId name } } @@ -6054,7 +6072,7 @@ describe('ParseGraphQLServer', () => { expect(result.name).toEqual('imACountry2'); expect(result.companies.results.length).toEqual(3); expect( - result.companies.results.some(o => o.id === company.id) + result.companies.results.some(o => o.objectId === company.id) ).toBeTruthy(); expect( result.companies.results.some(o => o.name === 'imACompany2') @@ -6179,9 +6197,11 @@ describe('ParseGraphQLServer', () => { ) { updateCountry(id: $id, fields: $fields) { id + objectId companies { results { id + objectId name } } @@ -6204,16 +6224,16 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.id).toEqual(country.id); + expect(result.objectId).toEqual(country.id); expect(result.companies.results.length).toEqual(2); expect( - result.companies.results.some(o => o.id === company2.id) + 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.id === company1.id) + result.companies.results.some(o => o.objectId === company1.id) ).toBeFalsy(); } ); @@ -6308,9 +6328,11 @@ describe('ParseGraphQLServer', () => { query getCountry($id: ID!) { country(id: $id) { id + objectId companies { results { id + objectId name } count @@ -6323,13 +6345,13 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result1.id).toEqual(country.id); + expect(result1.objectId).toEqual(country.id); expect(result1.companies.results.length).toEqual(2); expect( - result1.companies.results.some(o => o.id === company1.id) + result1.companies.results.some(o => o.objectId === company1.id) ).toBeTruthy(); expect( - result1.companies.results.some(o => o.id === company2.id) + result1.companies.results.some(o => o.objectId === company2.id) ).toBeTruthy(); // With where @@ -6340,9 +6362,11 @@ describe('ParseGraphQLServer', () => { query getCountry($id: ID!, $where: CompanyWhereInput) { country(id: $id) { id + objectId companies(where: $where) { results { id + objectId name } } @@ -6356,9 +6380,9 @@ describe('ParseGraphQLServer', () => { }, }, }); - expect(result2.id).toEqual(country.id); + expect(result2.objectId).toEqual(country.id); expect(result2.companies.results.length).toEqual(1); - expect(result2.companies.results[0].id).toEqual(company1.id); + expect(result2.companies.results[0].objectId).toEqual(company1.id); }); it('should support files', async () => { @@ -7313,7 +7337,7 @@ describe('ParseGraphQLServer', () => { query: gql` query GetSomeObject($id: ID!) { get: user(id: $id) { - id + objectId } } `, @@ -7322,7 +7346,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(getResult.data.get.id).toEqual(user.id); + expect(getResult.data.get.objectId).toEqual(user.id); }); it('should support Installation class', async () => { @@ -7337,7 +7361,7 @@ describe('ParseGraphQLServer', () => { query: gql` query GetSomeObject($id: ID!) { get: installation(id: $id) { - id + objectId } } `, @@ -7346,7 +7370,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(getResult.data.get.id).toEqual(installation.id); + expect(getResult.data.get.objectId).toEqual(installation.id); }); it('should support Role class', async () => { @@ -7361,7 +7385,7 @@ describe('ParseGraphQLServer', () => { query: gql` query GetSomeObject($id: ID!) { get: role(id: $id) { - id + objectId } } `, @@ -7370,7 +7394,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(getResult.data.get.id).toEqual(role.id); + expect(getResult.data.get.objectId).toEqual(role.id); }); it('should support Session class', async () => { @@ -7387,6 +7411,7 @@ describe('ParseGraphQLServer', () => { query GetSomeObject($id: ID!) { get: session(id: $id) { id + objectId } } `, @@ -7400,7 +7425,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(getResult.data.get.id).toEqual(session.id); + expect(getResult.data.get.objectId).toEqual(session.id); }); it('should support Product class', async () => { @@ -7423,7 +7448,7 @@ describe('ParseGraphQLServer', () => { query: gql` query GetSomeObject($id: ID!) { get: product(id: $id) { - id + objectId } } `, @@ -7437,7 +7462,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(getResult.data.get.id).toEqual(product.id); + expect(getResult.data.get.objectId).toEqual(product.id); }); }); }); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 919f2d2948..801e77ea49 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -21,6 +21,7 @@ import { toGraphQLError } from './parseGraphQLUtils'; import * as schemaDirectives from './loaders/schemaDirectives'; import * as schemaTypes from './loaders/schemaTypes'; import { getFunctionNames } from '../triggers'; +import * as defaultRelaySchema from './loaders/defaultRelaySchema'; const RESERVED_GRAPHQL_TYPE_NAMES = [ 'String', @@ -126,6 +127,7 @@ class ParseGraphQLSchema { this.relayNodeInterface = null; defaultGraphQLTypes.load(this); + defaultRelaySchema.load(this); schemaTypes.load(this); this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach( diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index dc357d49ee..ec60020bb4 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -402,10 +402,15 @@ const CLASS_NAME_ATT = { type: new GraphQLNonNull(GraphQLString), }; +const GLOBAL_OR_OBJECT_ID_ATT = { + description: + 'This is the object id. You can use either the global or the object id.', + type: OBJECT_ID, +}; + const OBJECT_ID_ATT = { description: 'This is the object id.', type: OBJECT_ID, - resolve: ({ objectId }) => objectId, }; const CREATED_AT_ATT = { @@ -428,7 +433,7 @@ const INPUT_FIELDS = { }; const CREATE_RESULT_FIELDS = { - id: OBJECT_ID_ATT, + objectId: OBJECT_ID_ATT, createdAt: CREATED_AT_ATT, }; @@ -1073,7 +1078,6 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(POLYGON_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(FIND_RESULT, true); parseGraphQLSchema.addGraphQLType(ELEMENT, true); - parseGraphQLSchema.addGraphQLType(OBJECT_ID, true); }; export { @@ -1101,6 +1105,7 @@ export { POLYGON, OBJECT_ID, CLASS_NAME_ATT, + GLOBAL_OR_OBJECT_ID_ATT, OBJECT_ID_ATT, UPDATED_AT_ATT, CREATED_AT_ATT, diff --git a/src/GraphQL/loaders/defaultRelaySchema.js b/src/GraphQL/loaders/defaultRelaySchema.js new file mode 100644 index 0000000000..9e6aea1694 --- /dev/null +++ b/src/GraphQL/loaders/defaultRelaySchema.js @@ -0,0 +1,51 @@ +import { nodeDefinitions, fromGlobalId } from 'graphql-relay'; +import getFieldNames from 'graphql-list-fields'; +import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import * as objectsQueries from './objectsQueries'; +import { extractKeysAndInclude } from './parseClassTypes'; + +const GLOBAL_ID_ATT = { + description: 'This is the global id.', + type: defaultGraphQLTypes.OBJECT_ID, +}; + +const load = parseGraphQLSchema => { + const { nodeInterface, nodeField } = nodeDefinitions( + async (globalId, context, queryInfo) => { + try { + const { type, id } = fromGlobalId(globalId); + const { config, auth, info } = context; + const selectedFields = getFieldNames(queryInfo); + + const { keys, include } = extractKeysAndInclude(selectedFields); + + return { + className: type, + ...(await objectsQueries.getObject( + type, + id, + keys, + include, + undefined, + undefined, + config, + auth, + info + )), + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + obj => { + return parseGraphQLSchema.parseClassTypes[obj.className] + .classGraphQLOutputType; + } + ); + + parseGraphQLSchema.addGraphQLType(nodeInterface, true); + parseGraphQLSchema.relayNodeInterface = nodeInterface; + parseGraphQLSchema.addGraphQLQuery('node', nodeField, true); +}; + +export { GLOBAL_ID_ATT, load }; diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 4a3b87f89e..5945de7599 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,4 +1,5 @@ import { GraphQLNonNull } from 'graphql'; +import { fromGlobalId } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import { @@ -125,7 +126,7 @@ const load = function( parseGraphQLSchema.addGraphQLMutation(updateGraphQLMutationName, { description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${graphQLClassName} class.`, args: { - id: defaultGraphQLTypes.OBJECT_ID_ATT, + id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT, fields: { description: 'These are the fields used to update the object.', type: classGraphQLUpdateType || defaultGraphQLTypes.OBJECT, @@ -136,9 +137,16 @@ const load = function( ), async resolve(_source, args, context, mutationInfo) { try { - const { id, fields } = args; + let { id } = args; + const { fields } = args; const { config, auth, info } = context; + const globalIdObject = fromGlobalId(id); + + if (globalIdObject.type === className) { + id = globalIdObject.id; + } + const parseFields = await transformTypes('update', fields, { className, parseGraphQLSchema, @@ -194,18 +202,24 @@ const load = function( parseGraphQLSchema.addGraphQLMutation(deleteGraphQLMutationName, { description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${graphQLClassName} class.`, args: { - id: defaultGraphQLTypes.OBJECT_ID_ATT, + id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT, }, type: new GraphQLNonNull( classGraphQLOutputType || defaultGraphQLTypes.OBJECT ), async resolve(_source, args, context, mutationInfo) { try { - const { id } = args; + let { id } = args; const { config, auth, info } = context; const selectedFields = getFieldNames(mutationInfo); const { keys, include } = extractKeysAndInclude(selectedFields); + const globalIdObject = fromGlobalId(id); + + if (globalIdObject.type === className) { + id = globalIdObject.id; + } + let optimizedObject = {}; const splitedKeys = keys.split(','); if (splitedKeys.length > 1 || splitedKeys[0] !== 'id') { diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 63041ad94d..475e92dfad 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -1,4 +1,5 @@ import { GraphQLNonNull } from 'graphql'; +import { fromGlobalId } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import pluralize from 'pluralize'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; @@ -14,11 +15,18 @@ const getParseClassQueryConfig = function( }; const getQuery = async (className, _source, args, context, queryInfo) => { - const { id, options } = args; + let { id } = args; + const { options } = args; const { readPreference, includeReadPreference } = options || {}; const { config, auth, info } = context; const selectedFields = getFieldNames(queryInfo); + const globalIdObject = fromGlobalId(id); + + if (globalIdObject.type === className) { + id = globalIdObject.id; + } + const { keys, include } = extractKeysAndInclude(selectedFields); return await objectsQueries.getObject( @@ -58,7 +66,7 @@ const load = function( parseGraphQLSchema.addGraphQLQuery(getGraphQLQueryName, { description: `The ${getGraphQLQueryName} query can be used to get an object of the ${graphQLClassName} class by its id.`, args: { - id: defaultGraphQLTypes.OBJECT_ID_ATT, + id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT, options: defaultGraphQLTypes.READ_OPTIONS_ATT, }, type: new GraphQLNonNull( diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 99a9df0eef..e0a1206906 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -7,6 +7,7 @@ import { GraphQLNonNull, GraphQLEnumType, } from 'graphql'; +import { globalIdField } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from '../helpers/objectsQueries'; @@ -30,9 +31,7 @@ const getInputFieldsAndConstraints = function( parseClass, parseClassConfig: ?ParseGraphQLClassConfig ) { - const classFields = Object.keys(parseClass.fields) - .filter(field => field !== 'objectId') - .concat('id'); + const classFields = Object.keys(parseClass.fields).concat('id'); const { inputFields: allowedInputFields, outputFields: allowedOutputFields, @@ -48,8 +47,9 @@ const getInputFieldsAndConstraints = function( // All allowed customs fields const classCustomFields = classFields.filter(field => { - return !Object.keys(defaultGraphQLTypes.PARSE_OBJECT_FIELDS).includes( - field + return ( + !Object.keys(defaultGraphQLTypes.PARSE_OBJECT_FIELDS).includes(field) && + field !== 'id' ); }); @@ -370,6 +370,14 @@ const load = ( }; const classGraphQLOutputTypeName = `${graphQLClassName}`; + const interfaces = [ + defaultGraphQLTypes.PARSE_OBJECT, + parseGraphQLSchema.relayNodeInterface, + ]; + const parseObjectFields = { + id: globalIdField(className, obj => obj.objectId), + ...defaultGraphQLTypes.PARSE_OBJECT_FIELDS, + }; const outputFields = () => { return classOutputFields.reduce((fields, field) => { const type = transformOutputTypeToGraphQL( @@ -492,12 +500,12 @@ const load = ( } else { return fields; } - }, defaultGraphQLTypes.PARSE_OBJECT_FIELDS); + }, parseObjectFields); }; let classGraphQLOutputType = new GraphQLObjectType({ name: classGraphQLOutputTypeName, description: `The ${classGraphQLOutputTypeName} object type is used in operations that involve outputting objects of ${graphQLClassName} class.`, - interfaces: [defaultGraphQLTypes.PARSE_OBJECT], + interfaces, fields: outputFields, }); classGraphQLOutputType = parseGraphQLSchema.addGraphQLType( @@ -547,7 +555,7 @@ const load = ( const viewerType = new GraphQLObjectType({ name: 'Viewer', description: `The Viewer object type is used in operations that involve outputting the current user data.`, - interfaces: [defaultGraphQLTypes.PARSE_OBJECT], + interfaces, fields: () => ({ ...outputFields(), sessionToken: defaultGraphQLTypes.SESSION_TOKEN_ATT, From d75b3ec397116355bd7adfc21496625f211ce845 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 17:48:43 -0700 Subject: [PATCH 04/27] Add support to global id in other operations --- src/GraphQL/loaders/parseClassMutations.js | 13 ++++--- src/GraphQL/loaders/parseClassTypes.js | 12 +++---- src/GraphQL/transformers/mutation.js | 40 ++++++++++++++++------ src/GraphQL/transformers/query.js | 16 +++++++-- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 5945de7599..6b13bca1d2 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -92,7 +92,7 @@ const load = function( fields, keys, include, - ['id', 'createdAt', 'updatedAt'] + ['id', 'objectId', 'createdAt', 'updatedAt'] ); let optimizedObject = {}; if (needGet) { @@ -168,7 +168,7 @@ const load = function( fields, keys, include, - ['id', 'updatedAt'] + ['id', 'objectId', 'updatedAt'] ); let optimizedObject = {}; if (needGet) { @@ -185,7 +185,7 @@ const load = function( ); } return { - id, + objectId: id, ...updatedObject, ...fields, ...optimizedObject, @@ -222,7 +222,10 @@ const load = function( let optimizedObject = {}; const splitedKeys = keys.split(','); - if (splitedKeys.length > 1 || splitedKeys[0] !== 'id') { + if ( + splitedKeys.filter(key => !['id', 'objectId'].includes(key)) + .length > 0 + ) { optimizedObject = await objectsQueries.getObject( className, id, @@ -242,7 +245,7 @@ const load = function( auth, info ); - return { id, ...optimizedObject }; + return { objectId: id, ...optimizedObject }; } catch (e) { parseGraphQLSchema.handleError(e); } diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index e0a1206906..1e14e8cd97 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -209,7 +209,7 @@ const load = ( fields: () => { const fields = { link: { - description: `Link an existing object from ${graphQLClassName} class.`, + description: `Link an existing object from ${graphQLClassName} class. You can use either the global or the object id.`, type: GraphQLID, }, }; @@ -233,17 +233,17 @@ const load = ( fields: () => { const fields = { add: { - description: `Add an existing object from the ${graphQLClassName} class into the relation.`, + description: `Add existing objects from the ${graphQLClassName} class into the relation. You can use either the global or the object ids.`, type: new GraphQLList(defaultGraphQLTypes.OBJECT_ID), }, remove: { - description: `Remove an existing object from the ${graphQLClassName} class out of the relation.`, + description: `Remove existing objects from the ${graphQLClassName} class out of the relation. You can use either the global or the object ids.`, type: new GraphQLList(defaultGraphQLTypes.OBJECT_ID), }, }; if (isCreateEnabled) { fields['createAndAdd'] = { - description: `Create and add an object of the ${graphQLClassName} class into the relation.`, + description: `Create and add objects of the ${graphQLClassName} class into the relation.`, type: new GraphQLList(new GraphQLNonNull(classGraphQLCreateType)), }; } @@ -268,12 +268,12 @@ const load = ( notInQueryKey: defaultGraphQLTypes.notInQueryKey, inQuery: { description: - 'This is the inQuery operator to specify a constraint to select the objects where a field equals to any of the ids in the result of a different query.', + 'This is the inQuery operator to specify a constraint to select the objects where a field equals to any of the object ids in the result of a different query.', type: defaultGraphQLTypes.SUBQUERY_INPUT, }, notInQuery: { description: - 'This is the notInQuery operator to specify a constraint to select the objects where a field do not equal to any of the ids in the result of a different query.', + 'This is the notInQuery operator to specify a constraint to select the objects where a field do not equal to any of the object ids in the result of a different query.', type: defaultGraphQLTypes.SUBQUERY_INPUT, }, }, diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 4fc5f6275c..3fe29ef7e4 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -1,3 +1,4 @@ +import { fromGlobalId } from 'graphql-relay'; import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; import * as objectsMutations from '../helpers/objectsMutations'; @@ -116,11 +117,17 @@ const transformers = { if (value.add || nestedObjectsToAdd.length > 0) { if (!value.add) value.add = []; - value.add = value.add.map(input => ({ - __type: 'Pointer', - className: targetClass, - objectId: input, - })); + value.add = value.add.map(input => { + const globalIdObject = fromGlobalId(input); + if (globalIdObject.type === targetClass) { + input = globalIdObject.id; + } + return { + __type: 'Pointer', + className: targetClass, + objectId: input, + }; + }); op.ops.push({ __op: 'AddRelation', objects: [...value.add, ...nestedObjectsToAdd], @@ -130,11 +137,17 @@ const transformers = { if (value.remove) { op.ops.push({ __op: 'RemoveRelation', - objects: value.remove.map(input => ({ - __type: 'Pointer', - className: targetClass, - objectId: input, - })), + objects: value.remove.map(input => { + const globalIdObject = fromGlobalId(input); + if (globalIdObject.type === targetClass) { + input = globalIdObject.id; + } + return { + __type: 'Pointer', + className: targetClass, + objectId: input, + }; + }), }); } return op; @@ -172,10 +185,15 @@ const transformers = { }; } if (value.link) { + let objectId = value.link; + const globalIdObject = fromGlobalId(objectId); + if (globalIdObject.type === targetClass) { + objectId = globalIdObject.id; + } return { __type: 'Pointer', className: targetClass, - objectId: value.link, + objectId, }; } }, diff --git a/src/GraphQL/transformers/query.js b/src/GraphQL/transformers/query.js index e202b2a94f..6f1575f703 100644 --- a/src/GraphQL/transformers/query.js +++ b/src/GraphQL/transformers/query.js @@ -1,3 +1,5 @@ +import { fromGlobalId } from 'graphql-relay'; + const parseQueryMap = { id: 'objectId', OR: '$or', @@ -102,10 +104,15 @@ const transformQueryConstraintInputToParse = ( typeof fieldValue === 'string' ) { const { targetClass } = fields[parentFieldName]; + let objectId = fieldValue; + const globalIdObject = fromGlobalId(objectId); + if (globalIdObject.type === targetClass) { + objectId = globalIdObject.id; + } constraints[fieldName] = { __type: 'Pointer', className: targetClass, - objectId: fieldValue, + objectId, }; } } @@ -198,7 +205,12 @@ const transformQueryInputToParse = (constraints, fields) => { } if (typeof fieldValue === 'object') { - transformQueryConstraintInputToParse(fieldValue, fields, fieldName, constraints); + transformQueryConstraintInputToParse( + fieldValue, + fields, + fieldName, + constraints + ); } }); }; From 8edc4808123db5fbbb19f11d7d2748d46d180d34 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 18:29:50 -0700 Subject: [PATCH 05/27] Fix sort by glboal id --- src/GraphQL/loaders/parseClassTypes.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 1e14e8cd97..afea55012b 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -339,11 +339,12 @@ const load = ( const updatedSortFields = { ...sortFields, }; + const value = field === 'id' ? 'objectId' : field; if (asc) { - updatedSortFields[`${field}_ASC`] = { value: field }; + updatedSortFields[`${field}_ASC`] = { value }; } if (desc) { - updatedSortFields[`${field}_DESC`] = { value: `-${field}` }; + updatedSortFields[`${field}_DESC`] = { value: `-${value}` }; } return updatedSortFields; }, {}), From 94e00532636a655a8ffcb32315640a3b13190396 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 18:53:02 -0700 Subject: [PATCH 06/27] Fix where by global id --- src/GraphQL/helpers/objectsQueries.js | 2 +- src/GraphQL/transformers/query.js | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/GraphQL/helpers/objectsQueries.js b/src/GraphQL/helpers/objectsQueries.js index b63e38eb25..423e998906 100644 --- a/src/GraphQL/helpers/objectsQueries.js +++ b/src/GraphQL/helpers/objectsQueries.js @@ -68,7 +68,7 @@ const findObjects = async ( if (!where) { where = {}; } - transformQueryInputToParse(where, fields); + transformQueryInputToParse(where, fields, className); const options = {}; diff --git a/src/GraphQL/transformers/query.js b/src/GraphQL/transformers/query.js index 6f1575f703..a417ef4c5d 100644 --- a/src/GraphQL/transformers/query.js +++ b/src/GraphQL/transformers/query.js @@ -183,7 +183,7 @@ const transformQueryConstraintInputToParse = ( }); }; -const transformQueryInputToParse = (constraints, fields) => { +const transformQueryInputToParse = (constraints, fields, className) => { if (!constraints || typeof constraints !== 'object') { return; } @@ -198,9 +198,30 @@ const transformQueryInputToParse = (constraints, fields) => { if (fieldName !== 'objectId') { fieldValue.forEach(fieldValueItem => { - transformQueryInputToParse(fieldValueItem, fields); + transformQueryInputToParse(fieldValueItem, fields, className); }); return; + } else if (className) { + Object.keys(fieldValue).forEach(constraintName => { + const constraintValue = fieldValue[constraintName]; + if (typeof constraintValue === 'string') { + const globalIdObject = fromGlobalId(constraintValue); + + if (globalIdObject.type === className) { + fieldValue[constraintName] = globalIdObject.id; + } + } else if (Array.isArray(constraintValue)) { + fieldValue[constraintName] = constraintValue.map(value => { + const globalIdObject = fromGlobalId(value); + + if (globalIdObject.type === className) { + return globalIdObject.id; + } + + return value; + }); + } + }); } } From f1080515cab5a90b967b143f790af578eb9f8ecf Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 20:59:51 -0700 Subject: [PATCH 07/27] Introduce IdWhereInput --- src/GraphQL/loaders/defaultGraphQLTypes.js | 21 +++++++++++++++++++++ src/GraphQL/loaders/parseClassTypes.js | 3 ++- src/GraphQL/transformers/constraintType.js | 7 ++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index ec60020bb4..d1b6d1397a 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -744,6 +744,25 @@ const options = { type: GraphQLString, }; +const ID_WHERE_INPUT = new GraphQLInputObjectType({ + name: 'IdWhereInput', + description: + 'The IdWhereInput input type is used in operations that involve filtering objects by an id.', + fields: { + equalTo: equalTo(GraphQLID), + notEqualTo: notEqualTo(GraphQLID), + lessThan: lessThan(GraphQLID), + lessThanOrEqualTo: lessThanOrEqualTo(GraphQLID), + greaterThan: greaterThan(GraphQLID), + greaterThanOrEqualTo: greaterThanOrEqualTo(GraphQLID), + in: inOp(GraphQLID), + notIn: notIn(GraphQLID), + exists, + inQueryKey, + notInQueryKey, + }, +}); + const STRING_WHERE_INPUT = new GraphQLInputObjectType({ name: 'StringWhereInput', description: @@ -1065,6 +1084,7 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(CENTER_SPHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_WITHIN_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_INTERSECTS_INPUT, true); + parseGraphQLSchema.addGraphQLType(ID_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(STRING_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(NUMBER_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(BOOLEAN_WHERE_INPUT, true); @@ -1148,6 +1168,7 @@ export { notInQueryKey, matchesRegex, options, + ID_WHERE_INPUT, STRING_WHERE_INPUT, NUMBER_WHERE_INPUT, BOOLEAN_WHERE_INPUT, diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index afea55012b..a733925f1c 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -298,7 +298,8 @@ const load = ( const type = transformConstraintTypeToGraphQL( parseClass.fields[parseField].type, parseClass.fields[parseField].targetClass, - parseGraphQLSchema.parseClassTypes + parseGraphQLSchema.parseClassTypes, + field ); if (type) { return { diff --git a/src/GraphQL/transformers/constraintType.js b/src/GraphQL/transformers/constraintType.js index 83c4e4b264..16f0b5d671 100644 --- a/src/GraphQL/transformers/constraintType.js +++ b/src/GraphQL/transformers/constraintType.js @@ -3,8 +3,13 @@ import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; const transformConstraintTypeToGraphQL = ( parseType, targetClass, - parseClassTypes + parseClassTypes, + fieldName ) => { + if (fieldName === 'id' || fieldName === 'objectId') { + return defaultGraphQLTypes.ID_WHERE_INPUT; + } + switch (parseType) { case 'String': return defaultGraphQLTypes.STRING_WHERE_INPUT; From b536f5b387ba922715b84f8ee6f875eb9d13bdf9 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 22 Sep 2019 21:37:59 -0700 Subject: [PATCH 08/27] Add Relay object identification tests --- spec/ParseGraphQLServer.spec.js | 662 +++++++++++++++++++++++++++++++- 1 file changed, 661 insertions(+), 1 deletion(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 478deb2369..4c82dfbd91 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -260,6 +260,7 @@ describe('ParseGraphQLServer', () => { describe('Auto API', () => { let httpServer; + let parseLiveQueryServer; const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-Javascript-Key': 'test', @@ -412,7 +413,7 @@ describe('ParseGraphQLServer', () => { const expressApp = express(); httpServer = http.createServer(expressApp); expressApp.use('/parse', parseServer.app); - ParseServer.createLiveQueryServer(httpServer, { + parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, { port: 1338, }); parseGraphQLServer.applyGraphQL(expressApp); @@ -455,6 +456,7 @@ describe('ParseGraphQLServer', () => { }); afterAll(async () => { + await parseLiveQueryServer.server.close(); await httpServer.close(); }); @@ -544,6 +546,13 @@ describe('ParseGraphQLServer', () => { }); describe('Schema', () => { + const resetGraphQLCache = async () => { + await Promise.all([ + parseGraphQLServer.parseGraphQLController.cacheController.graphQL.clear(), + parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(), + ]); + }; + describe('Default Types', () => { it('should have Object scalar type', async () => { const objectType = (await apolloClient.query({ @@ -714,6 +723,65 @@ describe('ParseGraphQLServer', () => { }); }); + describe('Relay Specific Types', () => { + beforeAll(async () => { + await resetGraphQLCache(); + }); + + afterAll(async () => { + await resetGraphQLCache(); + }); + + it('should have Node interface', async () => { + const schemaTypes = (await apolloClient.query({ + query: gql` + query SchemaTypes { + __schema { + types { + name + } + } + } + `, + })).data['__schema'].types.map(type => type.name); + + expect(schemaTypes).toContain('Node'); + }); + + it('should have node query', async () => { + const queryFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "Query") { + fields { + name + } + } + } + `, + })).data['__type'].fields.map(field => field.name); + + expect(queryFields).toContain('node'); + }); + + it('should return global id', async () => { + const userFields = (await apolloClient.query({ + query: gql` + query UserType { + __type(name: "User") { + fields { + name + } + } + } + `, + })).data['__type'].fields.map(field => field.name); + + expect(userFields).toContain('id'); + expect(userFields).toContain('objectId'); + }); + }); + describe('Parse Class Types', () => { it('should have all expected types', async () => { await parseServer.config.databaseController.loadSchema(); @@ -1560,6 +1628,598 @@ describe('ParseGraphQLServer', () => { }); }); + describe('Relay Spec', () => { + beforeAll(async () => { + await resetGraphQLCache(); + }); + + afterAll(async () => { + await resetGraphQLCache(); + }); + + describe('Object Identification', () => { + it('Class get custom method should return valid gobal id', async () => { + const obj = new Parse.Object('SomeClass'); + obj.set('someField', 'some value'); + await obj.save(); + + const getResult = await apolloClient.query({ + query: gql` + query GetSomeClass($objectId: ID!) { + someClass(id: $objectId) { + id + objectId + } + } + `, + variables: { + objectId: obj.id, + }, + }); + + expect(getResult.data.someClass.objectId).toBe(obj.id); + + const nodeResult = await apolloClient.query({ + query: gql` + query Node($id: ID!) { + node(id: $id) { + id + ... on SomeClass { + objectId + someField + } + } + } + `, + variables: { + id: getResult.data.someClass.id, + }, + }); + + expect(nodeResult.data.node.id).toBe(getResult.data.someClass.id); + expect(nodeResult.data.node.objectId).toBe(obj.id); + expect(nodeResult.data.node.someField).toBe('some value'); + }); + + it('Class find custom method should return valid gobal id', async () => { + const obj1 = new Parse.Object('SomeClass'); + obj1.set('someField', 'some value 1'); + await obj1.save(); + + const obj2 = new Parse.Object('SomeClass'); + obj2.set('someField', 'some value 2'); + await obj2.save(); + + const findResult = await apolloClient.query({ + query: gql` + query FindSomeClass { + someClasses(order: [createdAt_ASC]) { + results { + id + objectId + } + } + } + `, + }); + + expect(findResult.data.someClasses.results[0].objectId).toBe( + obj1.id + ); + expect(findResult.data.someClasses.results[1].objectId).toBe( + obj2.id + ); + + const nodeResult = await apolloClient.query({ + query: gql` + query Node($id1: ID!, $id2: ID!) { + node1: node(id: $id1) { + id + ... on SomeClass { + objectId + someField + } + } + node2: node(id: $id2) { + id + ... on SomeClass { + objectId + someField + } + } + } + `, + variables: { + id1: findResult.data.someClasses.results[0].id, + id2: findResult.data.someClasses.results[1].id, + }, + }); + + expect(nodeResult.data.node1.id).toBe( + findResult.data.someClasses.results[0].id + ); + expect(nodeResult.data.node1.objectId).toBe(obj1.id); + expect(nodeResult.data.node1.someField).toBe('some value 1'); + expect(nodeResult.data.node2.id).toBe( + findResult.data.someClasses.results[1].id + ); + expect(nodeResult.data.node2.objectId).toBe(obj2.id); + expect(nodeResult.data.node2.someField).toBe('some value 2'); + }); + + it('Id inputs should work either with global id or object id', async () => { + try { + await apolloClient.mutate({ + mutation: gql` + mutation CreateClasses { + secondaryObject: createClass( + name: "SecondaryObject" + schemaFields: { addStrings: [{ name: "someField" }] } + ) { + name + } + primaryObject: createClass( + name: "PrimaryObject" + schemaFields: { + addStrings: [{ name: "stringField" }] + addArrays: [{ name: "arrayField" }] + addPointers: [ + { + name: "pointerField" + targetClassName: "SecondaryObject" + } + ] + addRelations: [ + { + name: "relationField" + targetClassName: "SecondaryObject" + } + ] + } + ) { + name + } + } + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + await resetGraphQLCache(); + + const createSecondaryObjectsResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSecondaryObjects { + secondaryObject1: createSecondaryObject( + fields: { someField: "some value 1" } + ) { + id + objectId + someField + } + secondaryObject2: createSecondaryObject( + fields: { someField: "some value 2" } + ) { + id + someField + } + secondaryObject3: createSecondaryObject( + fields: { someField: "some value 3" } + ) { + objectId + someField + } + secondaryObject4: createSecondaryObject( + fields: { someField: "some value 4" } + ) { + id + objectId + } + secondaryObject5: createSecondaryObject( + fields: { someField: "some value 5" } + ) { + id + } + secondaryObject6: createSecondaryObject( + fields: { someField: "some value 6" } + ) { + objectId + } + secondaryObject7: createSecondaryObject( + fields: { someField: "some value 7" } + ) { + someField + } + } + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + const updateSecondaryObjectsResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSecondaryObjects( + $id1: ID! + $id2: ID! + $id3: ID! + $id4: ID! + $id5: ID! + $id6: ID! + ) { + secondaryObject1: updateSecondaryObject( + id: $id1 + fields: { someField: "some value 11" } + ) { + id + objectId + someField + } + secondaryObject2: updateSecondaryObject( + id: $id2 + fields: { someField: "some value 22" } + ) { + id + someField + } + secondaryObject3: updateSecondaryObject( + id: $id3 + fields: { someField: "some value 33" } + ) { + objectId + someField + } + secondaryObject4: updateSecondaryObject( + id: $id4 + fields: { someField: "some value 44" } + ) { + id + objectId + } + secondaryObject5: updateSecondaryObject( + id: $id5 + fields: { someField: "some value 55" } + ) { + id + } + secondaryObject6: updateSecondaryObject( + id: $id6 + fields: { someField: "some value 66" } + ) { + objectId + } + } + `, + variables: { + id1: createSecondaryObjectsResult.data.secondaryObject1.id, + id2: createSecondaryObjectsResult.data.secondaryObject2.id, + id3: + createSecondaryObjectsResult.data.secondaryObject3.objectId, + id4: + createSecondaryObjectsResult.data.secondaryObject4.objectId, + id5: createSecondaryObjectsResult.data.secondaryObject5.id, + id6: + createSecondaryObjectsResult.data.secondaryObject6.objectId, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + const deleteSecondaryObjectsResult = await apolloClient.mutate({ + mutation: gql` + mutation DeleteSecondaryObjects( + $id1: ID! + $id3: ID! + $id5: ID! + $id6: ID! + ) { + secondaryObject1: deleteSecondaryObject(id: $id1) { + id + objectId + someField + } + secondaryObject3: deleteSecondaryObject(id: $id3) { + objectId + someField + } + secondaryObject5: deleteSecondaryObject(id: $id5) { + id + } + secondaryObject6: deleteSecondaryObject(id: $id6) { + objectId + } + } + `, + variables: { + id1: updateSecondaryObjectsResult.data.secondaryObject1.id, + id3: + updateSecondaryObjectsResult.data.secondaryObject3.objectId, + id5: updateSecondaryObjectsResult.data.secondaryObject5.id, + id6: + updateSecondaryObjectsResult.data.secondaryObject6.objectId, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + const getSecondaryObjectsResult = await apolloClient.query({ + query: gql` + query GetSecondaryObjects($id2: ID!, $id4: ID!) { + secondaryObject2: secondaryObject(id: $id2) { + id + objectId + someField + } + secondaryObject4: secondaryObject(id: $id4) { + objectId + someField + } + } + `, + variables: { + id2: updateSecondaryObjectsResult.data.secondaryObject2.id, + id4: + updateSecondaryObjectsResult.data.secondaryObject4.objectId, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + const findSecondaryObjectsResult = await apolloClient.query({ + query: gql` + query FindSecondaryObjects( + $id1: ID! + $id2: ID! + $id3: ID! + $id4: ID! + $id5: ID! + $id6: ID! + ) { + secondaryObjects( + where: { + AND: [ + { + OR: [ + { id: { equalTo: $id2 } } + { + AND: [ + { id: { equalTo: $id4 } } + { objectId: { equalTo: $id4 } } + ] + } + ] + } + { id: { notEqualTo: $id1 } } + { id: { notEqualTo: $id3 } } + { objectId: { notEqualTo: $id2 } } + { objectId: { notIn: [$id5, $id6] } } + { id: { in: [$id2, $id4] } } + ] + } + order: [id_ASC, objectId_ASC] + ) { + results { + id + objectId + someField + } + count + } + } + `, + variables: { + id1: + deleteSecondaryObjectsResult.data.secondaryObject1.objectId, + id2: getSecondaryObjectsResult.data.secondaryObject2.id, + id3: + deleteSecondaryObjectsResult.data.secondaryObject3.objectId, + id4: getSecondaryObjectsResult.data.secondaryObject4.objectId, + id5: deleteSecondaryObjectsResult.data.secondaryObject5.id, + id6: + deleteSecondaryObjectsResult.data.secondaryObject6.objectId, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect( + findSecondaryObjectsResult.data.secondaryObjects.count + ).toEqual(2); + expect( + findSecondaryObjectsResult.data.secondaryObjects.results + .map(value => value.someField) + .sort() + ).toEqual(['some value 22', 'some value 44']); + expect( + findSecondaryObjectsResult.data.secondaryObjects.results[0].id + ).toBeLessThan( + findSecondaryObjectsResult.data.secondaryObjects.results[1].id + ); + expect( + findSecondaryObjectsResult.data.secondaryObjects.results[0] + .objectId + ).toBeLessThan( + findSecondaryObjectsResult.data.secondaryObjects.results[1] + .objectId + ); + + const createPrimaryObjectResult = await apolloClient.mutate({ + mutation: gql` + mutation CreatePrimaryObject( + $pointer: Any + $secondaryObject2: ID! + $secondaryObject4: ID! + ) { + createPrimaryObject( + fields: { + stringField: "some value" + arrayField: [1, "abc", $pointer] + pointerField: { link: $secondaryObject2 } + relationField: { + add: [$secondaryObject2, $secondaryObject4] + } + } + ) { + id + stringField + arrayField { + ... on Element { + value + } + ... on SecondaryObject { + someField + } + } + pointerField { + id + objectId + someField + } + relationField { + results { + id + objectId + someField + } + } + } + } + `, + variables: { + pointer: { + __type: 'Pointer', + className: 'SecondaryObject', + objectId: + getSecondaryObjectsResult.data.secondaryObject4.objectId, + }, + secondaryObject2: + getSecondaryObjectsResult.data.secondaryObject2.id, + secondaryObject4: + getSecondaryObjectsResult.data.secondaryObject4.objectId, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + const updatePrimaryObjectResult = await apolloClient.mutate({ + mutation: gql` + mutation UpdatePrimaryObject( + $id: ID! + $secondaryObject2: ID! + $secondaryObject4: ID! + ) { + updatePrimaryObject( + id: $id + fields: { + pointerField: { link: $secondaryObject4 } + relationField: { + remove: [$secondaryObject2, $secondaryObject4] + } + } + ) { + id + stringField + arrayField { + ... on Element { + value + } + ... on SecondaryObject { + someField + } + } + pointerField { + id + objectId + someField + } + relationField { + results { + id + objectId + someField + } + } + } + } + `, + variables: { + id: createPrimaryObjectResult.data.createPrimaryObject.id, + secondaryObject2: + getSecondaryObjectsResult.data.secondaryObject2.id, + secondaryObject4: + getSecondaryObjectsResult.data.secondaryObject4.objectId, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + expect( + createPrimaryObjectResult.data.createPrimaryObject.stringField + ).toEqual('some value'); + expect( + createPrimaryObjectResult.data.createPrimaryObject.arrayField + ).toEqual([ + { __typename: 'Element', value: 1 }, + { __typename: 'Element', value: 'abc' }, + { __typename: 'SecondaryObject', someField: 'some value 44' }, + ]); + expect( + createPrimaryObjectResult.data.createPrimaryObject.pointerField + .someField + ).toEqual('some value 22'); + expect( + createPrimaryObjectResult.data.createPrimaryObject.relationField.results + .map(value => value.someField) + .sort() + ).toEqual(['some value 22', 'some value 44']); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject.stringField + ).toEqual('some value'); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject.arrayField + ).toEqual([ + { __typename: 'Element', value: 1 }, + { __typename: 'Element', value: 'abc' }, + { __typename: 'SecondaryObject', someField: 'some value 44' }, + ]); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject.pointerField + .someField + ).toEqual('some value 44'); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject.relationField + .results + ).toEqual([]); + } catch (e) { + handleError(e); + } + }); + }); + }); + describe('Class Schema Mutations', () => { it('should create a new class', async () => { try { From dd0d4519d06e85214f6dc8d34dc15a7fa4c0e339 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 11:02:32 -0700 Subject: [PATCH 09/27] Client mutation id on createFile mutation --- spec/ParseGraphQLServer.spec.js | 101 ++++++++++++++++---- src/GraphQL/ParseGraphQLSchema.js | 2 + src/GraphQL/loaders/filesMutations.js | 127 ++++++++++++++------------ 3 files changed, 153 insertions(+), 77 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 4c82dfbd91..3a831cb6bc 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -17,6 +17,7 @@ const gql = require('graphql-tag'); const { ParseServer } = require('../'); const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer'); const ReadPreference = require('mongodb').ReadPreference; +const uuidv4 = require('uuid/v4'); function handleError(e) { if ( @@ -780,6 +781,45 @@ describe('ParseGraphQLServer', () => { expect(userFields).toContain('id'); expect(userFields).toContain('objectId'); }); + + it('should have clientMutationId in create file input', async () => { + const createFileInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFileInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(createFileInputFields).toEqual(['clientMutationId', 'upload']); + }); + + it('should have clientMutationId in create file payload', async () => { + const createFilePayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateFilePayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(createFilePayloadFields).toEqual([ + 'clientMutationId', + 'fileInfo', + ]); + }); }); describe('Parse Class Types', () => { @@ -5510,6 +5550,8 @@ describe('ParseGraphQLServer', () => { describe('Files Mutations', () => { describe('Create', () => { it('should return File object', async () => { + const clientMutationId = uuidv4(); + parseServer = await global.reconfigureServer({ publicServerURL: 'http://localhost:13377/parse', }); @@ -5519,19 +5561,28 @@ describe('ParseGraphQLServer', () => { 'operations', JSON.stringify({ query: ` - mutation CreateFile($upload: Upload!) { - createFile(upload: $upload) { - name - url + mutation CreateFile($input: CreateFileInput!) { + createFile(input: $input) { + clientMutationId + fileInfo { + name + url + } } } `, variables: { - upload: null, + input: { + clientMutationId, + upload: null, + }, }, }) ); - body.append('map', JSON.stringify({ 1: ['variables.upload'] })); + body.append( + 'map', + JSON.stringify({ 1: ['variables.input.upload'] }) + ); body.append('1', 'My File Content', { filename: 'myFileName.txt', contentType: 'text/plain', @@ -5547,14 +5598,17 @@ describe('ParseGraphQLServer', () => { const result = JSON.parse(await res.text()); - expect(result.data.createFile.name).toEqual( + expect(result.data.createFile.clientMutationId).toEqual( + clientMutationId + ); + expect(result.data.createFile.fileInfo.name).toEqual( jasmine.stringMatching(/_myFileName.txt$/) ); - expect(result.data.createFile.url).toEqual( + expect(result.data.createFile.fileInfo.url).toEqual( jasmine.stringMatching(/_myFileName.txt$/) ); - res = await fetch(result.data.createFile.url); + res = await fetch(result.data.createFile.fileInfo.url); expect(res.status).toEqual(200); expect(await res.text()).toEqual('My File Content'); @@ -7056,19 +7110,26 @@ describe('ParseGraphQLServer', () => { 'operations', JSON.stringify({ query: ` - mutation CreateFile($upload: Upload!) { - createFile(upload: $upload) { - name - url + mutation CreateFile($input: CreateFileInput!) { + createFile(input: $input) { + fileInfo { + name + url + } } } `, variables: { - upload: null, + input: { + upload: null, + }, }, }) ); - body.append('map', JSON.stringify({ 1: ['variables.upload'] })); + body.append( + 'map', + JSON.stringify({ 1: ['variables.input.upload'] }) + ); body.append('1', 'My File Content', { filename: 'myFileName.txt', contentType: 'text/plain', @@ -7084,14 +7145,14 @@ describe('ParseGraphQLServer', () => { const result = JSON.parse(await res.text()); - expect(result.data.createFile.name).toEqual( + expect(result.data.createFile.fileInfo.name).toEqual( jasmine.stringMatching(/_myFileName.txt$/) ); - expect(result.data.createFile.url).toEqual( + expect(result.data.createFile.fileInfo.url).toEqual( jasmine.stringMatching(/_myFileName.txt$/) ); - const someFieldValue = result.data.createFile.name; + const someFieldValue = result.data.createFile.fileInfo.name; await apolloClient.mutate({ mutation: gql` @@ -7180,10 +7241,10 @@ describe('ParseGraphQLServer', () => { expect(typeof getResult.data.someClass.someField).toEqual('object'); expect(getResult.data.someClass.someField.name).toEqual( - result.data.createFile.name + result.data.createFile.fileInfo.name ); expect(getResult.data.someClass.someField.url).toEqual( - result.data.createFile.url + result.data.createFile.fileInfo.url ); expect(getResult.data.findSomeClass1.results.length).toEqual(1); expect(getResult.data.findSomeClass2.results.length).toEqual(1); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 801e77ea49..1de8b03d93 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -33,6 +33,8 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [ 'Query', 'Mutation', 'Subscription', + 'CreateFileInput', + 'CreateFilePayload', 'Viewer', 'SignUpFieldsInput', 'LogInFieldsInput', diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 58aefa72b3..1772bc8667 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -1,80 +1,93 @@ import { GraphQLNonNull } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import { GraphQLUpload } from 'graphql-upload'; import Parse from 'parse/node'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import logger from '../../logger'; const load = parseGraphQLSchema => { - parseGraphQLSchema.addGraphQLMutation( - 'createFile', - { - description: - 'The create mutation can be used to create and upload a new file.', - args: { - upload: { - description: 'This is the new file to be created and uploaded', - type: new GraphQLNonNull(GraphQLUpload), - }, + const createMutation = mutationWithClientMutationId({ + name: 'CreateFile', + description: + 'The createFile mutation can be used to create and upload a new file.', + inputFields: { + upload: { + description: 'This is the new file to be created and uploaded.', + type: new GraphQLNonNull(GraphQLUpload), }, - type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO), - async resolve(_source, args, context) { - try { - const { upload } = args; - const { config } = context; + }, + outputFields: { + fileInfo: { + description: 'This is the created file info.', + type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO), + }, + }, + mutateAndGetPayload: async (args, context) => { + try { + const { upload } = args; + const { config } = context; - const { createReadStream, filename, mimetype } = await upload; - let data = null; - if (createReadStream) { - const stream = createReadStream(); - data = await new Promise((resolve, reject) => { - const chunks = []; - stream - .on('error', reject) - .on('data', chunk => chunks.push(chunk)) - .on('end', () => resolve(Buffer.concat(chunks))); - }); - } + const { createReadStream, filename, mimetype } = await upload; + let data = null; + if (createReadStream) { + const stream = createReadStream(); + data = await new Promise((resolve, reject) => { + const chunks = []; + stream + .on('error', reject) + .on('data', chunk => chunks.push(chunk)) + .on('end', () => resolve(Buffer.concat(chunks))); + }); + } - if (!data || !data.length) { - throw new Parse.Error( - Parse.Error.FILE_SAVE_ERROR, - 'Invalid file upload.' - ); - } + if (!data || !data.length) { + throw new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + 'Invalid file upload.' + ); + } - if (filename.length > 128) { - throw new Parse.Error( - Parse.Error.INVALID_FILE_NAME, - 'Filename too long.' - ); - } + if (filename.length > 128) { + throw new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'Filename too long.' + ); + } - if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { - throw new Parse.Error( - Parse.Error.INVALID_FILE_NAME, - 'Filename contains invalid characters.' - ); - } + if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { + throw new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'Filename contains invalid characters.' + ); + } - try { - return await config.filesController.createFile( + try { + return { + fileInfo: await config.filesController.createFile( config, filename, data, mimetype - ); - } catch (e) { - logger.error('Error creating a file: ', e); - throw new Parse.Error( - Parse.Error.FILE_SAVE_ERROR, - `Could not store file: ${filename}.` - ); - } + ), + }; } catch (e) { - parseGraphQLSchema.handleError(e); + logger.error('Error creating a file: ', e); + throw new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + `Could not store file: ${filename}.` + ); } - }, + } catch (e) { + parseGraphQLSchema.handleError(e); + } }, + }); + + parseGraphQLSchema.addGraphQLType(createMutation.args.input.type, true, true); + parseGraphQLSchema.addGraphQLType(createMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation( + 'createFile', + createMutation, true, true ); From 58b9dd940b23602ecbcb9d4c54e3112e355ae352 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 12:08:33 -0700 Subject: [PATCH 10/27] Client mutation id on callCloudCode mutation --- spec/ParseGraphQLServer.spec.js | 75 +++++++++++++++++++++-- src/GraphQL/ParseGraphQLSchema.js | 2 + src/GraphQL/loaders/filesMutations.js | 6 +- src/GraphQL/loaders/functionsMutations.js | 68 ++++++++++++-------- 4 files changed, 120 insertions(+), 31 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 3a831cb6bc..51732ed174 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -820,6 +820,53 @@ describe('ParseGraphQLServer', () => { 'fileInfo', ]); }); + + it('should have clientMutationId in call function input', async () => { + Parse.Cloud.define('hello', () => {}); + + const callFunctionInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallCloudCodeInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(callFunctionInputFields).toEqual([ + 'clientMutationId', + 'functionName', + 'params', + ]); + }); + + it('should have clientMutationId in call function payload', async () => { + Parse.Cloud.define('hello', () => {}); + + const callFunctionPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CallCloudCodePayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(callFunctionPayloadFields).toEqual([ + 'clientMutationId', + 'result', + ]); + }); }); describe('Parse Class Types', () => { @@ -5923,19 +5970,33 @@ describe('ParseGraphQLServer', () => { describe('Functions Mutations', () => { it('can be called', async () => { try { + const clientMutationId = uuidv4(); + Parse.Cloud.define('hello', async () => { return 'Hello world!'; }); const result = await apolloClient.mutate({ mutation: gql` - mutation CallFunction { - callCloudCode(functionName: hello) + mutation CallFunction($input: CallCloudCodeInput!) { + callCloudCode(input: $input) { + clientMutationId + result + } } `, + variables: { + input: { + clientMutationId, + functionName: 'hello', + }, + }, }); - expect(result.data.callCloudCode).toEqual('Hello world!'); + expect(result.data.callCloudCode.clientMutationId).toEqual( + clientMutationId + ); + expect(result.data.callCloudCode.result).toEqual('Hello world!'); } catch (e) { handleError(e); } @@ -5950,7 +6011,9 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CallFunction { - callCloudCode(functionName: hello) + callCloudCode(input: { functionName: hello }) { + result + } } `, }); @@ -6051,7 +6114,9 @@ describe('ParseGraphQLServer', () => { apolloClient.mutate({ mutation: gql` mutation CallFunction($params: Object) { - callCloudCode(functionName: hello, params: $params) + callCloudCode(input: { functionName: hello, params: $params }) { + result + } } `, variables: { diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 1de8b03d93..d9130ee12b 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -39,6 +39,8 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [ 'SignUpFieldsInput', 'LogInFieldsInput', 'CloudCodeFunction', + 'CallCloudCodeInput', + 'CallCloudCodePayload', ]; const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes']; const RESERVED_GRAPHQL_MUTATION_NAMES = [ diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index 1772bc8667..cf906a6898 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -83,7 +83,11 @@ const load = parseGraphQLSchema => { }, }); - parseGraphQLSchema.addGraphQLType(createMutation.args.input.type, true, true); + parseGraphQLSchema.addGraphQLType( + createMutation.args.input.type.ofType, + true, + true + ); parseGraphQLSchema.addGraphQLType(createMutation.type, true, true); parseGraphQLSchema.addGraphQLMutation( 'createFile', diff --git a/src/GraphQL/loaders/functionsMutations.js b/src/GraphQL/loaders/functionsMutations.js index 5c3f5d0659..418791583e 100644 --- a/src/GraphQL/loaders/functionsMutations.js +++ b/src/GraphQL/loaders/functionsMutations.js @@ -1,4 +1,5 @@ import { GraphQLNonNull, GraphQLEnumType } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import { FunctionsRouter } from '../../Routers/FunctionsRouter'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; @@ -21,28 +22,34 @@ const load = parseGraphQLSchema => { true ); - parseGraphQLSchema.addGraphQLMutation( - 'callCloudCode', - { - description: - 'The call mutation can be used to invoke a cloud code function.', - args: { - functionName: { - description: 'This is the function to be called.', - type: new GraphQLNonNull(cloudCodeFunctionEnum), - }, - params: { - description: 'These are the params to be passed to the function.', - type: defaultGraphQLTypes.OBJECT, - }, + const callCloudCodeMutation = mutationWithClientMutationId({ + name: 'CallCloudCode', + description: + 'The callCloudCode mutation can be used to invoke a cloud code function.', + inputFields: { + functionName: { + description: 'This is the function to be called.', + type: new GraphQLNonNull(cloudCodeFunctionEnum), + }, + params: { + description: 'These are the params to be passed to the function.', + type: defaultGraphQLTypes.OBJECT, }, - type: defaultGraphQLTypes.ANY, - async resolve(_source, args, context) { - try { - const { functionName, params } = args; - const { config, auth, info } = context; + }, + outputFields: { + result: { + description: + 'This is the result value of the cloud code function execution.', + type: defaultGraphQLTypes.ANY, + }, + }, + mutateAndGetPayload: async (args, context) => { + try { + const { functionName, params } = args; + const { config, auth, info } = context; - return (await FunctionsRouter.handleCloudFunction({ + return { + result: (await FunctionsRouter.handleCloudFunction({ params: { functionName, }, @@ -50,12 +57,23 @@ const load = parseGraphQLSchema => { auth, info, body: params, - })).response.result; - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + })).response.result, + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } }, + }); + + parseGraphQLSchema.addGraphQLType( + callCloudCodeMutation.args.input.type.ofType, + true, + true + ); + parseGraphQLSchema.addGraphQLType(callCloudCodeMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation( + 'callCloudCode', + callCloudCodeMutation, true, true ); From 337140f724e2bee95ab3393e9cc9579bd1d0e77a Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 15:26:56 -0700 Subject: [PATCH 11/27] Client mutation id on signUp mutation --- spec/ParseGraphQLServer.spec.js | 68 +++++++++++++++++++----- src/GraphQL/ParseGraphQLSchema.js | 3 +- src/GraphQL/loaders/parseClassTypes.js | 38 ++------------ src/GraphQL/loaders/usersMutations.js | 71 ++++++++++++++++---------- 4 files changed, 106 insertions(+), 74 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 51732ed174..71f315cca6 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -867,6 +867,42 @@ describe('ParseGraphQLServer', () => { 'result', ]); }); + + it('should have clientMutationId in sign up mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "SignUpInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual(['clientMutationId', 'userFields']); + }); + + it('should have clientMutationId in sign up mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "SignUpPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['clientMutationId', 'viewer']); + }); }); describe('Parse Class Types', () => { @@ -894,7 +930,6 @@ describe('ParseGraphQLServer', () => { 'User', 'UserWhereInput', 'UserFindResult', - 'SignUpFieldsInput', 'CreateUserFieldsInput', 'UpdateUserFieldsInput', ]; @@ -5755,31 +5790,39 @@ describe('ParseGraphQLServer', () => { describe('Users Mutations', () => { it('should sign user up', async () => { + const clientMutationId = uuidv4(); const userSchema = new Parse.Schema('_User'); userSchema.addString('someField'); await userSchema.update(); await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` - mutation SignUp($fields: SignUpFieldsInput) { - signUp(fields: $fields) { - sessionToken - someField + mutation SignUp($input: SignUpInput!) { + signUp(input: $input) { + clientMutationId + viewer { + sessionToken + someField + } } } `, variables: { - fields: { - username: 'user1', - password: 'user1', - someField: 'someValue', + input: { + clientMutationId, + userFields: { + username: 'user1', + password: 'user1', + someField: 'someValue', + }, }, }, }); - expect(result.data.signUp.sessionToken).toBeDefined(); - expect(result.data.signUp.someField).toEqual('someValue'); - expect(typeof result.data.signUp.sessionToken).toBe('string'); + expect(result.data.signUp.clientMutationId).toEqual(clientMutationId); + expect(result.data.signUp.viewer.sessionToken).toBeDefined(); + expect(result.data.signUp.viewer.someField).toEqual('someValue'); + expect(typeof result.data.signUp.viewer.sessionToken).toBe('string'); }); it('should log the user in', async () => { @@ -8347,6 +8390,7 @@ describe('ParseGraphQLServer', () => { variables: { user: { username: 'somefolk', + password: 'somepassword', }, }, }); diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index d9130ee12b..f63f6d7c6a 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -36,7 +36,8 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [ 'CreateFileInput', 'CreateFilePayload', 'Viewer', - 'SignUpFieldsInput', + 'SignUpInput', + 'SignUpPayload', 'LogInFieldsInput', 'CloudCodeFunction', 'CallCloudCodeInput', diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index a733925f1c..7b94808731 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -153,7 +153,11 @@ const load = ( ...fields, [field]: { description: `This is the object ${field}.`, - type, + type: + className === '_User' && + (field === 'username' || field === 'password') + ? new GraphQLNonNull(type) + : type, }, }; } else { @@ -566,35 +570,6 @@ const load = ( parseGraphQLSchema.viewerType = viewerType; parseGraphQLSchema.addGraphQLType(viewerType, true, true); - const userSignUpInputTypeName = 'SignUpFieldsInput'; - const userSignUpInputType = new GraphQLInputObjectType({ - name: userSignUpInputTypeName, - description: `The ${userSignUpInputTypeName} input type is used in operations that involve inputting objects of ${graphQLClassName} class when signing up.`, - fields: () => - classCreateFields.reduce((fields, field) => { - const type = transformInputTypeToGraphQL( - parseClass.fields[field].type, - parseClass.fields[field].targetClass, - parseGraphQLSchema.parseClassTypes - ); - if (type) { - return { - ...fields, - [field]: { - description: `This is the object ${field}.`, - type: - field === 'username' || field === 'password' - ? new GraphQLNonNull(type) - : type, - }, - }; - } else { - return fields; - } - }, {}), - }); - parseGraphQLSchema.addGraphQLType(userSignUpInputType, true, true); - const userLogInInputTypeName = 'LogInFieldsInput'; const userLogInInputType = new GraphQLInputObjectType({ name: userLogInInputTypeName, @@ -612,9 +587,6 @@ const load = ( }); parseGraphQLSchema.addGraphQLType(userLogInInputType, true, true); - parseGraphQLSchema.parseClassTypes[ - className - ].signUpInputType = userSignUpInputType; parseGraphQLSchema.parseClassTypes[ className ].logInInputType = userLogInInputType; diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index a4fb69faca..997c531350 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -1,4 +1,5 @@ import { GraphQLNonNull } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import UsersRouter from '../../Routers/UsersRouter'; import * as objectsMutations from '../helpers/objectsMutations'; import { getUserFromSessionToken } from './usersQueries'; @@ -10,42 +11,56 @@ const load = parseGraphQLSchema => { return; } - parseGraphQLSchema.addGraphQLMutation( - 'signUp', - { - description: 'The signUp mutation can be used to sign the user up.', - args: { - fields: { - descriptions: 'These are the fields of the user.', - type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType, - }, + const signUpMutation = mutationWithClientMutationId({ + name: 'SignUp', + description: + 'The signUp mutation can be used to create and sign up a new user.', + inputFields: { + userFields: { + descriptions: + 'These are the fields of the new user to be created and signed up.', + type: + parseGraphQLSchema.parseClassTypes['_User'].classGraphQLCreateType, }, - type: new GraphQLNonNull(parseGraphQLSchema.viewerType), - async resolve(_source, args, context, mutationInfo) { - try { - const { fields } = args; - - const { config, auth, info } = context; + }, + outputFields: { + viewer: { + description: + 'This is the new user that was created, signed up and returned as a viewer.', + type: new GraphQLNonNull(parseGraphQLSchema.viewerType), + }, + }, + mutateAndGetPayload: async (args, context, mutationInfo) => { + try { + const { userFields } = args; + const { config, auth, info } = context; - const { sessionToken } = await objectsMutations.createObject( - '_User', - fields, - config, - auth, - info - ); + const { sessionToken } = await objectsMutations.createObject( + '_User', + userFields, + config, + auth, + info + ); - info.sessionToken = sessionToken; + info.sessionToken = sessionToken; - return await getUserFromSessionToken(config, info, mutationInfo); - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + return { + viewer: await getUserFromSessionToken(config, info, mutationInfo), + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } }, + }); + + parseGraphQLSchema.addGraphQLType( + signUpMutation.args.input.type.ofType, true, true ); + parseGraphQLSchema.addGraphQLType(signUpMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation('signUp', signUpMutation, true, true); parseGraphQLSchema.addGraphQLMutation( 'logIn', From 9d689f128f14e13136f83832f94b34315eadc323 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 15:43:11 -0700 Subject: [PATCH 12/27] Client mutation id on logIn mutation --- spec/ParseGraphQLServer.spec.js | 76 +++++++++++++++++++----- src/GraphQL/ParseGraphQLSchema.js | 3 +- src/GraphQL/loaders/parseClassTypes.js | 21 ------- src/GraphQL/loaders/usersMutations.js | 80 ++++++++++++++++---------- 4 files changed, 113 insertions(+), 67 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 71f315cca6..e15d6590d4 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -903,6 +903,46 @@ describe('ParseGraphQLServer', () => { expect(payloadFields).toEqual(['clientMutationId', 'viewer']); }); + + it('should have clientMutationId in log in mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogInInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual([ + 'clientMutationId', + 'password', + 'username', + ]); + }); + + it('should have clientMutationId in log in mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogInPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['clientMutationId', 'viewer']); + }); }); describe('Parse Class Types', () => { @@ -5826,6 +5866,7 @@ describe('ParseGraphQLServer', () => { }); it('should log the user in', async () => { + const clientMutationId = uuidv4(); const user = new Parse.User(); user.setUsername('user1'); user.setPassword('user1'); @@ -5835,24 +5876,29 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` - mutation LogInUser($fields: LogInFieldsInput) { - logIn(fields: $fields) { - sessionToken - someField + mutation LogInUser($input: LogInInput!) { + logIn(input: $input) { + clientMutationId + viewer { + sessionToken + someField + } } } `, variables: { - fields: { + input: { + clientMutationId, username: 'user1', password: 'user1', }, }, }); - expect(result.data.logIn.sessionToken).toBeDefined(); - expect(result.data.logIn.someField).toEqual('someValue'); - expect(typeof result.data.logIn.sessionToken).toBe('string'); + expect(result.data.logIn.clientMutationId).toEqual(clientMutationId); + expect(result.data.logIn.viewer.sessionToken).toBeDefined(); + expect(result.data.logIn.viewer.someField).toEqual('someValue'); + expect(typeof result.data.logIn.viewer.sessionToken).toBe('string'); }); it('should log the user out', async () => { @@ -5864,21 +5910,23 @@ describe('ParseGraphQLServer', () => { const logIn = await apolloClient.mutate({ mutation: gql` - mutation LogInUser($fields: LogInFieldsInput) { - logIn(fields: $fields) { - sessionToken + mutation LogInUser($input: LogInInput!) { + logIn(input: $input) { + viewer { + sessionToken + } } } `, variables: { - fields: { + input: { username: 'user1', password: 'user1', }, }, }); - const sessionToken = logIn.data.logIn.sessionToken; + const sessionToken = logIn.data.logIn.viewer.sessionToken; const logOut = await apolloClient.mutate({ mutation: gql` @@ -5900,7 +5948,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.query({ query: gql` query GetCurrentUser { - me { + viewer { username } } diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index f63f6d7c6a..ae316f9edd 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -38,7 +38,8 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [ 'Viewer', 'SignUpInput', 'SignUpPayload', - 'LogInFieldsInput', + 'LogInInput', + 'LogInPayload', 'CloudCodeFunction', 'CallCloudCodeInput', 'CallCloudCodePayload', diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 7b94808731..06553739f4 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -569,27 +569,6 @@ const load = ( }); parseGraphQLSchema.viewerType = viewerType; parseGraphQLSchema.addGraphQLType(viewerType, true, true); - - const userLogInInputTypeName = 'LogInFieldsInput'; - const userLogInInputType = new GraphQLInputObjectType({ - name: userLogInInputTypeName, - description: `The ${userLogInInputTypeName} input type is used to login.`, - fields: { - username: { - description: 'This is the username used to log the user in.', - type: new GraphQLNonNull(GraphQLString), - }, - password: { - description: 'This is the password used to log the user in.', - type: new GraphQLNonNull(GraphQLString), - }, - }, - }); - parseGraphQLSchema.addGraphQLType(userLogInInputType, true, true); - - parseGraphQLSchema.parseClassTypes[ - className - ].logInInputType = userLogInInputType; } }; diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 997c531350..d107a146ca 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -1,4 +1,4 @@ -import { GraphQLNonNull } from 'graphql'; +import { GraphQLNonNull, GraphQLString } from 'graphql'; import { mutationWithClientMutationId } from 'graphql-relay'; import UsersRouter from '../../Routers/UsersRouter'; import * as objectsMutations from '../helpers/objectsMutations'; @@ -62,42 +62,60 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(signUpMutation.type, true, true); parseGraphQLSchema.addGraphQLMutation('signUp', signUpMutation, true, true); - parseGraphQLSchema.addGraphQLMutation( - 'logIn', - { - description: 'The logIn mutation can be used to log the user in.', - args: { - fields: { - description: 'This is data needed to login', - type: parseGraphQLSchema.parseClassTypes['_User'].logInInputType, - }, + const logInMutation = mutationWithClientMutationId({ + name: 'LogIn', + description: 'The logIn mutation can be used to log in an existing user.', + inputFields: { + username: { + description: 'This is the username used to log in the user.', + type: new GraphQLNonNull(GraphQLString), }, - type: new GraphQLNonNull(parseGraphQLSchema.viewerType), - async resolve(_source, args, context) { - try { - const { - fields: { username, password }, - } = args; - const { config, auth, info } = context; - - return (await usersRouter.handleLogIn({ - body: { - username, - password, - }, - query: {}, - config, - auth, - info, - })).response; - } catch (e) { - parseGraphQLSchema.handleError(e); - } + password: { + description: 'This is the password used to log in the user.', + type: new GraphQLNonNull(GraphQLString), + }, + }, + outputFields: { + viewer: { + description: + 'This is the existing user that was logged in and returned as a viewer.', + type: new GraphQLNonNull(parseGraphQLSchema.viewerType), }, }, + mutateAndGetPayload: async (args, context, mutationInfo) => { + try { + const { username, password } = args; + const { config, auth, info } = context; + + const { sessionToken } = (await usersRouter.handleLogIn({ + body: { + username, + password, + }, + query: {}, + config, + auth, + info, + })).response; + + info.sessionToken = sessionToken; + + return { + viewer: await getUserFromSessionToken(config, info, mutationInfo), + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }); + + parseGraphQLSchema.addGraphQLType( + logInMutation.args.input.type.ofType, true, true ); + parseGraphQLSchema.addGraphQLType(logInMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation('logIn', logInMutation, true, true); parseGraphQLSchema.addGraphQLMutation( 'logOut', From 0288ebb171cfbdf25c5328802f7799b41273db37 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 15:56:54 -0700 Subject: [PATCH 13/27] Client mutation id on logOut mutation --- spec/ParseGraphQLServer.spec.js | 54 ++++++++++++++++++++++-- src/GraphQL/ParseGraphQLSchema.js | 2 + src/GraphQL/loaders/usersMutations.js | 60 ++++++++++++++++----------- 3 files changed, 87 insertions(+), 29 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index e15d6590d4..4d86e6d2e1 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -943,6 +943,42 @@ describe('ParseGraphQLServer', () => { expect(payloadFields).toEqual(['clientMutationId', 'viewer']); }); + + it('should have clientMutationId in log out mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogOutInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual(['clientMutationId']); + }); + + it('should have clientMutationId in log out mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "LogOutPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['clientMutationId', 'viewer']); + }); }); describe('Parse Class Types', () => { @@ -5902,6 +5938,7 @@ describe('ParseGraphQLServer', () => { }); it('should log the user out', async () => { + const clientMutationId = uuidv4(); const user = new Parse.User(); user.setUsername('user1'); user.setPassword('user1'); @@ -5930,9 +5967,12 @@ describe('ParseGraphQLServer', () => { const logOut = await apolloClient.mutate({ mutation: gql` - mutation LogOutUser { - logOut { - sessionToken + mutation LogOutUser($input: LogOutInput!) { + logOut(input: $input) { + clientMutationId + viewer { + sessionToken + } } } `, @@ -5941,8 +5981,14 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Session-Token': sessionToken, }, }, + variables: { + input: { + clientMutationId, + }, + }, }); - expect(logOut.data.logOut).toBeDefined(); + expect(logOut.data.logOut.clientMutationId).toEqual(clientMutationId); + expect(logOut.data.logOut.viewer.sessionToken).toEqual(sessionToken); try { await apolloClient.query({ diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index ae316f9edd..3be689eb4f 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -40,6 +40,8 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [ 'SignUpPayload', 'LogInInput', 'LogInPayload', + 'LogOutInput', + 'LogOutPayload', 'CloudCodeFunction', 'CallCloudCodeInput', 'CallCloudCodePayload', diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index d107a146ca..5ff6b9b5f6 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -117,36 +117,46 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(logInMutation.type, true, true); parseGraphQLSchema.addGraphQLMutation('logIn', logInMutation, true, true); - parseGraphQLSchema.addGraphQLMutation( - 'logOut', - { - description: 'The logOut mutation can be used to log the user out.', - type: new GraphQLNonNull(parseGraphQLSchema.viewerType), - async resolve(_source, _args, context, mutationInfo) { - try { - const { config, auth, info } = context; - - const viewer = await getUserFromSessionToken( - config, - info, - mutationInfo - ); - - await usersRouter.handleLogOut({ - config, - auth, - info, - }); - - return viewer; - } catch (e) { - parseGraphQLSchema.handleError(e); - } + const logOutMutation = mutationWithClientMutationId({ + name: 'LogOut', + description: 'The logOut mutation can be used to log out an existing user.', + outputFields: { + viewer: { + description: + 'This is the existing user that was logged out and returned as a viewer.', + type: new GraphQLNonNull(parseGraphQLSchema.viewerType), }, }, + mutateAndGetPayload: async (_args, context, mutationInfo) => { + try { + const { config, auth, info } = context; + + const viewer = await getUserFromSessionToken( + config, + info, + mutationInfo + ); + + await usersRouter.handleLogOut({ + config, + auth, + info, + }); + + return { viewer }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } + }, + }); + + parseGraphQLSchema.addGraphQLType( + logOutMutation.args.input.type.ofType, true, true ); + parseGraphQLSchema.addGraphQLType(logOutMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation('logOut', logOutMutation, true, true); }; export { load }; From 204dfff92e8abf65c6603629cf12711dd1d06317 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 16:42:13 -0700 Subject: [PATCH 14/27] Client mutation id on createClass mutation --- spec/ParseGraphQLServer.spec.js | 803 +++++++++++++++---------- src/GraphQL/ParseGraphQLSchema.js | 6 + src/GraphQL/loaders/schemaMutations.js | 85 +-- 3 files changed, 540 insertions(+), 354 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 4d86e6d2e1..16aac6299a 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -979,6 +979,46 @@ describe('ParseGraphQLServer', () => { expect(payloadFields).toEqual(['clientMutationId', 'viewer']); }); + + it('should have clientMutationId in createClass mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateClassInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual([ + 'clientMutationId', + 'name', + 'schemaFields', + ]); + }); + + it('should have clientMutationId in createClass mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateClassPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['class', 'clientMutationId']); + }); }); describe('Parse Class Types', () => { @@ -1951,31 +1991,35 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation CreateClasses { secondaryObject: createClass( - name: "SecondaryObject" - schemaFields: { addStrings: [{ name: "someField" }] } + input: { + name: "SecondaryObject" + schemaFields: { addStrings: [{ name: "someField" }] } + } ) { - name + clientMutationId } primaryObject: createClass( - name: "PrimaryObject" - schemaFields: { - addStrings: [{ name: "stringField" }] - addArrays: [{ name: "arrayField" }] - addPointers: [ - { - name: "pointerField" - targetClassName: "SecondaryObject" - } - ] - addRelations: [ - { - name: "relationField" - targetClassName: "SecondaryObject" - } - ] + input: { + name: "PrimaryObject" + schemaFields: { + addStrings: [{ name: "stringField" }] + addArrays: [{ name: "arrayField" }] + addPointers: [ + { + name: "pointerField" + targetClassName: "SecondaryObject" + } + ] + addRelations: [ + { + name: "relationField" + targetClassName: "SecondaryObject" + } + ] + } } ) { - name + clientMutationId } } `, @@ -2424,156 +2468,197 @@ describe('ParseGraphQLServer', () => { const result = await apolloClient.mutate({ mutation: gql` mutation { - class1: createClass(name: "Class1") { - name - schemaFields { + class1: createClass( + input: { name: "Class1", clientMutationId: "cmid1" } + ) { + clientMutationId + class { name - __typename + schemaFields { + name + __typename + } } } - class2: createClass(name: "Class2", schemaFields: null) { - name - schemaFields { + class2: createClass( + input: { + name: "Class2" + schemaFields: null + clientMutationId: "cmid2" + } + ) { + clientMutationId + class { name - __typename + schemaFields { + name + __typename + } } } - class3: createClass(name: "Class3", schemaFields: {}) { - name - schemaFields { + class3: createClass( + input: { + name: "Class3" + schemaFields: {} + clientMutationId: "cmid3" + } + ) { + clientMutationId + class { name - __typename + schemaFields { + name + __typename + } } } class4: createClass( - name: "Class4" - schemaFields: { - addStrings: null - addNumbers: null - addBooleans: null - addArrays: null - addObjects: null - addDates: null - addFiles: null - addGeoPoint: null - addPolygons: null - addBytes: null - addPointers: null - addRelations: null + input: { + name: "Class4" + schemaFields: { + addStrings: null + addNumbers: null + addBooleans: null + addArrays: null + addObjects: null + addDates: null + addFiles: null + addGeoPoint: null + addPolygons: null + addBytes: null + addPointers: null + addRelations: null + } + clientMutationId: "cmid4" } ) { - name - schemaFields { + clientMutationId + class { name - __typename + schemaFields { + name + __typename + } } } class5: createClass( - name: "Class5" - schemaFields: { - addStrings: [] - addNumbers: [] - addBooleans: [] - addArrays: [] - addObjects: [] - addDates: [] - addFiles: [] - addPolygons: [] - addBytes: [] - addPointers: [] - addRelations: [] + input: { + name: "Class5" + schemaFields: { + addStrings: [] + addNumbers: [] + addBooleans: [] + addArrays: [] + addObjects: [] + addDates: [] + addFiles: [] + addPolygons: [] + addBytes: [] + addPointers: [] + addRelations: [] + } + clientMutationId: "cmid5" } ) { - name - schemaFields { + clientMutationId + class { name - __typename + schemaFields { + name + __typename + } } } class6: createClass( - name: "Class6" - schemaFields: { - addStrings: [ - { name: "stringField1" } - { name: "stringField2" } - { name: "stringField3" } - ] - addNumbers: [ - { name: "numberField1" } - { name: "numberField2" } - { name: "numberField3" } - ] - addBooleans: [ - { name: "booleanField1" } - { name: "booleanField2" } - { name: "booleanField3" } - ] - addArrays: [ - { name: "arrayField1" } - { name: "arrayField2" } - { name: "arrayField3" } - ] - addObjects: [ - { name: "objectField1" } - { name: "objectField2" } - { name: "objectField3" } - ] - addDates: [ - { name: "dateField1" } - { name: "dateField2" } - { name: "dateField3" } - ] - addFiles: [ - { name: "fileField1" } - { name: "fileField2" } - { name: "fileField3" } - ] - addGeoPoint: { name: "geoPointField" } - addPolygons: [ - { name: "polygonField1" } - { name: "polygonField2" } - { name: "polygonField3" } - ] - addBytes: [ - { name: "bytesField1" } - { name: "bytesField2" } - { name: "bytesField3" } - ] - addPointers: [ - { name: "pointerField1", targetClassName: "Class1" } - { name: "pointerField2", targetClassName: "Class6" } - { name: "pointerField3", targetClassName: "Class2" } - ] - addRelations: [ - { name: "relationField1", targetClassName: "Class1" } - { name: "relationField2", targetClassName: "Class6" } - { name: "relationField3", targetClassName: "Class2" } - ] - remove: [ - { name: "stringField3" } - { name: "numberField3" } - { name: "booleanField3" } - { name: "arrayField3" } - { name: "objectField3" } - { name: "dateField3" } - { name: "fileField3" } - { name: "polygonField3" } - { name: "bytesField3" } - { name: "pointerField3" } - { name: "relationField3" } - { name: "doesNotExist" } - ] + input: { + name: "Class6" + schemaFields: { + addStrings: [ + { name: "stringField1" } + { name: "stringField2" } + { name: "stringField3" } + ] + addNumbers: [ + { name: "numberField1" } + { name: "numberField2" } + { name: "numberField3" } + ] + addBooleans: [ + { name: "booleanField1" } + { name: "booleanField2" } + { name: "booleanField3" } + ] + addArrays: [ + { name: "arrayField1" } + { name: "arrayField2" } + { name: "arrayField3" } + ] + addObjects: [ + { name: "objectField1" } + { name: "objectField2" } + { name: "objectField3" } + ] + addDates: [ + { name: "dateField1" } + { name: "dateField2" } + { name: "dateField3" } + ] + addFiles: [ + { name: "fileField1" } + { name: "fileField2" } + { name: "fileField3" } + ] + addGeoPoint: { name: "geoPointField" } + addPolygons: [ + { name: "polygonField1" } + { name: "polygonField2" } + { name: "polygonField3" } + ] + addBytes: [ + { name: "bytesField1" } + { name: "bytesField2" } + { name: "bytesField3" } + ] + addPointers: [ + { name: "pointerField1", targetClassName: "Class1" } + { name: "pointerField2", targetClassName: "Class6" } + { name: "pointerField3", targetClassName: "Class2" } + ] + addRelations: [ + { name: "relationField1", targetClassName: "Class1" } + { name: "relationField2", targetClassName: "Class6" } + { name: "relationField3", targetClassName: "Class2" } + ] + remove: [ + { name: "stringField3" } + { name: "numberField3" } + { name: "booleanField3" } + { name: "arrayField3" } + { name: "objectField3" } + { name: "dateField3" } + { name: "fileField3" } + { name: "polygonField3" } + { name: "bytesField3" } + { name: "pointerField3" } + { name: "relationField3" } + { name: "doesNotExist" } + ] + } + clientMutationId: "cmid6" } ) { - name - schemaFields { + clientMutationId + class { name - __typename - ... on SchemaPointerField { - targetClassName - } - ... on SchemaRelationField { - targetClassName + schemaFields { + name + __typename + ... on SchemaPointerField { + targetClassName + } + ... on SchemaRelationField { + targetClassName + } } } } @@ -2586,114 +2671,142 @@ describe('ParseGraphQLServer', () => { }, }); const classes = Object.keys(result.data).map(fieldName => ({ - name: result.data[fieldName].name, - schemaFields: result.data[fieldName].schemaFields.sort((a, b) => - a.name > b.name ? 1 : -1 - ), + clientMutationId: result.data[fieldName].clientMutationId, + class: { + name: result.data[fieldName].class.name, + schemaFields: result.data[fieldName].class.schemaFields.sort( + (a, b) => (a.name > b.name ? 1 : -1) + ), + __typename: result.data[fieldName].class.__typename, + }, __typename: result.data[fieldName].__typename, })); expect(classes).toEqual([ { - name: 'Class1', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - ], - __typename: 'Class', + clientMutationId: 'cmid1', + class: { + name: 'Class1', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, { - name: 'Class2', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - ], - __typename: 'Class', + clientMutationId: 'cmid2', + class: { + name: 'Class2', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, { - name: 'Class3', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - ], - __typename: 'Class', + clientMutationId: 'cmid3', + class: { + name: 'Class3', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, { - name: 'Class4', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - ], - __typename: 'Class', + clientMutationId: 'cmid4', + class: { + name: 'Class4', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, { - name: 'Class5', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - ], - __typename: 'Class', + clientMutationId: 'cmid5', + class: { + name: 'Class5', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, { - name: 'Class6', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'arrayField1', __typename: 'SchemaArrayField' }, - { name: 'arrayField2', __typename: 'SchemaArrayField' }, - { name: 'booleanField1', __typename: 'SchemaBooleanField' }, - { name: 'booleanField2', __typename: 'SchemaBooleanField' }, - { name: 'bytesField1', __typename: 'SchemaBytesField' }, - { name: 'bytesField2', __typename: 'SchemaBytesField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'dateField1', __typename: 'SchemaDateField' }, - { name: 'dateField2', __typename: 'SchemaDateField' }, - { name: 'fileField1', __typename: 'SchemaFileField' }, - { name: 'fileField2', __typename: 'SchemaFileField' }, - { - name: 'geoPointField', - __typename: 'SchemaGeoPointField', - }, - { name: 'numberField1', __typename: 'SchemaNumberField' }, - { name: 'numberField2', __typename: 'SchemaNumberField' }, - { name: 'objectField1', __typename: 'SchemaObjectField' }, - { name: 'objectField2', __typename: 'SchemaObjectField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { - name: 'pointerField1', - __typename: 'SchemaPointerField', - targetClassName: 'Class1', - }, - { - name: 'pointerField2', - __typename: 'SchemaPointerField', - targetClassName: 'Class6', - }, - { name: 'polygonField1', __typename: 'SchemaPolygonField' }, - { name: 'polygonField2', __typename: 'SchemaPolygonField' }, - { - name: 'relationField1', - __typename: 'SchemaRelationField', - targetClassName: 'Class1', - }, - { - name: 'relationField2', - __typename: 'SchemaRelationField', - targetClassName: 'Class6', - }, - { name: 'stringField1', __typename: 'SchemaStringField' }, - { name: 'stringField2', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - ], - __typename: 'Class', + clientMutationId: 'cmid6', + class: { + name: 'Class6', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'arrayField1', __typename: 'SchemaArrayField' }, + { name: 'arrayField2', __typename: 'SchemaArrayField' }, + { name: 'booleanField1', __typename: 'SchemaBooleanField' }, + { name: 'booleanField2', __typename: 'SchemaBooleanField' }, + { name: 'bytesField1', __typename: 'SchemaBytesField' }, + { name: 'bytesField2', __typename: 'SchemaBytesField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'dateField1', __typename: 'SchemaDateField' }, + { name: 'dateField2', __typename: 'SchemaDateField' }, + { name: 'fileField1', __typename: 'SchemaFileField' }, + { name: 'fileField2', __typename: 'SchemaFileField' }, + { + name: 'geoPointField', + __typename: 'SchemaGeoPointField', + }, + { name: 'numberField1', __typename: 'SchemaNumberField' }, + { name: 'numberField2', __typename: 'SchemaNumberField' }, + { name: 'objectField1', __typename: 'SchemaObjectField' }, + { name: 'objectField2', __typename: 'SchemaObjectField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { + name: 'pointerField1', + __typename: 'SchemaPointerField', + targetClassName: 'Class1', + }, + { + name: 'pointerField2', + __typename: 'SchemaPointerField', + targetClassName: 'Class6', + }, + { name: 'polygonField1', __typename: 'SchemaPolygonField' }, + { name: 'polygonField2', __typename: 'SchemaPolygonField' }, + { + name: 'relationField1', + __typename: 'SchemaRelationField', + targetClassName: 'Class1', + }, + { + name: 'relationField2', + __typename: 'SchemaRelationField', + targetClassName: 'Class6', + }, + { name: 'stringField1', __typename: 'SchemaStringField' }, + { name: 'stringField2', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, ]); @@ -2843,8 +2956,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation { - createClass(name: "SomeClass") { - name + createClass(input: { name: "SomeClass" }) { + clientMutationId } } `, @@ -2866,13 +2979,15 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation { createClass( - name: "SomeClass" - schemaFields: { - addStrings: [{ name: "someField" }] - addNumbers: [{ name: "someField" }] + input: { + name: "SomeClass" + schemaFields: { + addStrings: [{ name: "someField" }] + addNumbers: [{ name: "someField" }] + } } ) { - name + clientMutationId } } `, @@ -2899,13 +3014,17 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation { createClass( - name: "MyNewClass" - schemaFields: { addStrings: [{ name: "willBeRemoved" }] } + input: { + name: "MyNewClass" + schemaFields: { addStrings: [{ name: "willBeRemoved" }] } + } ) { - name - schemaFields { + class { name - __typename + schemaFields { + name + __typename + } } } updateClass( @@ -3004,7 +3123,7 @@ describe('ParseGraphQLServer', () => { }, }, }); - result.data.createClass.schemaFields = result.data.createClass.schemaFields.sort( + result.data.createClass.class.schemaFields = result.data.createClass.class.schemaFields.sort( (a, b) => (a.name > b.name ? 1 : -1) ); result.data.updateClass.schemaFields = result.data.updateClass.schemaFields.sort( @@ -3013,15 +3132,21 @@ describe('ParseGraphQLServer', () => { expect(result).toEqual({ data: { createClass: { - name: 'MyNewClass', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - { name: 'willBeRemoved', __typename: 'SchemaStringField' }, - ], - __typename: 'Class', + class: { + name: 'MyNewClass', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + { + name: 'willBeRemoved', + __typename: 'SchemaStringField', + }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, updateClass: { name: 'MyNewClass', @@ -3169,8 +3294,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation { - createClass(name: "SomeClass") { - name + createClass(input: { name: "SomeClass" }) { + clientMutationId } } `, @@ -3211,10 +3336,12 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation { createClass( - name: "SomeClass" - schemaFields: { addStrings: [{ name: "someField" }] } + input: { + name: "SomeClass" + schemaFields: { addStrings: [{ name: "someField" }] } + } ) { - name + clientMutationId } } `, @@ -3293,13 +3420,17 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation { createClass( - name: "MyNewClass" - schemaFields: { addStrings: [{ name: "willBeRemoved" }] } + input: { + name: "MyNewClass" + schemaFields: { addStrings: [{ name: "willBeRemoved" }] } + } ) { - name - schemaFields { + class { name - __typename + schemaFields { + name + __typename + } } } deleteClass(name: "MyNewClass") { @@ -3316,7 +3447,7 @@ describe('ParseGraphQLServer', () => { }, }, }); - result.data.createClass.schemaFields = result.data.createClass.schemaFields.sort( + result.data.createClass.class.schemaFields = result.data.createClass.class.schemaFields.sort( (a, b) => (a.name > b.name ? 1 : -1) ); result.data.deleteClass.schemaFields = result.data.deleteClass.schemaFields.sort( @@ -3325,15 +3456,21 @@ describe('ParseGraphQLServer', () => { expect(result).toEqual({ data: { createClass: { - name: 'MyNewClass', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - { name: 'willBeRemoved', __typename: 'SchemaStringField' }, - ], - __typename: 'Class', + class: { + name: 'MyNewClass', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + { + name: 'willBeRemoved', + __typename: 'SchemaStringField', + }, + ], + __typename: 'Class', + }, + __typename: 'CreateClassPayload', }, deleteClass: { name: 'MyNewClass', @@ -3383,8 +3520,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation { - createClass(name: "SomeClass") { - name + createClass(input: { name: "SomeClass" }) { + clientMutationId } } `, @@ -6359,8 +6496,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -6432,8 +6571,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -6505,8 +6646,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -6579,8 +6722,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -6672,8 +6817,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -6740,8 +6887,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass { - createClass(name: "SomeClass") { - name + createClass(input: { name: "SomeClass" }) { + clientMutationId } } `, @@ -7359,8 +7506,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreaClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -7470,8 +7619,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -7567,10 +7718,12 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation CreateClass { createClass( - name: "SomeClass" - schemaFields: { addObjects: [{ name: "someField" }] } + input: { + name: "SomeClass" + schemaFields: { addObjects: [{ name: "someField" }] } + } ) { - name + clientMutationId } } `, @@ -7681,8 +7834,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -7792,18 +7947,20 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation CreateClass { createClass( - name: "SomeClass" - schemaFields: { - addStrings: [ - { name: "someStringField" } - { name: "someNullField" } - ] - addNumbers: [{ name: "someNumberField" }] - addBooleans: [{ name: "someBooleanField" }] - addObjects: [{ name: "someObjectField" }] + input: { + name: "SomeClass" + schemaFields: { + addStrings: [ + { name: "someStringField" } + { name: "someNullField" } + ] + addNumbers: [{ name: "someNumberField" }] + addBooleans: [{ name: "someBooleanField" }] + addObjects: [{ name: "someObjectField" }] + } } ) { - name + clientMutationId } } `, @@ -7894,8 +8051,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -7981,8 +8140,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, @@ -8069,8 +8230,10 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation CreateClass($schemaFields: SchemaFieldsInput) { - createClass(name: "SomeClass", schemaFields: $schemaFields) { - name + createClass( + input: { name: "SomeClass", schemaFields: $schemaFields } + ) { + clientMutationId } } `, diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 3be689eb4f..585ec4d84d 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -45,6 +45,12 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [ 'CloudCodeFunction', 'CallCloudCodeInput', 'CallCloudCodePayload', + 'CreateClassInput', + 'CreateClassPayload', + 'UpdateClassInput', + 'UpdateClassPayload', + 'DeleteClassInput', + 'DeleteClassPayload', ]; const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes']; const RESERVED_GRAPHQL_MUTATION_NAMES = [ diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 71ac32d43d..8ec3a0d45b 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -1,5 +1,6 @@ import Parse from 'parse/node'; import { GraphQLNonNull } from 'graphql'; +import { mutationWithClientMutationId } from 'graphql-relay'; import * as schemaTypes from './schemaTypes'; import { transformToParse, @@ -9,47 +10,63 @@ import { enforceMasterKeyAccess } from '../parseGraphQLUtils'; import { getClass } from './schemaQueries'; const load = parseGraphQLSchema => { - parseGraphQLSchema.addGraphQLMutation( - 'createClass', - { - description: - 'The createClass mutation can be used to create the schema for a new object class.', - args: { - name: schemaTypes.CLASS_NAME_ATT, - schemaFields: { - description: "These are the schema's fields of the object class.", - type: schemaTypes.SCHEMA_FIELDS_INPUT, - }, + const createClassMutation = mutationWithClientMutationId({ + name: 'CreateClass', + description: + 'The createClass mutation can be used to create the schema for a new object class.', + inputFields: { + name: schemaTypes.CLASS_NAME_ATT, + schemaFields: { + description: "These are the schema's fields of the object class.", + type: schemaTypes.SCHEMA_FIELDS_INPUT, }, - type: new GraphQLNonNull(schemaTypes.CLASS), - resolve: async (_source, args, context) => { - try { - const { name, schemaFields } = args; - const { config, auth } = context; + }, + outputFields: { + class: { + description: 'This is the created class.', + type: new GraphQLNonNull(schemaTypes.CLASS), + }, + }, + mutateAndGetPayload: async (args, context) => { + try { + const { name, schemaFields } = args; + const { config, auth } = context; - enforceMasterKeyAccess(auth); + enforceMasterKeyAccess(auth); - if (auth.isReadOnly) { - throw new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to create a schema." - ); - } - - const schema = await config.database.loadSchema({ clearCache: true }); - const parseClass = await schema.addClassIfNotExists( - name, - transformToParse(schemaFields) + if (auth.isReadOnly) { + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to create a schema." ); - return { + } + + const schema = await config.database.loadSchema({ clearCache: true }); + const parseClass = await schema.addClassIfNotExists( + name, + transformToParse(schemaFields) + ); + return { + class: { name: parseClass.className, schemaFields: transformToGraphQL(parseClass.fields), - }; - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + }, + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } }, + }); + + parseGraphQLSchema.addGraphQLType( + createClassMutation.args.input.type.ofType, + true, + true + ); + parseGraphQLSchema.addGraphQLType(createClassMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation( + 'createClass', + createClassMutation, true, true ); From 21b474ccf7f20e14985a435585ed46308c3ba179 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 17:16:04 -0700 Subject: [PATCH 15/27] Client mutation id on updateClass mutation --- spec/ParseGraphQLServer.spec.js | 203 ++++++++++++++++--------- src/GraphQL/loaders/schemaMutations.js | 92 ++++++----- 2 files changed, 188 insertions(+), 107 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 16aac6299a..24a5faed64 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1019,6 +1019,46 @@ describe('ParseGraphQLServer', () => { expect(payloadFields).toEqual(['class', 'clientMutationId']); }); + + it('should have clientMutationId in updateClass mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateClassInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual([ + 'clientMutationId', + 'name', + 'schemaFields', + ]); + }); + + it('should have clientMutationId in updateClass mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateClassPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['class', 'clientMutationId']); + }); }); describe('Parse Class Types', () => { @@ -3010,6 +3050,7 @@ describe('ParseGraphQLServer', () => { it('should update an existing class', async () => { try { + const clientMutationId = uuidv4(); const result = await apolloClient.mutate({ mutation: gql` mutation { @@ -3027,7 +3068,8 @@ describe('ParseGraphQLServer', () => { } } } - updateClass( + updateClass(input: { + clientMutationId: "${clientMutationId}" name: "MyNewClass" schemaFields: { addStrings: [ @@ -3102,16 +3144,19 @@ describe('ParseGraphQLServer', () => { { name: "doesNotExist" } ] } - ) { - name - schemaFields { + }) { + clientMutationId + class { name - __typename - ... on SchemaPointerField { - targetClassName - } - ... on SchemaRelationField { - targetClassName + schemaFields { + name + __typename + ... on SchemaPointerField { + targetClassName + } + ... on SchemaRelationField { + targetClassName + } } } } @@ -3126,7 +3171,7 @@ describe('ParseGraphQLServer', () => { result.data.createClass.class.schemaFields = result.data.createClass.class.schemaFields.sort( (a, b) => (a.name > b.name ? 1 : -1) ); - result.data.updateClass.schemaFields = result.data.updateClass.schemaFields.sort( + result.data.updateClass.class.schemaFields = result.data.updateClass.class.schemaFields.sort( (a, b) => (a.name > b.name ? 1 : -1) ); expect(result).toEqual({ @@ -3149,56 +3194,72 @@ describe('ParseGraphQLServer', () => { __typename: 'CreateClassPayload', }, updateClass: { - name: 'MyNewClass', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'arrayField1', __typename: 'SchemaArrayField' }, - { name: 'arrayField2', __typename: 'SchemaArrayField' }, - { name: 'booleanField1', __typename: 'SchemaBooleanField' }, - { name: 'booleanField2', __typename: 'SchemaBooleanField' }, - { name: 'bytesField1', __typename: 'SchemaBytesField' }, - { name: 'bytesField2', __typename: 'SchemaBytesField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'dateField1', __typename: 'SchemaDateField' }, - { name: 'dateField2', __typename: 'SchemaDateField' }, - { name: 'fileField1', __typename: 'SchemaFileField' }, - { name: 'fileField2', __typename: 'SchemaFileField' }, - { - name: 'geoPointField', - __typename: 'SchemaGeoPointField', - }, - { name: 'numberField1', __typename: 'SchemaNumberField' }, - { name: 'numberField2', __typename: 'SchemaNumberField' }, - { name: 'objectField1', __typename: 'SchemaObjectField' }, - { name: 'objectField2', __typename: 'SchemaObjectField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { - name: 'pointerField1', - __typename: 'SchemaPointerField', - targetClassName: 'Class1', - }, - { - name: 'pointerField2', - __typename: 'SchemaPointerField', - targetClassName: 'Class6', - }, - { name: 'polygonField1', __typename: 'SchemaPolygonField' }, - { name: 'polygonField2', __typename: 'SchemaPolygonField' }, - { - name: 'relationField1', - __typename: 'SchemaRelationField', - targetClassName: 'Class1', - }, - { - name: 'relationField2', - __typename: 'SchemaRelationField', - targetClassName: 'Class6', - }, - { name: 'stringField1', __typename: 'SchemaStringField' }, - { name: 'stringField2', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - ], - __typename: 'Class', + clientMutationId, + class: { + name: 'MyNewClass', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'arrayField1', __typename: 'SchemaArrayField' }, + { name: 'arrayField2', __typename: 'SchemaArrayField' }, + { + name: 'booleanField1', + __typename: 'SchemaBooleanField', + }, + { + name: 'booleanField2', + __typename: 'SchemaBooleanField', + }, + { name: 'bytesField1', __typename: 'SchemaBytesField' }, + { name: 'bytesField2', __typename: 'SchemaBytesField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'dateField1', __typename: 'SchemaDateField' }, + { name: 'dateField2', __typename: 'SchemaDateField' }, + { name: 'fileField1', __typename: 'SchemaFileField' }, + { name: 'fileField2', __typename: 'SchemaFileField' }, + { + name: 'geoPointField', + __typename: 'SchemaGeoPointField', + }, + { name: 'numberField1', __typename: 'SchemaNumberField' }, + { name: 'numberField2', __typename: 'SchemaNumberField' }, + { name: 'objectField1', __typename: 'SchemaObjectField' }, + { name: 'objectField2', __typename: 'SchemaObjectField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { + name: 'pointerField1', + __typename: 'SchemaPointerField', + targetClassName: 'Class1', + }, + { + name: 'pointerField2', + __typename: 'SchemaPointerField', + targetClassName: 'Class6', + }, + { + name: 'polygonField1', + __typename: 'SchemaPolygonField', + }, + { + name: 'polygonField2', + __typename: 'SchemaPolygonField', + }, + { + name: 'relationField1', + __typename: 'SchemaRelationField', + targetClassName: 'Class1', + }, + { + name: 'relationField2', + __typename: 'SchemaRelationField', + targetClassName: 'Class6', + }, + { name: 'stringField1', __typename: 'SchemaStringField' }, + { name: 'stringField2', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + ], + __typename: 'Class', + }, + __typename: 'UpdateClassPayload', }, }, }); @@ -3313,8 +3374,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation { - updateClass(name: "SomeClass") { - name + updateClass(input: { name: "SomeClass" }) { + clientMutationId } } `, @@ -3360,10 +3421,12 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation { updateClass( - name: "SomeClass" - schemaFields: { addNumbers: [{ name: "someField" }] } + input: { + name: "SomeClass" + schemaFields: { addNumbers: [{ name: "someField" }] } + } ) { - name + clientMutationId } } `, @@ -3390,10 +3453,12 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation { updateClass( - name: "SomeInexistentClass" - schemaFields: { addNumbers: [{ name: "someField" }] } + input: { + name: "SomeInexistentClass" + schemaFields: { addNumbers: [{ name: "someField" }] } + } ) { - name + clientMutationId } } `, diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index 8ec3a0d45b..dec2fdd126 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -71,51 +71,67 @@ const load = parseGraphQLSchema => { true ); - parseGraphQLSchema.addGraphQLMutation( - 'updateClass', - { - description: - 'The updateClass mutation can be used to update the schema for an existing object class.', - args: { - name: schemaTypes.CLASS_NAME_ATT, - schemaFields: { - description: "These are the schema's fields of the object class.", - type: schemaTypes.SCHEMA_FIELDS_INPUT, - }, + const updateClassMutation = mutationWithClientMutationId({ + name: 'UpdateClass', + description: + 'The updateClass mutation can be used to update the schema for an existing object class.', + inputFields: { + name: schemaTypes.CLASS_NAME_ATT, + schemaFields: { + description: "These are the schema's fields of the object class.", + type: schemaTypes.SCHEMA_FIELDS_INPUT, }, - type: new GraphQLNonNull(schemaTypes.CLASS), - resolve: async (_source, args, context) => { - try { - const { name, schemaFields } = args; - const { config, auth } = context; - - enforceMasterKeyAccess(auth); + }, + outputFields: { + class: { + description: 'This is the created class.', + type: new GraphQLNonNull(schemaTypes.CLASS), + }, + }, + mutateAndGetPayload: async (args, context) => { + try { + const { name, schemaFields } = args; + const { config, auth } = context; - if (auth.isReadOnly) { - throw new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to update a schema." - ); - } + enforceMasterKeyAccess(auth); - const schema = await config.database.loadSchema({ clearCache: true }); - const existingParseClass = await getClass(name, schema); - const parseClass = await schema.updateClass( - name, - transformToParse(schemaFields, existingParseClass.fields), - undefined, - undefined, - config.database + if (auth.isReadOnly) { + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to update a schema." ); - return { + } + + const schema = await config.database.loadSchema({ clearCache: true }); + const existingParseClass = await getClass(name, schema); + const parseClass = await schema.updateClass( + name, + transformToParse(schemaFields, existingParseClass.fields), + undefined, + undefined, + config.database + ); + return { + class: { name: parseClass.className, schemaFields: transformToGraphQL(parseClass.fields), - }; - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + }, + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } }, + }); + + parseGraphQLSchema.addGraphQLType( + updateClassMutation.args.input.type.ofType, + true, + true + ); + parseGraphQLSchema.addGraphQLType(updateClassMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation( + 'updateClass', + updateClassMutation, true, true ); From 8fac91aeff0e6036fe5fed30609f977ea9562a12 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 17:25:26 -0700 Subject: [PATCH 16/27] Client mutation id on deleteClass mutation --- spec/ParseGraphQLServer.spec.js | 81 ++++++++++++++++++++------ src/GraphQL/loaders/schemaMutations.js | 80 +++++++++++++++---------- 2 files changed, 112 insertions(+), 49 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 24a5faed64..f46b82d9b2 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1059,6 +1059,42 @@ describe('ParseGraphQLServer', () => { expect(payloadFields).toEqual(['class', 'clientMutationId']); }); + + it('should have clientMutationId in deleteClass mutation input', async () => { + const inputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteClassInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(inputFields).toEqual(['clientMutationId', 'name']); + }); + + it('should have clientMutationId in deleteClass mutation payload', async () => { + const payloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateClassPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(payloadFields).toEqual(['class', 'clientMutationId']); + }); }); describe('Parse Class Types', () => { @@ -3481,6 +3517,7 @@ describe('ParseGraphQLServer', () => { it('should delete an existing class', async () => { try { + const clientMutationId = uuidv4(); const result = await apolloClient.mutate({ mutation: gql` mutation { @@ -3498,10 +3535,13 @@ describe('ParseGraphQLServer', () => { } } } - deleteClass(name: "MyNewClass") { - name - schemaFields { + deleteClass(input: { clientMutationId: "${clientMutationId}" name: "MyNewClass" }) { + clientMutationId + class { name + schemaFields { + name + } } } } @@ -3515,7 +3555,7 @@ describe('ParseGraphQLServer', () => { result.data.createClass.class.schemaFields = result.data.createClass.class.schemaFields.sort( (a, b) => (a.name > b.name ? 1 : -1) ); - result.data.deleteClass.schemaFields = result.data.deleteClass.schemaFields.sort( + result.data.deleteClass.class.schemaFields = result.data.deleteClass.class.schemaFields.sort( (a, b) => (a.name > b.name ? 1 : -1) ); expect(result).toEqual({ @@ -3538,15 +3578,22 @@ describe('ParseGraphQLServer', () => { __typename: 'CreateClassPayload', }, deleteClass: { - name: 'MyNewClass', - schemaFields: [ - { name: 'ACL', __typename: 'SchemaACLField' }, - { name: 'createdAt', __typename: 'SchemaDateField' }, - { name: 'objectId', __typename: 'SchemaStringField' }, - { name: 'updatedAt', __typename: 'SchemaDateField' }, - { name: 'willBeRemoved', __typename: 'SchemaStringField' }, - ], - __typename: 'Class', + clientMutationId, + class: { + name: 'MyNewClass', + schemaFields: [ + { name: 'ACL', __typename: 'SchemaACLField' }, + { name: 'createdAt', __typename: 'SchemaDateField' }, + { name: 'objectId', __typename: 'SchemaStringField' }, + { name: 'updatedAt', __typename: 'SchemaDateField' }, + { + name: 'willBeRemoved', + __typename: 'SchemaStringField', + }, + ], + __typename: 'Class', + }, + __typename: 'DeleteClassPayload', }, }, }); @@ -3604,8 +3651,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation { - deleteClass(name: "SomeClass") { - name + deleteClass(input: { name: "SomeClass" }) { + clientMutationId } } `, @@ -3626,8 +3673,8 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` mutation { - deleteClass(name: "SomeInexistentClass") { - name + deleteClass(input: { name: "SomeInexistentClass" }) { + clientMutationId } } `, diff --git a/src/GraphQL/loaders/schemaMutations.js b/src/GraphQL/loaders/schemaMutations.js index dec2fdd126..12a828e9d7 100644 --- a/src/GraphQL/loaders/schemaMutations.js +++ b/src/GraphQL/loaders/schemaMutations.js @@ -84,7 +84,7 @@ const load = parseGraphQLSchema => { }, outputFields: { class: { - description: 'This is the created class.', + description: 'This is the updated class.', type: new GraphQLNonNull(schemaTypes.CLASS), }, }, @@ -136,41 +136,57 @@ const load = parseGraphQLSchema => { true ); - parseGraphQLSchema.addGraphQLMutation( - 'deleteClass', - { - description: - 'The deleteClass mutation can be used to delete an existing object class.', - args: { - name: schemaTypes.CLASS_NAME_ATT, + const deleteClassMutation = mutationWithClientMutationId({ + name: 'DeleteClass', + description: + 'The deleteClass mutation can be used to delete an existing object class.', + inputFields: { + name: schemaTypes.CLASS_NAME_ATT, + }, + outputFields: { + class: { + description: 'This is the deleted class.', + type: new GraphQLNonNull(schemaTypes.CLASS), }, - type: new GraphQLNonNull(schemaTypes.CLASS), - resolve: async (_source, args, context) => { - try { - const { name } = args; - const { config, auth } = context; - - enforceMasterKeyAccess(auth); - - if (auth.isReadOnly) { - throw new Parse.Error( - Parse.Error.OPERATION_FORBIDDEN, - "read-only masterKey isn't allowed to delete a schema." - ); - } - - const schema = await config.database.loadSchema({ clearCache: true }); - const existingParseClass = await getClass(name, schema); - await config.database.deleteSchema(name); - return { + }, + mutateAndGetPayload: async (args, context) => { + try { + const { name } = args; + const { config, auth } = context; + + enforceMasterKeyAccess(auth); + + if (auth.isReadOnly) { + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to delete a schema." + ); + } + + const schema = await config.database.loadSchema({ clearCache: true }); + const existingParseClass = await getClass(name, schema); + await config.database.deleteSchema(name); + return { + class: { name: existingParseClass.className, schemaFields: transformToGraphQL(existingParseClass.fields), - }; - } catch (e) { - parseGraphQLSchema.handleError(e); - } - }, + }, + }; + } catch (e) { + parseGraphQLSchema.handleError(e); + } }, + }); + + parseGraphQLSchema.addGraphQLType( + deleteClassMutation.args.input.type.ofType, + true, + true + ); + parseGraphQLSchema.addGraphQLType(deleteClassMutation.type, true, true); + parseGraphQLSchema.addGraphQLMutation( + 'deleteClass', + deleteClassMutation, true, true ); From 8a68f19ec1d471b80f93e612cb7119449f983744 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Mon, 23 Sep 2019 23:42:12 -0700 Subject: [PATCH 17/27] Client mutation id on create object mutation --- spec/ParseGraphQLServer.spec.js | 537 ++++++++++++++------- src/GraphQL/loaders/parseClassMutations.js | 50 +- 2 files changed, 396 insertions(+), 191 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index f46b82d9b2..4d94049f7f 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1095,6 +1095,58 @@ describe('ParseGraphQLServer', () => { expect(payloadFields).toEqual(['class', 'clientMutationId']); }); + + it('should have clientMutationId in custom create object mutation input', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateSomeClassInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(createObjectInputFields).toEqual([ + 'clientMutationId', + 'fields', + ]); + }); + + it('should have clientMutationId in custom create object mutation payload', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "CreateSomeClassPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(createObjectPayloadFields).toEqual([ + 'clientMutationId', + 'someClass', + ]); + }); }); describe('Parse Class Types', () => { @@ -1406,8 +1458,10 @@ describe('ParseGraphQLServer', () => { const { data: customerData } = await apolloClient.query({ query: gql` mutation CreateCustomer($foo: String!) { - createCustomer(fields: { foo: $foo }) { - id + createCustomer(input: { fields: { foo: $foo } }) { + customer { + id + } } } `, @@ -1415,10 +1469,10 @@ describe('ParseGraphQLServer', () => { foo: 'rah', }, }); - expect(customerData.createCustomer).toBeTruthy(); + expect(customerData.createCustomer.customer).toBeTruthy(); // used later - const customer2Id = customerData.createCustomer.id; + const customer2Id = customerData.createCustomer.customer.id; await parseGraphQLServer.setGraphQLConfig({ classConfigs: [ @@ -1445,8 +1499,10 @@ describe('ParseGraphQLServer', () => { const { data: superCarData } = await apolloClient.query({ query: gql` mutation CreateSuperCar($foo: String!) { - createSuperCar(fields: { foo: $foo }) { - id + createSuperCar(input: { fields: { foo: $foo } }) { + superCar { + id + } } } `, @@ -1455,7 +1511,7 @@ describe('ParseGraphQLServer', () => { }, }); expect(superCarData.createSuperCar).toBeTruthy(); - const superCar3Id = superCarData.createSuperCar.id; + const superCar3Id = superCarData.createSuperCar.superCar.id; await expectAsync( apolloClient.query({ @@ -1491,8 +1547,10 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation CreateCustomer($foo: String!) { - createCustomer(fields: { foo: $foo }) { - id + createCustomer(input: { fields: { foo: $foo } }) { + customer { + id + } } } `, @@ -1529,6 +1587,7 @@ describe('ParseGraphQLServer', () => { }) ).toBeRejected(); }); + it('should only allow the supplied create and update fields for a class', async () => { const schemaController = await parseServer.config.databaseController.loadSchema(); await schemaController.addClassIfNotExists('SuperCar', { @@ -1558,8 +1617,12 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation InvalidCreateSuperCar { - createSuperCar(fields: { engine: "diesel", mileage: 1000 }) { - id + createSuperCar( + input: { fields: { engine: "diesel", mileage: 1000 } } + ) { + superCar { + id + } } } `, @@ -1569,13 +1632,17 @@ describe('ParseGraphQLServer', () => { query: gql` mutation ValidCreateSuperCar { createSuperCar( - fields: { engine: "diesel", doors: 5, price: "£10000" } + input: { + fields: { engine: "diesel", doors: 5, price: "£10000" } + } ) { - id + superCar { + id + } } } `, - })).data.createSuperCar; + })).data.createSuperCar.superCar; expect(superCarId).toBeTruthy(); @@ -2112,44 +2179,58 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation CreateSecondaryObjects { secondaryObject1: createSecondaryObject( - fields: { someField: "some value 1" } + input: { fields: { someField: "some value 1" } } ) { - id - objectId - someField + secondaryObject { + id + objectId + someField + } } secondaryObject2: createSecondaryObject( - fields: { someField: "some value 2" } + input: { fields: { someField: "some value 2" } } ) { - id - someField + secondaryObject { + id + someField + } } secondaryObject3: createSecondaryObject( - fields: { someField: "some value 3" } + input: { fields: { someField: "some value 3" } } ) { - objectId - someField + secondaryObject { + objectId + someField + } } secondaryObject4: createSecondaryObject( - fields: { someField: "some value 4" } + input: { fields: { someField: "some value 4" } } ) { - id - objectId + secondaryObject { + id + objectId + } } secondaryObject5: createSecondaryObject( - fields: { someField: "some value 5" } + input: { fields: { someField: "some value 5" } } ) { - id + secondaryObject { + id + } } secondaryObject6: createSecondaryObject( - fields: { someField: "some value 6" } + input: { fields: { someField: "some value 6" } } ) { - objectId + secondaryObject { + objectId + } } secondaryObject7: createSecondaryObject( - fields: { someField: "some value 7" } + input: { fields: { someField: "some value 7" } } ) { - someField + secondaryObject { + someField + } } } `, @@ -2162,7 +2243,7 @@ describe('ParseGraphQLServer', () => { const updateSecondaryObjectsResult = await apolloClient.mutate({ mutation: gql` - mutation CreateSecondaryObjects( + mutation UpdateSecondaryObjects( $id1: ID! $id2: ID! $id3: ID! @@ -2214,15 +2295,24 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id1: createSecondaryObjectsResult.data.secondaryObject1.id, - id2: createSecondaryObjectsResult.data.secondaryObject2.id, + id1: + createSecondaryObjectsResult.data.secondaryObject1 + .secondaryObject.id, + id2: + createSecondaryObjectsResult.data.secondaryObject2 + .secondaryObject.id, id3: - createSecondaryObjectsResult.data.secondaryObject3.objectId, + createSecondaryObjectsResult.data.secondaryObject3 + .secondaryObject.objectId, id4: - createSecondaryObjectsResult.data.secondaryObject4.objectId, - id5: createSecondaryObjectsResult.data.secondaryObject5.id, + createSecondaryObjectsResult.data.secondaryObject4 + .secondaryObject.objectId, + id5: + createSecondaryObjectsResult.data.secondaryObject5 + .secondaryObject.id, id6: - createSecondaryObjectsResult.data.secondaryObject6.objectId, + createSecondaryObjectsResult.data.secondaryObject6 + .secondaryObject.objectId, }, context: { headers: { @@ -2386,36 +2476,40 @@ describe('ParseGraphQLServer', () => { $secondaryObject4: ID! ) { createPrimaryObject( - fields: { - stringField: "some value" - arrayField: [1, "abc", $pointer] - pointerField: { link: $secondaryObject2 } - relationField: { - add: [$secondaryObject2, $secondaryObject4] + input: { + fields: { + stringField: "some value" + arrayField: [1, "abc", $pointer] + pointerField: { link: $secondaryObject2 } + relationField: { + add: [$secondaryObject2, $secondaryObject4] + } } } ) { - id - stringField - arrayField { - ... on Element { - value - } - ... on SecondaryObject { - someField - } - } - pointerField { + primaryObject { id - objectId - someField - } - relationField { - results { + stringField + arrayField { + ... on Element { + value + } + ... on SecondaryObject { + someField + } + } + pointerField { id objectId someField } + relationField { + results { + id + objectId + someField + } + } } } } @@ -2481,7 +2575,9 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createPrimaryObjectResult.data.createPrimaryObject.id, + id: + createPrimaryObjectResult.data.createPrimaryObject + .primaryObject.id, secondaryObject2: getSecondaryObjectsResult.data.secondaryObject2.id, secondaryObject4: @@ -2495,21 +2591,23 @@ describe('ParseGraphQLServer', () => { }); expect( - createPrimaryObjectResult.data.createPrimaryObject.stringField + createPrimaryObjectResult.data.createPrimaryObject.primaryObject + .stringField ).toEqual('some value'); expect( - createPrimaryObjectResult.data.createPrimaryObject.arrayField + createPrimaryObjectResult.data.createPrimaryObject.primaryObject + .arrayField ).toEqual([ { __typename: 'Element', value: 1 }, { __typename: 'Element', value: 'abc' }, { __typename: 'SecondaryObject', someField: 'some value 44' }, ]); expect( - createPrimaryObjectResult.data.createPrimaryObject.pointerField - .someField + createPrimaryObjectResult.data.createPrimaryObject.primaryObject + .pointerField.someField ).toEqual('some value 22'); expect( - createPrimaryObjectResult.data.createPrimaryObject.relationField.results + createPrimaryObjectResult.data.createPrimaryObject.primaryObject.relationField.results .map(value => value.someField) .sort() ).toEqual(['some value 22', 'some value 44']); @@ -5204,6 +5302,7 @@ describe('ParseGraphQLServer', () => { describe('Objects Mutations', () => { describe('Create', () => { it('should return specific type object using class specific mutation', async () => { + const clientMutationId = uuidv4(); const customerSchema = new Parse.Schema('Customer'); customerSchema.addString('someField'); await customerSchema.save(); @@ -5212,31 +5311,42 @@ describe('ParseGraphQLServer', () => { const result = await apolloClient.mutate({ mutation: gql` - mutation CreateCustomer($fields: CreateCustomerFieldsInput) { - createCustomer(fields: $fields) { - id - objectId - createdAt - someField + mutation CreateCustomer($input: CreateCustomerInput!) { + createCustomer(input: $input) { + clientMutationId + customer { + id + objectId + createdAt + someField + } } } `, variables: { - fields: { - someField: 'someValue', + input: { + clientMutationId, + fields: { + someField: 'someValue', + }, }, }, }); - expect(result.data.createCustomer.id).toBeDefined(); - expect(result.data.createCustomer.someField).toEqual('someValue'); + expect(result.data.createCustomer.clientMutationId).toEqual( + clientMutationId + ); + expect(result.data.createCustomer.customer.id).toBeDefined(); + expect(result.data.createCustomer.customer.someField).toEqual( + 'someValue' + ); const customer = await new Parse.Query('Customer').get( - result.data.createCustomer.objectId + result.data.createCustomer.customer.objectId ); expect(customer.createdAt).toEqual( - new Date(result.data.createCustomer.createdAt) + new Date(result.data.createCustomer.customer.createdAt) ); expect(customer.get('someField')).toEqual('someValue'); }); @@ -5247,12 +5357,16 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); async function createObject(className, headers) { + const getClassName = + className.charAt(0).toLowerCase() + className.slice(1); const result = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject { - create${className} { - id - createdAt + create${className}(input: {}) { + ${getClassName} { + id + createdAt + } } } `, @@ -5261,7 +5375,8 @@ describe('ParseGraphQLServer', () => { }, }); - const specificCreate = result.data[`create${className}`]; + const specificCreate = + result.data[`create${className}`][getClassName]; expect(specificCreate.id).toBeDefined(); expect(specificCreate.createdAt).toBeDefined(); @@ -6635,8 +6750,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -6663,7 +6780,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, someFieldValue, }, }); @@ -6707,8 +6824,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -6738,7 +6857,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, someFieldValue, }, }); @@ -6785,8 +6904,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -6813,7 +6934,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, someFieldValue, }, }); @@ -6865,8 +6986,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -6902,7 +7025,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, someFieldValueTrue, someFieldValueFalse, }, @@ -6956,8 +7079,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -6982,7 +7107,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, }, }); @@ -7033,17 +7158,21 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { createCountry: result }, + data: { + createCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation Create($fields: CreateCountryFieldsInput) { - createCountry(fields: $fields) { - id - objectId - company { + createCountry(input: { fields: $fields }) { + country { id objectId - name + company { + id + objectId + name + } } } } @@ -7074,15 +7203,19 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { createCountry: result }, + data: { + createCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation Create($fields: CreateCountryFieldsInput) { - createCountry(fields: $fields) { - id - company { + createCountry(input: { fields: $fields }) { + country { id - name + company { + id + name + } } } } @@ -7207,19 +7340,23 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { createCountry: result }, + data: { + createCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation CreateCountry($fields: CreateCountryFieldsInput) { - createCountry(fields: $fields) { - id - objectId - name - companies { - results { - id - objectId - name + createCountry(input: { fields: $fields }) { + country { + id + objectId + name + companies { + results { + id + objectId + name + } } } } @@ -7276,21 +7413,25 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { createCountry: result }, + data: { + createCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation CreateCountry($fields: CreateCountryFieldsInput) { - createCountry(fields: $fields) { - id - name - companies { - results { - id - name - teams { - results { - id - name + createCountry(input: { fields: $fields }) { + country { + id + name + companies { + results { + id + name + teams { + results { + id + name + } } } } @@ -7428,20 +7569,24 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { createCountry: result }, + data: { + createCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation CreateCountry( $fields: CreateCountryFieldsInput $where: CompanyWhereInput ) { - createCountry(fields: $fields) { - id - name - companies(where: $where) { - results { - id - name + createCountry(input: { fields: $fields }) { + country { + id + name + companies(where: $where) { + results { + id + name + } } } } @@ -7617,7 +7762,7 @@ describe('ParseGraphQLServer', () => { await apolloClient.mutate({ mutation: gql` - mutation CreaClass($schemaFields: SchemaFieldsInput) { + mutation CreateClass($schemaFields: SchemaFieldsInput) { createClass( input: { name: "SomeClass", schemaFields: $schemaFields } ) { @@ -7645,11 +7790,19 @@ describe('ParseGraphQLServer', () => { $fields1: CreateSomeClassFieldsInput $fields2: CreateSomeClassFieldsInput ) { - createSomeClass1: createSomeClass(fields: $fields1) { - id + createSomeClass1: createSomeClass( + input: { fields: $fields1 } + ) { + someClass { + id + } } - createSomeClass2: createSomeClass(fields: $fields2) { - id + createSomeClass2: createSomeClass( + input: { fields: $fields2 } + ) { + someClass { + id + } } } `, @@ -7698,7 +7851,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass1.id, + id: createResult.data.createSomeClass1.someClass.id, }, }); @@ -7758,8 +7911,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -7794,7 +7949,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, where, }, }); @@ -7854,11 +8009,15 @@ describe('ParseGraphQLServer', () => { $fields1: CreateSomeClassFieldsInput $fields2: CreateSomeClassFieldsInput ) { - create1: createSomeClass(fields: $fields1) { - id + create1: createSomeClass(input: { fields: $fields1 }) { + someClass { + id + } } - create2: createSomeClass(fields: $fields2) { - id + create2: createSomeClass(input: { fields: $fields2 }) { + someClass { + id + } } } `, @@ -7923,10 +8082,12 @@ describe('ParseGraphQLServer', () => { const { results } = someClasses; expect(results.length).toEqual(2); expect( - results.find(result => result.id === create1.id).someField + results.find(result => result.id === create1.someClass.id) + .someField ).toEqual(someFieldValue); expect( - results.find(result => result.id === create2.id).someField + results.find(result => result.id === create2.someClass.id) + .someField ).toEqual(someFieldValue2); } catch (e) { handleError(e); @@ -7973,8 +8134,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -8008,7 +8171,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, }, }); @@ -8088,8 +8251,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -8116,7 +8281,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, fields: { someStringField: null, someNumberField: null, @@ -8140,7 +8305,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, }, }); @@ -8193,11 +8358,19 @@ describe('ParseGraphQLServer', () => { $fields1: CreateSomeClassFieldsInput $fields2: CreateSomeClassFieldsInput ) { - createSomeClass1: createSomeClass(fields: $fields1) { - id + createSomeClass1: createSomeClass( + input: { fields: $fields1 } + ) { + someClass { + id + } } - createSomeClass2: createSomeClass(fields: $fields2) { - id + createSomeClass2: createSomeClass( + input: { fields: $fields2 } + ) { + someClass { + id + } } } `, @@ -8228,7 +8401,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass1.id, + id: createResult.data.createSomeClass1.someClass.id, someFieldValue, }, }); @@ -8279,8 +8452,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -8315,7 +8490,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, }, }); @@ -8369,8 +8544,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -8402,7 +8579,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, }, }); @@ -8442,8 +8619,10 @@ describe('ParseGraphQLServer', () => { const createResult = await apolloClient.mutate({ mutation: gql` mutation CreateSomeObject($fields: CreateSomeClassFieldsInput) { - createSomeClass(fields: $fields) { - id + createSomeClass(input: { fields: $fields }) { + someClass { + id + } } } `, @@ -8463,7 +8642,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, }, }); @@ -8488,7 +8667,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass.id, + id: createResult.data.createSomeClass.someClass.id, fields: { someField: updatedSomeFieldValue, }, @@ -8518,7 +8697,9 @@ describe('ParseGraphQLServer', () => { }); const findResults = findResult.data.someClasses.results; expect(findResults.length).toBe(1); - expect(findResults[0].id).toBe(createResult.data.createSomeClass.id); + expect(findResults[0].id).toBe( + createResult.data.createSomeClass.someClass.id + ); }); }); diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 6b13bca1d2..7ac844e928 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,5 +1,5 @@ import { GraphQLNonNull } from 'graphql'; -import { fromGlobalId } from 'graphql-relay'; +import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import { @@ -41,6 +41,8 @@ const load = function( ) { const className = parseClass.className; const graphQLClassName = transformClassNameToGraphQL(className); + const getGraphQLQueryName = + graphQLClassName.charAt(0).toLowerCase() + graphQLClassName.slice(1); const { create: isCreateEnabled = true, @@ -56,18 +58,24 @@ const load = function( if (isCreateEnabled) { const createGraphQLMutationName = `create${graphQLClassName}`; - parseGraphQLSchema.addGraphQLMutation(createGraphQLMutationName, { + const createGraphQLMutation = mutationWithClientMutationId({ + name: `Create${graphQLClassName}`, description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${graphQLClassName} class.`, - args: { + inputFields: { fields: { - description: 'These are the fields used to create the object.', + description: + 'These are the fields that will be used to create the new object.', type: classGraphQLCreateType || defaultGraphQLTypes.OBJECT, }, }, - type: new GraphQLNonNull( - classGraphQLOutputType || defaultGraphQLTypes.OBJECT - ), - async resolve(_source, args, context, mutationInfo) { + outputFields: { + [getGraphQLQueryName]: { + type: new GraphQLNonNull( + classGraphQLOutputType || defaultGraphQLTypes.OBJECT + ), + }, + }, + mutateAndGetPayload: async (args, context, mutationInfo) => { try { let { fields } = args; if (!fields) fields = {}; @@ -86,7 +94,9 @@ const load = function( auth, info ); - const selectedFields = getFieldNames(mutationInfo); + const selectedFields = getFieldNames(mutationInfo) + .filter(field => field.startsWith(`${getGraphQLQueryName}.`)) + .map(field => field.replace(`${getGraphQLQueryName}.`, '')); const { keys, include } = extractKeysAndInclude(selectedFields); const { keys: requiredKeys, needGet } = getOnlyRequiredFields( fields, @@ -109,16 +119,30 @@ const load = function( ); } return { - ...createdObject, - updatedAt: createdObject.createdAt, - ...fields, - ...optimizedObject, + [getGraphQLQueryName]: { + ...createdObject, + updatedAt: createdObject.createdAt, + ...fields, + ...optimizedObject, + }, }; } catch (e) { parseGraphQLSchema.handleError(e); } }, }); + + if ( + parseGraphQLSchema.addGraphQLType( + createGraphQLMutation.args.input.type.ofType + ) && + parseGraphQLSchema.addGraphQLType(createGraphQLMutation.type) + ) { + parseGraphQLSchema.addGraphQLMutation( + createGraphQLMutationName, + createGraphQLMutation + ); + } } if (isUpdateEnabled) { From 34224193289104f0385f34b2b22377db59f00bb3 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Tue, 24 Sep 2019 00:47:45 -0700 Subject: [PATCH 18/27] Improve Viewer type --- spec/ParseGraphQLServer.spec.js | 47 ++++++++++------- src/GraphQL/loaders/defaultGraphQLTypes.js | 2 +- src/GraphQL/loaders/parseClassTypes.js | 8 +-- src/GraphQL/loaders/usersMutations.js | 20 ++++++-- src/GraphQL/loaders/usersQueries.js | 60 +++++++++++++++++----- 5 files changed, 99 insertions(+), 38 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 4d94049f7f..77d938a7b4 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -6155,9 +6155,11 @@ describe('ParseGraphQLServer', () => { query: gql` query GetCurrentUser { viewer { - id - username - email + user { + id + username + email + } } } `, @@ -6172,7 +6174,7 @@ describe('ParseGraphQLServer', () => { id, username: resultUserName, email: resultEmail, - } = result.data.viewer; + } = result.data.viewer.user; expect(id).toBeDefined(); expect(resultUserName).toEqual(userName); expect(resultEmail).toEqual(email); @@ -6200,11 +6202,13 @@ describe('ParseGraphQLServer', () => { query: gql` query GetCurrentUser { viewer { - id - objectId sessionToken - userFoo { - bar + user { + id + objectId + userFoo { + bar + } } } } @@ -6216,11 +6220,8 @@ describe('ParseGraphQLServer', () => { }, }); - const { - objectId, - sessionToken, - userFoo: resultFoo, - } = result.data.viewer; + const sessionToken = result.data.viewer.sessionToken; + const { objectId, userFoo: resultFoo } = result.data.viewer.user; expect(objectId).toEqual(user.id); expect(sessionToken).toBeDefined(); expect(resultFoo).toBeDefined(); @@ -6242,7 +6243,9 @@ describe('ParseGraphQLServer', () => { clientMutationId viewer { sessionToken - someField + user { + someField + } } } } @@ -6261,7 +6264,7 @@ describe('ParseGraphQLServer', () => { expect(result.data.signUp.clientMutationId).toEqual(clientMutationId); expect(result.data.signUp.viewer.sessionToken).toBeDefined(); - expect(result.data.signUp.viewer.someField).toEqual('someValue'); + expect(result.data.signUp.viewer.user.someField).toEqual('someValue'); expect(typeof result.data.signUp.viewer.sessionToken).toBe('string'); }); @@ -6281,7 +6284,9 @@ describe('ParseGraphQLServer', () => { clientMutationId viewer { sessionToken - someField + user { + someField + } } } } @@ -6297,7 +6302,7 @@ describe('ParseGraphQLServer', () => { expect(result.data.logIn.clientMutationId).toEqual(clientMutationId); expect(result.data.logIn.viewer.sessionToken).toBeDefined(); - expect(result.data.logIn.viewer.someField).toEqual('someValue'); + expect(result.data.logIn.viewer.user.someField).toEqual('someValue'); expect(typeof result.data.logIn.viewer.sessionToken).toBe('string'); }); @@ -6415,7 +6420,9 @@ describe('ParseGraphQLServer', () => { query: gql` query GetCurrentUser { viewer { - username + user { + username + } } } `, @@ -6444,7 +6451,9 @@ describe('ParseGraphQLServer', () => { query: gql` query GetCurrentUser { viewer { - username + user { + username + } } cars { results { diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index d1b6d1397a..653ef1c54b 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -455,7 +455,7 @@ const PARSE_OBJECT = new GraphQLInterfaceType({ }); const SESSION_TOKEN_ATT = { - description: 'The user session token', + description: 'The current user session token.', type: new GraphQLNonNull(GraphQLString), }; diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 06553739f4..00e7df9af6 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -561,14 +561,16 @@ const load = ( const viewerType = new GraphQLObjectType({ name: 'Viewer', description: `The Viewer object type is used in operations that involve outputting the current user data.`, - interfaces, fields: () => ({ - ...outputFields(), sessionToken: defaultGraphQLTypes.SESSION_TOKEN_ATT, + user: { + description: 'This is the current user.', + type: new GraphQLNonNull(classGraphQLOutputType), + }, }), }); - parseGraphQLSchema.viewerType = viewerType; parseGraphQLSchema.addGraphQLType(viewerType, true, true); + parseGraphQLSchema.viewerType = viewerType; } }; diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 5ff6b9b5f6..0a8677b610 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -46,7 +46,13 @@ const load = parseGraphQLSchema => { info.sessionToken = sessionToken; return { - viewer: await getUserFromSessionToken(config, info, mutationInfo), + viewer: await getUserFromSessionToken( + config, + info, + mutationInfo, + 'viewer.user.', + true + ), }; } catch (e) { parseGraphQLSchema.handleError(e); @@ -101,7 +107,13 @@ const load = parseGraphQLSchema => { info.sessionToken = sessionToken; return { - viewer: await getUserFromSessionToken(config, info, mutationInfo), + viewer: await getUserFromSessionToken( + config, + info, + mutationInfo, + 'viewer.user.', + true + ), }; } catch (e) { parseGraphQLSchema.handleError(e); @@ -134,7 +146,9 @@ const load = parseGraphQLSchema => { const viewer = await getUserFromSessionToken( config, info, - mutationInfo + mutationInfo, + 'viewer.user.', + true ); await usersRouter.handleLogOut({ diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index 19c98e9453..7c00d19315 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -5,7 +5,13 @@ import rest from '../../rest'; import Auth from '../../Auth'; import { extractKeysAndInclude } from './parseClassTypes'; -const getUserFromSessionToken = async (config, info, queryInfo) => { +const getUserFromSessionToken = async ( + config, + info, + queryInfo, + keysPrefix, + validatedToken +) => { if (!info || !info.sessionToken) { throw new Parse.Error( Parse.Error.INVALID_SESSION_TOKEN, @@ -13,20 +19,42 @@ const getUserFromSessionToken = async (config, info, queryInfo) => { ); } const sessionToken = info.sessionToken; - const selectedFields = getFieldNames(queryInfo); + const selectedFields = getFieldNames(queryInfo) + .filter(field => field.startsWith(keysPrefix)) + .map(field => field.replace(keysPrefix, '')); + + const keysAndInclude = extractKeysAndInclude(selectedFields); + const { keys } = keysAndInclude; + let { include } = keysAndInclude; + + if (validatedToken && !keys && !include) { + return { + sessionToken, + }; + } else if (keys && !include) { + include = 'user'; + } + + const options = {}; + if (keys) { + options.keys = keys + .split(',') + .map(key => `user.${key}`) + .join(','); + } + if (include) { + options.include = include + .split(',') + .map(included => `user.${included}`) + .join(','); + } - const { include } = extractKeysAndInclude(selectedFields); const response = await rest.find( config, Auth.master(config), '_Session', { sessionToken }, - { - include: include - .split(',') - .map(included => `user.${included}`) - .join(','), - }, + options, info.clientVersion ); if ( @@ -40,8 +68,10 @@ const getUserFromSessionToken = async (config, info, queryInfo) => { ); } else { const user = response.results[0].user; - user.sessionToken = sessionToken; - return user; + return { + sessionToken, + user, + }; } }; @@ -59,7 +89,13 @@ const load = parseGraphQLSchema => { async resolve(_source, _args, context, queryInfo) { try { const { config, info } = context; - return await getUserFromSessionToken(config, info, queryInfo); + return await getUserFromSessionToken( + config, + info, + queryInfo, + 'user.', + false + ); } catch (e) { parseGraphQLSchema.handleError(e); } From e9fe1c560bca087f3bb0d28a8250f40a38230d93 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Tue, 24 Sep 2019 10:24:28 -0700 Subject: [PATCH 19/27] Client mutation id on update object mutation --- spec/ParseGraphQLServer.spec.js | 356 +++++++++++++-------- src/GraphQL/loaders/parseClassMutations.js | 63 +++- 2 files changed, 271 insertions(+), 148 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 77d938a7b4..877911b67d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1428,8 +1428,8 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation UpdateSuperCar($id: ID!, $foo: String!) { - updateSuperCar(id: $id, fields: { foo: $foo }) { - updatedAt + updateSuperCar(input: { id: $id, fields: { foo: $foo } }) { + clientMutationId } } `, @@ -1517,8 +1517,8 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation UpdateSupercar($id: ID!, $foo: String!) { - updateSuperCar(id: $id, fields: { foo: $foo }) { - updatedAt + updateSuperCar(input: { id: $id, fields: { foo: $foo } }) { + clientMutationId } } `, @@ -1563,8 +1563,8 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation UpdateCustomer($id: ID!, $foo: String!) { - updateCustomer(id: $id, fields: { foo: $foo }) { - updatedAt + updateCustomer(input: { id: $id, fields: { foo: $foo } }) { + clientMutationId } } `, @@ -1650,8 +1650,10 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation InvalidUpdateSuperCar($id: ID!) { - updateSuperCar(id: $id, fields: { engine: "petrol" }) { - updatedAt + updateSuperCar( + input: { id: $id, fields: { engine: "petrol" } } + ) { + clientMutationId } } `, @@ -1664,8 +1666,8 @@ describe('ParseGraphQLServer', () => { const updatedSuperCar = (await apolloClient.query({ query: gql` mutation ValidUpdateSuperCar($id: ID!) { - updateSuperCar(id: $id, fields: { mileage: 2000 }) { - updatedAt + updateSuperCar(input: { id: $id, fields: { mileage: 2000 } }) { + clientMutationId } } `, @@ -2252,45 +2254,69 @@ describe('ParseGraphQLServer', () => { $id6: ID! ) { secondaryObject1: updateSecondaryObject( - id: $id1 - fields: { someField: "some value 11" } + input: { + id: $id1 + fields: { someField: "some value 11" } + } ) { - id - objectId - someField + secondaryObject { + id + objectId + someField + } } secondaryObject2: updateSecondaryObject( - id: $id2 - fields: { someField: "some value 22" } + input: { + id: $id2 + fields: { someField: "some value 22" } + } ) { - id - someField + secondaryObject { + id + someField + } } secondaryObject3: updateSecondaryObject( - id: $id3 - fields: { someField: "some value 33" } + input: { + id: $id3 + fields: { someField: "some value 33" } + } ) { - objectId - someField + secondaryObject { + objectId + someField + } } secondaryObject4: updateSecondaryObject( - id: $id4 - fields: { someField: "some value 44" } + input: { + id: $id4 + fields: { someField: "some value 44" } + } ) { - id - objectId + secondaryObject { + id + objectId + } } secondaryObject5: updateSecondaryObject( - id: $id5 - fields: { someField: "some value 55" } + input: { + id: $id5 + fields: { someField: "some value 55" } + } ) { - id + secondaryObject { + id + } } secondaryObject6: updateSecondaryObject( - id: $id6 - fields: { someField: "some value 66" } + input: { + id: $id6 + fields: { someField: "some value 66" } + } ) { - objectId + secondaryObject { + objectId + } } } `, @@ -2347,12 +2373,18 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id1: updateSecondaryObjectsResult.data.secondaryObject1.id, + id1: + updateSecondaryObjectsResult.data.secondaryObject1 + .secondaryObject.id, id3: - updateSecondaryObjectsResult.data.secondaryObject3.objectId, - id5: updateSecondaryObjectsResult.data.secondaryObject5.id, + updateSecondaryObjectsResult.data.secondaryObject3 + .secondaryObject.objectId, + id5: + updateSecondaryObjectsResult.data.secondaryObject5 + .secondaryObject.id, id6: - updateSecondaryObjectsResult.data.secondaryObject6.objectId, + updateSecondaryObjectsResult.data.secondaryObject6 + .secondaryObject.objectId, }, context: { headers: { @@ -2376,9 +2408,12 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id2: updateSecondaryObjectsResult.data.secondaryObject2.id, + id2: + updateSecondaryObjectsResult.data.secondaryObject2 + .secondaryObject.id, id4: - updateSecondaryObjectsResult.data.secondaryObject4.objectId, + updateSecondaryObjectsResult.data.secondaryObject4 + .secondaryObject.objectId, }, context: { headers: { @@ -2541,35 +2576,39 @@ describe('ParseGraphQLServer', () => { $secondaryObject4: ID! ) { updatePrimaryObject( - id: $id - fields: { - pointerField: { link: $secondaryObject4 } - relationField: { - remove: [$secondaryObject2, $secondaryObject4] + input: { + id: $id + fields: { + pointerField: { link: $secondaryObject4 } + relationField: { + remove: [$secondaryObject2, $secondaryObject4] + } } } ) { - id - stringField - arrayField { - ... on Element { - value - } - ... on SecondaryObject { - someField - } - } - pointerField { + primaryObject { id - objectId - someField - } - relationField { - results { + stringField + arrayField { + ... on Element { + value + } + ... on SecondaryObject { + someField + } + } + pointerField { id objectId someField } + relationField { + results { + id + objectId + someField + } + } } } } @@ -2612,22 +2651,24 @@ describe('ParseGraphQLServer', () => { .sort() ).toEqual(['some value 22', 'some value 44']); expect( - updatePrimaryObjectResult.data.updatePrimaryObject.stringField + updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject + .stringField ).toEqual('some value'); expect( - updatePrimaryObjectResult.data.updatePrimaryObject.arrayField + updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject + .arrayField ).toEqual([ { __typename: 'Element', value: 1 }, { __typename: 'Element', value: 'abc' }, { __typename: 'SecondaryObject', someField: 'some value 44' }, ]); expect( - updatePrimaryObjectResult.data.updatePrimaryObject.pointerField - .someField + updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject + .pointerField.someField ).toEqual('some value 44'); expect( - updatePrimaryObjectResult.data.updatePrimaryObject.relationField - .results + updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject + .relationField.results ).toEqual([]); } catch (e) { handleError(e); @@ -5434,6 +5475,7 @@ describe('ParseGraphQLServer', () => { describe('Update', () => { it('should return specific type object using class specific mutation', async () => { + const clientMutationId = uuidv4(); const obj = new Parse.Object('Customer'); obj.set('someField1', 'someField1Value1'); obj.set('someField2', 'someField2Value1'); @@ -5443,30 +5485,36 @@ describe('ParseGraphQLServer', () => { const result = await apolloClient.mutate({ mutation: gql` - mutation UpdateCustomer( - $id: ID! - $fields: UpdateCustomerFieldsInput - ) { - updateCustomer(id: $id, fields: $fields) { - updatedAt - someField1 - someField2 + mutation UpdateCustomer($input: UpdateCustomerInput!) { + updateCustomer(input: $input) { + clientMutationId + customer { + updatedAt + someField1 + someField2 + } } } `, variables: { - id: obj.id, - fields: { - someField1: 'someField1Value2', + input: { + clientMutationId, + id: obj.id, + fields: { + someField1: 'someField1Value2', + }, }, }, }); - expect(result.data.updateCustomer.updatedAt).toBeDefined(); - expect(result.data.updateCustomer.someField1).toEqual( + expect(result.data.updateCustomer.clientMutationId).toEqual( + clientMutationId + ); + expect(result.data.updateCustomer.customer.updatedAt).toBeDefined(); + expect(result.data.updateCustomer.customer.someField1).toEqual( 'someField1Value2' ); - expect(result.data.updateCustomer.someField2).toEqual( + expect(result.data.updateCustomer.customer.someField2).toEqual( 'someField2Value1' ); @@ -5490,9 +5538,11 @@ describe('ParseGraphQLServer', () => { $id: ID! $fields: UpdateCustomerFieldsInput ) { - updateCustomer(id: $id, fields: $fields) { - id - objectId + updateCustomer(input: { id: $id, fields: $fields }) { + customer { + id + objectId + } } } `, @@ -5504,7 +5554,9 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.updateCustomer.objectId).toEqual(obj.id); + expect(result.data.updateCustomer.customer.objectId).toEqual( + obj.id + ); await obj.fetch(); @@ -5524,11 +5576,12 @@ describe('ParseGraphQLServer', () => { $id: ID! $fields: Update${className}FieldsInput ) { - update: update${className}( + update: update${className}(input: { id: $id fields: $fields - ) { - updatedAt + clientMutationId: "someid" + }) { + clientMutationId } } `, @@ -5557,7 +5610,7 @@ describe('ParseGraphQLServer', () => { expect( (await updateObject(object4.className, object4.id, { someField: 'changedValue1', - })).data.update.updatedAt + })).data.update.clientMutationId ).toBeDefined(); await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue1'); @@ -5569,7 +5622,7 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue2' }, { 'X-Parse-Master-Key': 'test' } - )).data.update.updatedAt + )).data.update.clientMutationId ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue2'); @@ -5583,7 +5636,7 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue3' }, { 'X-Parse-Session-Token': user1.getSessionToken() } - )).data.update.updatedAt + )).data.update.clientMutationId ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue3'); @@ -5597,7 +5650,7 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue4' }, { 'X-Parse-Session-Token': user2.getSessionToken() } - )).data.update.updatedAt + )).data.update.clientMutationId ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue4'); @@ -5611,7 +5664,7 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue5' }, { 'X-Parse-Session-Token': user3.getSessionToken() } - )).data.update.updatedAt + )).data.update.clientMutationId ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue5'); @@ -5649,7 +5702,7 @@ describe('ParseGraphQLServer', () => { object4.id, { someField: 'changedValue6' }, { 'X-Parse-Session-Token': user4.getSessionToken() } - )).data.update.updatedAt + )).data.update.clientMutationId ).toBeDefined(); await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue6'); @@ -5674,7 +5727,7 @@ describe('ParseGraphQLServer', () => { object3.id, { someField: 'changedValue7' }, { 'X-Parse-Session-Token': user5.getSessionToken() } - )).data.update.updatedAt + )).data.update.clientMutationId ).toBeDefined(); await object3.fetch({ useMasterKey: true }); expect(object3.get('someField')).toEqual('changedValue7'); @@ -5684,7 +5737,7 @@ describe('ParseGraphQLServer', () => { object4.id, { someField: 'changedValue7' }, { 'X-Parse-Session-Token': user5.getSessionToken() } - )).data.update.updatedAt + )).data.update.clientMutationId ).toBeDefined(); await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue7'); @@ -5702,11 +5755,14 @@ describe('ParseGraphQLServer', () => { $id: ID! $fields: Update${className}FieldsInput ) { - update${className}( + update${className}(input: { id: $id fields: $fields - ) { - updatedAt + }) { + ${className.charAt(0).toLowerCase() + + className.slice(1)} { + updatedAt + } } } `, @@ -5735,7 +5791,10 @@ describe('ParseGraphQLServer', () => { expect( (await updateObject(object4.className, object4.id, { someField: 'changedValue1', - })).data[`update${object4.className}`].updatedAt + })).data[`update${object4.className}`][ + object4.className.charAt(0).toLowerCase() + + object4.className.slice(1) + ].updatedAt ).toBeDefined(); await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue1'); @@ -5747,7 +5806,10 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue2' }, { 'X-Parse-Master-Key': 'test' } - )).data[`update${obj.className}`].updatedAt + )).data[`update${obj.className}`][ + obj.className.charAt(0).toLowerCase() + + obj.className.slice(1) + ].updatedAt ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue2'); @@ -5761,7 +5823,10 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue3' }, { 'X-Parse-Session-Token': user1.getSessionToken() } - )).data[`update${obj.className}`].updatedAt + )).data[`update${obj.className}`][ + obj.className.charAt(0).toLowerCase() + + obj.className.slice(1) + ].updatedAt ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue3'); @@ -5775,7 +5840,10 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue4' }, { 'X-Parse-Session-Token': user2.getSessionToken() } - )).data[`update${obj.className}`].updatedAt + )).data[`update${obj.className}`][ + obj.className.charAt(0).toLowerCase() + + obj.className.slice(1) + ].updatedAt ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue4'); @@ -5789,7 +5857,10 @@ describe('ParseGraphQLServer', () => { obj.id, { someField: 'changedValue5' }, { 'X-Parse-Session-Token': user3.getSessionToken() } - )).data[`update${obj.className}`].updatedAt + )).data[`update${obj.className}`][ + obj.className.charAt(0).toLowerCase() + + obj.className.slice(1) + ].updatedAt ).toBeDefined(); await obj.fetch({ useMasterKey: true }); expect(obj.get('someField')).toEqual('changedValue5'); @@ -5827,7 +5898,10 @@ describe('ParseGraphQLServer', () => { object4.id, { someField: 'changedValue6' }, { 'X-Parse-Session-Token': user4.getSessionToken() } - )).data[`update${object4.className}`].updatedAt + )).data[`update${object4.className}`][ + object4.className.charAt(0).toLowerCase() + + object4.className.slice(1) + ].updatedAt ).toBeDefined(); await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue6'); @@ -5852,7 +5926,10 @@ describe('ParseGraphQLServer', () => { object3.id, { someField: 'changedValue7' }, { 'X-Parse-Session-Token': user5.getSessionToken() } - )).data[`update${object3.className}`].updatedAt + )).data[`update${object3.className}`][ + object3.className.charAt(0).toLowerCase() + + object3.className.slice(1) + ].updatedAt ).toBeDefined(); await object3.fetch({ useMasterKey: true }); expect(object3.get('someField')).toEqual('changedValue7'); @@ -5862,7 +5939,10 @@ describe('ParseGraphQLServer', () => { object4.id, { someField: 'changedValue7' }, { 'X-Parse-Session-Token': user5.getSessionToken() } - )).data[`update${object4.className}`].updatedAt + )).data[`update${object4.className}`][ + object4.className.charAt(0).toLowerCase() + + object4.className.slice(1) + ].updatedAt ).toBeDefined(); await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue7'); @@ -7263,17 +7343,21 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { updateCountry: result }, + data: { + updateCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation Update($id: ID!, $fields: UpdateCountryFieldsInput) { - updateCountry(id: $id, fields: $fields) { - id - objectId - company { + updateCountry(input: { id: $id, fields: $fields }) { + country { id objectId - name + company { + id + objectId + name + } } } } @@ -7304,15 +7388,19 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { updateCountry: result }, + data: { + updateCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation Update($id: ID!, $fields: UpdateCountryFieldsInput) { - updateCountry(id: $id, fields: $fields) { - id - company { + updateCountry(input: { id: $id, fields: $fields }) { + country { id - name + company { + id + name + } } } } @@ -7513,21 +7601,25 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const { - data: { updateCountry: result }, + data: { + updateCountry: { country: result }, + }, } = await apolloClient.mutate({ mutation: gql` mutation UpdateCountry( $id: ID! $fields: UpdateCountryFieldsInput ) { - updateCountry(id: $id, fields: $fields) { - id - objectId - companies { - results { - id - objectId - name + updateCountry(input: { id: $id, fields: $fields }) { + country { + id + objectId + companies { + results { + id + objectId + name + } } } } @@ -8284,8 +8376,8 @@ describe('ParseGraphQLServer', () => { $id: ID! $fields: UpdateSomeClassFieldsInput ) { - updateSomeClass(id: $id, fields: $fields) { - updatedAt + updateSomeClass(input: { id: $id, fields: $fields }) { + clientMutationId } } `, @@ -8670,8 +8762,10 @@ describe('ParseGraphQLServer', () => { $id: ID! $fields: UpdateSomeClassFieldsInput ) { - updateSomeClass(id: $id, fields: $fields) { - updatedAt + updateSomeClass(input: { id: $id, fields: $fields }) { + someClass { + updatedAt + } } } `, @@ -8683,7 +8777,7 @@ describe('ParseGraphQLServer', () => { }, }); - const { updatedAt } = updatedResult.data.updateSomeClass; + const { updatedAt } = updatedResult.data.updateSomeClass.someClass; expect(updatedAt).toBeDefined(); const findResult = await apolloClient.query({ diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 7ac844e928..f537bf4f7c 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -18,8 +18,12 @@ const getOnlyRequiredFields = ( includedFieldsString, nativeObjectFields ) => { - const includedFields = includedFieldsString.split(','); - const selectedFields = selectedFieldsString.split(','); + const includedFields = includedFieldsString + ? includedFieldsString.split(',') + : []; + const selectedFields = selectedFieldsString + ? selectedFieldsString.split(',') + : []; const missingFields = selectedFields .filter( field => @@ -70,6 +74,7 @@ const load = function( }, outputFields: { [getGraphQLQueryName]: { + description: 'This is the created object.', type: new GraphQLNonNull( classGraphQLOutputType || defaultGraphQLTypes.OBJECT ), @@ -147,22 +152,29 @@ const load = function( if (isUpdateEnabled) { const updateGraphQLMutationName = `update${graphQLClassName}`; - parseGraphQLSchema.addGraphQLMutation(updateGraphQLMutationName, { + const updateGraphQLMutation = mutationWithClientMutationId({ + name: `Update${graphQLClassName}`, description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${graphQLClassName} class.`, - args: { + inputFields: { id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT, fields: { - description: 'These are the fields used to update the object.', + description: + 'These are the fields that will be used to update the object.', type: classGraphQLUpdateType || defaultGraphQLTypes.OBJECT, }, }, - type: new GraphQLNonNull( - classGraphQLOutputType || defaultGraphQLTypes.OBJECT - ), - async resolve(_source, args, context, mutationInfo) { + outputFields: { + [getGraphQLQueryName]: { + description: 'This is the updated object.', + type: new GraphQLNonNull( + classGraphQLOutputType || defaultGraphQLTypes.OBJECT + ), + }, + }, + mutateAndGetPayload: async (args, context, mutationInfo) => { try { - let { id } = args; - const { fields } = args; + let { id, fields } = args; + if (!fields) fields = {}; const { config, auth, info } = context; const globalIdObject = fromGlobalId(id); @@ -185,15 +197,18 @@ const load = function( auth, info ); - const selectedFields = getFieldNames(mutationInfo); - const { keys, include } = extractKeysAndInclude(selectedFields); + const selectedFields = getFieldNames(mutationInfo) + .filter(field => field.startsWith(`${getGraphQLQueryName}.`)) + .map(field => field.replace(`${getGraphQLQueryName}.`, '')); + const { keys, include } = extractKeysAndInclude(selectedFields); const { keys: requiredKeys, needGet } = getOnlyRequiredFields( fields, keys, include, ['id', 'objectId', 'updatedAt'] ); + let optimizedObject = {}; if (needGet) { optimizedObject = await objectsQueries.getObject( @@ -209,16 +224,30 @@ const load = function( ); } return { - objectId: id, - ...updatedObject, - ...fields, - ...optimizedObject, + [getGraphQLQueryName]: { + objectId: id, + ...updatedObject, + ...fields, + ...optimizedObject, + }, }; } catch (e) { parseGraphQLSchema.handleError(e); } }, }); + + if ( + parseGraphQLSchema.addGraphQLType( + updateGraphQLMutation.args.input.type.ofType + ) && + parseGraphQLSchema.addGraphQLType(updateGraphQLMutation.type) + ) { + parseGraphQLSchema.addGraphQLMutation( + updateGraphQLMutationName, + updateGraphQLMutation + ); + } } if (isDestroyEnabled) { From ab8a49a16e5f08fa8a9d2326832dbe504c42290e Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Tue, 24 Sep 2019 11:56:38 -0700 Subject: [PATCH 20/27] Client mutation id on delete object mutation --- spec/ParseGraphQLServer.spec.js | 249 +++++++++++++++++---- src/GraphQL/loaders/parseClassMutations.js | 47 +++- 2 files changed, 244 insertions(+), 52 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 877911b67d..9c0597a274 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1147,6 +1147,108 @@ describe('ParseGraphQLServer', () => { 'someClass', ]); }); + + it('should have clientMutationId in custom update object mutation input', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateSomeClassInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(createObjectInputFields).toEqual([ + 'clientMutationId', + 'fields', + 'id', + ]); + }); + + it('should have clientMutationId in custom update object mutation payload', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "UpdateSomeClassPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(createObjectPayloadFields).toEqual([ + 'clientMutationId', + 'someClass', + ]); + }); + + it('should have clientMutationId in custom delete object mutation input', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectInputFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteSomeClassInput") { + inputFields { + name + } + } + } + `, + })).data['__type'].inputFields + .map(field => field.name) + .sort(); + + expect(createObjectInputFields).toEqual(['clientMutationId', 'id']); + }); + + it('should have clientMutationId in custom delete object mutation payload', async () => { + const obj = new Parse.Object('SomeClass'); + await obj.save(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const createObjectPayloadFields = (await apolloClient.query({ + query: gql` + query { + __type(name: "DeleteSomeClassPayload") { + fields { + name + } + } + } + `, + })).data['__type'].fields + .map(field => field.name) + .sort(); + + expect(createObjectPayloadFields).toEqual([ + 'clientMutationId', + 'someClass', + ]); + }); }); describe('Parse Class Types', () => { @@ -1444,8 +1546,8 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation DeleteCustomer($id: ID!) { - deleteCustomer(id: $id) { - id + deleteCustomer(input: { id: $id }) { + clientMutationId } } `, @@ -1532,8 +1634,8 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation DeleteSuperCar($id: ID!) { - deleteSuperCar(id: $id) { - id + deleteSuperCar(input: { id: $id }) { + clientMutationId } } `, @@ -1578,7 +1680,9 @@ describe('ParseGraphQLServer', () => { apolloClient.query({ query: gql` mutation DeleteCustomer($id: ID!, $foo: String!) { - deleteCustomer(id: $id) + deleteCustomer(input: { id: $id }) { + clientMutationId + } } `, variables: { @@ -2355,20 +2459,36 @@ describe('ParseGraphQLServer', () => { $id5: ID! $id6: ID! ) { - secondaryObject1: deleteSecondaryObject(id: $id1) { - id - objectId - someField + secondaryObject1: deleteSecondaryObject( + input: { id: $id1 } + ) { + secondaryObject { + id + objectId + someField + } } - secondaryObject3: deleteSecondaryObject(id: $id3) { - objectId - someField + secondaryObject3: deleteSecondaryObject( + input: { id: $id3 } + ) { + secondaryObject { + objectId + someField + } } - secondaryObject5: deleteSecondaryObject(id: $id5) { - id + secondaryObject5: deleteSecondaryObject( + input: { id: $id5 } + ) { + secondaryObject { + id + } } - secondaryObject6: deleteSecondaryObject(id: $id6) { - objectId + secondaryObject6: deleteSecondaryObject( + input: { id: $id6 } + ) { + secondaryObject { + objectId + } } } `, @@ -2466,14 +2586,19 @@ describe('ParseGraphQLServer', () => { `, variables: { id1: - deleteSecondaryObjectsResult.data.secondaryObject1.objectId, + deleteSecondaryObjectsResult.data.secondaryObject1 + .secondaryObject.objectId, id2: getSecondaryObjectsResult.data.secondaryObject2.id, id3: - deleteSecondaryObjectsResult.data.secondaryObject3.objectId, + deleteSecondaryObjectsResult.data.secondaryObject3 + .secondaryObject.objectId, id4: getSecondaryObjectsResult.data.secondaryObject4.objectId, - id5: deleteSecondaryObjectsResult.data.secondaryObject5.id, + id5: + deleteSecondaryObjectsResult.data.secondaryObject5 + .secondaryObject.id, id6: - deleteSecondaryObjectsResult.data.secondaryObject6.objectId, + deleteSecondaryObjectsResult.data.secondaryObject6 + .secondaryObject.objectId, }, context: { headers: { @@ -5951,6 +6076,7 @@ describe('ParseGraphQLServer', () => { describe('Delete', () => { it('should return a specific type using class specific mutation', async () => { + const clientMutationId = uuidv4(); const obj = new Parse.Object('Customer'); obj.set('someField1', 'someField1Value1'); obj.set('someField2', 'someField2Value1'); @@ -5960,25 +6086,36 @@ describe('ParseGraphQLServer', () => { const result = await apolloClient.mutate({ mutation: gql` - mutation DeleteCustomer($id: ID!) { - deleteCustomer(id: $id) { - id - objectId - someField1 - someField2 + mutation DeleteCustomer($input: DeleteCustomerInput!) { + deleteCustomer(input: $input) { + clientMutationId + customer { + id + objectId + someField1 + someField2 + } } } `, variables: { - id: obj.id, + input: { + clientMutationId, + id: obj.id, + }, }, }); - expect(result.data.deleteCustomer.objectId).toEqual(obj.id); - expect(result.data.deleteCustomer.someField1).toEqual( + expect(result.data.deleteCustomer.clientMutationId).toEqual( + clientMutationId + ); + expect(result.data.deleteCustomer.customer.objectId).toEqual( + obj.id + ); + expect(result.data.deleteCustomer.customer.someField1).toEqual( 'someField1Value1' ); - expect(result.data.deleteCustomer.someField2).toEqual( + expect(result.data.deleteCustomer.customer.someField2).toEqual( 'someField2Value1' ); @@ -5998,8 +6135,11 @@ describe('ParseGraphQLServer', () => { mutation DeleteSomeObject( $id: ID! ) { - delete: delete${className}(id: $id) { - objectId + delete: delete${className}(input: { id: $id }) { + ${className.charAt(0).toLowerCase() + + className.slice(1)} { + objectId + } } } `, @@ -6035,7 +6175,10 @@ describe('ParseGraphQLServer', () => { }) ); expect( - (await deleteObject(object4.className, object4.id)).data.delete + (await deleteObject(object4.className, object4.id)).data.delete[ + object4.className.charAt(0).toLowerCase() + + object4.className.slice(1) + ] ).toEqual({ objectId: object4.id, __typename: 'PublicClass' }); await expectAsync( object4.fetch({ useMasterKey: true }) @@ -6043,7 +6186,10 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object1.className, object1.id, { 'X-Parse-Master-Key': 'test', - })).data.delete + })).data.delete[ + object1.className.charAt(0).toLowerCase() + + object1.className.slice(1) + ] ).toEqual({ objectId: object1.id, __typename: 'GraphQLClass' }); await expectAsync( object1.fetch({ useMasterKey: true }) @@ -6051,7 +6197,10 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object2.className, object2.id, { 'X-Parse-Session-Token': user2.getSessionToken(), - })).data.delete + })).data.delete[ + object2.className.charAt(0).toLowerCase() + + object2.className.slice(1) + ] ).toEqual({ objectId: object2.id, __typename: 'GraphQLClass' }); await expectAsync( object2.fetch({ useMasterKey: true }) @@ -6059,7 +6208,10 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object3.className, object3.id, { 'X-Parse-Session-Token': user5.getSessionToken(), - })).data.delete + })).data.delete[ + object3.className.charAt(0).toLowerCase() + + object3.className.slice(1) + ] ).toEqual({ objectId: object3.id, __typename: 'GraphQLClass' }); await expectAsync( object3.fetch({ useMasterKey: true }) @@ -6077,8 +6229,11 @@ describe('ParseGraphQLServer', () => { mutation DeleteSomeObject( $id: ID! ) { - delete${className}(id: $id) { - objectId + delete${className}(input: { id: $id }) { + ${className.charAt(0).toLowerCase() + + className.slice(1)} { + objectId + } } } `, @@ -6116,6 +6271,9 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object4.className, object4.id)).data[ `delete${object4.className}` + ][ + object4.className.charAt(0).toLowerCase() + + object4.className.slice(1) ].objectId ).toEqual(object4.id); await expectAsync( @@ -6124,7 +6282,10 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object1.className, object1.id, { 'X-Parse-Master-Key': 'test', - })).data[`delete${object1.className}`].objectId + })).data[`delete${object1.className}`][ + object1.className.charAt(0).toLowerCase() + + object1.className.slice(1) + ].objectId ).toEqual(object1.id); await expectAsync( object1.fetch({ useMasterKey: true }) @@ -6132,7 +6293,10 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object2.className, object2.id, { 'X-Parse-Session-Token': user2.getSessionToken(), - })).data[`delete${object2.className}`].objectId + })).data[`delete${object2.className}`][ + object2.className.charAt(0).toLowerCase() + + object2.className.slice(1) + ].objectId ).toEqual(object2.id); await expectAsync( object2.fetch({ useMasterKey: true }) @@ -6140,7 +6304,10 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object3.className, object3.id, { 'X-Parse-Session-Token': user5.getSessionToken(), - })).data[`delete${object3.className}`].objectId + })).data[`delete${object3.className}`][ + object3.className.charAt(0).toLowerCase() + + object3.className.slice(1) + ].objectId ).toEqual(object3.id); await expectAsync( object3.fetch({ useMasterKey: true }) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index f537bf4f7c..68b9c03d61 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -252,20 +252,24 @@ const load = function( if (isDestroyEnabled) { const deleteGraphQLMutationName = `delete${graphQLClassName}`; - parseGraphQLSchema.addGraphQLMutation(deleteGraphQLMutationName, { + const deleteGraphQLMutation = mutationWithClientMutationId({ + name: `Delete${graphQLClassName}`, description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${graphQLClassName} class.`, - args: { + inputFields: { id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT, }, - type: new GraphQLNonNull( - classGraphQLOutputType || defaultGraphQLTypes.OBJECT - ), - async resolve(_source, args, context, mutationInfo) { + outputFields: { + [getGraphQLQueryName]: { + description: 'This is the deleted object.', + type: new GraphQLNonNull( + classGraphQLOutputType || defaultGraphQLTypes.OBJECT + ), + }, + }, + mutateAndGetPayload: async (args, context, mutationInfo) => { try { let { id } = args; const { config, auth, info } = context; - const selectedFields = getFieldNames(mutationInfo); - const { keys, include } = extractKeysAndInclude(selectedFields); const globalIdObject = fromGlobalId(id); @@ -273,10 +277,14 @@ const load = function( id = globalIdObject.id; } + const selectedFields = getFieldNames(mutationInfo) + .filter(field => field.startsWith(`${getGraphQLQueryName}.`)) + .map(field => field.replace(`${getGraphQLQueryName}.`, '')); + const { keys, include } = extractKeysAndInclude(selectedFields); let optimizedObject = {}; - const splitedKeys = keys.split(','); if ( - splitedKeys.filter(key => !['id', 'objectId'].includes(key)) + keys && + keys.split(',').filter(key => !['id', 'objectId'].includes(key)) .length > 0 ) { optimizedObject = await objectsQueries.getObject( @@ -298,12 +306,29 @@ const load = function( auth, info ); - return { objectId: id, ...optimizedObject }; + return { + [getGraphQLQueryName]: { + objectId: id, + ...optimizedObject, + }, + }; } catch (e) { parseGraphQLSchema.handleError(e); } }, }); + + if ( + parseGraphQLSchema.addGraphQLType( + deleteGraphQLMutation.args.input.type.ofType + ) && + parseGraphQLSchema.addGraphQLType(deleteGraphQLMutation.type) + ) { + parseGraphQLSchema.addGraphQLMutation( + deleteGraphQLMutationName, + deleteGraphQLMutation + ); + } } }; From 789731d024554e86a6aac9451df998d4f64cdbe1 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 26 Sep 2019 11:12:02 -0700 Subject: [PATCH 21/27] Introducing connections --- src/GraphQL/ParseGraphQLSchema.js | 11 +- src/GraphQL/helpers/objectsQueries.js | 186 +++++++++++++++++++++-- src/GraphQL/loaders/parseClassQueries.js | 22 ++- src/GraphQL/loaders/parseClassTypes.js | 68 +++++---- src/GraphQL/transformers/mutation.js | 9 +- 5 files changed, 246 insertions(+), 50 deletions(-) diff --git a/src/GraphQL/ParseGraphQLSchema.js b/src/GraphQL/ParseGraphQLSchema.js index 585ec4d84d..d01f5eee0a 100644 --- a/src/GraphQL/ParseGraphQLSchema.js +++ b/src/GraphQL/ParseGraphQLSchema.js @@ -51,6 +51,7 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [ 'UpdateClassPayload', 'DeleteClassInput', 'DeleteClassPayload', + 'PageInfo', ]; const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes']; const RESERVED_GRAPHQL_MUTATION_NAMES = [ @@ -243,10 +244,16 @@ class ParseGraphQLSchema { return this.graphQLSchema; } - addGraphQLType(type, throwError = false, ignoreReserved = false) { + addGraphQLType( + type, + throwError = false, + ignoreReserved = false, + ignoreConnection = false + ) { if ( (!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) || - this.graphQLTypes.find(existingType => existingType.name === type.name) + this.graphQLTypes.find(existingType => existingType.name === type.name) || + (!ignoreConnection && type.name.endsWith('Connection')) ) { const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`; if (throwError) { diff --git a/src/GraphQL/helpers/objectsQueries.js b/src/GraphQL/helpers/objectsQueries.js index 423e998906..ae9a12187a 100644 --- a/src/GraphQL/helpers/objectsQueries.js +++ b/src/GraphQL/helpers/objectsQueries.js @@ -1,4 +1,5 @@ import Parse from 'parse/node'; +import { offsetToCursor, cursorToOffset } from 'graphql-relay'; import rest from '../../rest'; import { transformQueryInputToParse } from '../transformers/query'; @@ -51,8 +52,11 @@ const findObjects = async ( className, where, order, - skip, - limit, + skipInput, + first, + after, + last, + before, keys, include, includeAll, @@ -70,9 +74,48 @@ const findObjects = async ( } transformQueryInputToParse(where, fields, className); + const skipAndLimitCalculation = calculateSkipAndLimit( + skipInput, + first, + after, + last, + before, + config.maxLimit + ); + let { skip } = skipAndLimitCalculation; + const { limit, needToPreCount } = skipAndLimitCalculation; + let preCount = undefined; + if (needToPreCount) { + const preCountOptions = { + limit: 0, + count: true, + }; + if (readPreference) { + preCountOptions.readPreference = readPreference; + } + if (Object.keys(where).length > 0 && subqueryReadPreference) { + preCountOptions.subqueryReadPreference = subqueryReadPreference; + } + preCount = (await rest.find( + config, + auth, + className, + where, + preCountOptions, + info.clientSDK + )).count; + if ((skip || 0) + limit < preCount) { + skip = preCount - limit; + } + } + const options = {}; - if (selectedFields.includes('results')) { + if ( + selectedFields.find( + field => field.startsWith('edges.') || field.startsWith('pageInfo.') + ) + ) { if (limit || limit === 0) { options.limit = limit; } @@ -104,7 +147,12 @@ const findObjects = async ( options.limit = 0; } - if (selectedFields.includes('count')) { + if ( + (selectedFields.includes('count') || + selectedFields.includes('pageInfo.hasPreviousPage') || + selectedFields.includes('pageInfo.hasNextPage')) && + !needToPreCount + ) { options.count = true; } @@ -115,14 +163,126 @@ const findObjects = async ( options.subqueryReadPreference = subqueryReadPreference; } - return await rest.find( - config, - auth, - className, - where, - options, - info.clientSDK - ); + let results, count; + if (options.count || !options.limit || (options.limit && options.limit > 0)) { + const findResult = await rest.find( + config, + auth, + className, + where, + options, + info.clientSDK + ); + results = findResult.results; + count = findResult.count; + } + + let edges = null; + let pageInfo = null; + if (results) { + edges = results.map((result, index) => ({ + cursor: offsetToCursor((skip || 0) + index), + node: result, + })); + + pageInfo = { + hasPreviousPage: + ((preCount && preCount > 0) || (count && count > 0)) && + skip !== undefined && + skip > 0, + startCursor: offsetToCursor(skip || 0), + endCursor: offsetToCursor((skip || 0) + (results.length || 1) - 1), + hasNextPage: (preCount || count) > (skip || 0) + results.length, + }; + } + + return { + edges, + pageInfo, + count: preCount || count, + }; +}; + +const calculateSkipAndLimit = ( + skipInput, + first, + after, + last, + before, + maxLimit +) => { + let skip = undefined; + let limit = undefined; + let needToPreCount = false; + if (skipInput || skipInput === 0) { + if (skipInput < 0) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Skip should be a positive number' + ); + } + skip = skipInput; + } + if (after) { + after = cursorToOffset(after); + if ((!after && after !== 0) || after < 0) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'After is not a valid cursor' + ); + } + skip = (skip || 0) + (after + 1); + } + if (first || first === 0) { + if (first < 0) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'First should be a positive number' + ); + } + limit = first; + } + if (before || before === 0) { + before = cursorToOffset(before); + if ((!before && before !== 0) || before < 0) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Before is not a valid cursor' + ); + } + if ((skip || 0) >= before) { + limit = 0; + } else if ((!limit && limit !== 0) || (skip || 0) + limit > before) { + limit = before - (skip || 0); + } + } + if (last || last === 0) { + if (last < 0) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Last should be a positive number' + ); + } + if (last > maxLimit) { + last = maxLimit; + } + if (limit || limit === 0) { + if (last < limit) { + skip = (skip || 0) + (limit - last); + limit = last; + } + } else if (last === 0) { + limit = 0; + } else { + limit = last; + needToPreCount = true; + } + } + return { + skip, + limit, + needToPreCount, + }; }; -export { getObject, findObjects }; +export { getObject, findObjects, calculateSkipAndLimit }; diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 475e92dfad..9ec5b99967 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -94,7 +94,16 @@ const load = function( ), async resolve(_source, args, context, queryInfo) { try { - const { where, order, skip, limit, options } = args; + const { + where, + order, + skip, + first, + after, + last, + before, + options, + } = args; const { readPreference, includeReadPreference, @@ -105,8 +114,8 @@ const load = function( const { keys, include } = extractKeysAndInclude( selectedFields - .filter(field => field.includes('.')) - .map(field => field.slice(field.indexOf('.') + 1)) + .filter(field => field.startsWith('edges.node.')) + .map(field => field.replace('edges.node.', '')) ); const parseOrder = order && order.join(','); @@ -115,7 +124,10 @@ const load = function( where, parseOrder, skip, - limit, + first, + after, + last, + before, keys, include, false, @@ -125,7 +137,7 @@ const load = function( config, auth, info, - selectedFields.map(field => field.split('.', 1)[0]), + selectedFields, parseClass.fields ); } catch (e) { diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index 00e7df9af6..133d0b934f 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -7,7 +7,11 @@ import { GraphQLNonNull, GraphQLEnumType, } from 'graphql'; -import { globalIdField } from 'graphql-relay'; +import { + globalIdField, + connectionArgs, + connectionDefinitions, +} from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsQueries from '../helpers/objectsQueries'; @@ -371,7 +375,7 @@ const load = ( : GraphQLString, }, skip: defaultGraphQLTypes.SKIP_ATT, - limit: defaultGraphQLTypes.LIMIT_ATT, + ...connectionArgs, options: defaultGraphQLTypes.READ_OPTIONS_ATT, }; @@ -407,7 +411,16 @@ const load = ( type, async resolve(source, args, context, queryInfo) { try { - const { where, order, skip, limit, options } = args; + const { + where, + order, + skip, + first, + after, + last, + before, + options, + } = args; const { readPreference, includeReadPreference, @@ -418,9 +431,11 @@ const load = ( const { keys, include } = extractKeysAndInclude( selectedFields - .filter(field => field.includes('.')) - .map(field => field.slice(field.indexOf('.') + 1)) + .filter(field => field.startsWith('edges.node.')) + .map(field => field.replace('edges.node.', '')) ); + const parseOrder = order && order.join(','); + return await objectsQueries.findObjects( source[field].className, { @@ -434,9 +449,12 @@ const load = ( }, ...(where || {}), }, - order, + parseOrder, skip, - limit, + first, + after, + last, + before, keys, include, false, @@ -446,8 +464,11 @@ const load = ( config, auth, info, - selectedFields.map(field => field.split('.', 1)[0]), - parseClass.fields + selectedFields, + parseGraphQLSchema.parseClasses.find( + parseClass => + parseClass.className === source[field].className + ).fields ); } catch (e) { parseGraphQLSchema.handleError(e); @@ -518,27 +539,20 @@ const load = ( classGraphQLOutputType ); - const classGraphQLFindResultTypeName = `${graphQLClassName}FindResult`; - let classGraphQLFindResultType = new GraphQLObjectType({ - name: classGraphQLFindResultTypeName, - description: `The ${classGraphQLFindResultTypeName} object type is used in the ${graphQLClassName} find query to return the data of the matched objects.`, - fields: { - results: { - description: 'This is the objects returned by the query', - type: new GraphQLNonNull( - new GraphQLList( - new GraphQLNonNull( - classGraphQLOutputType || defaultGraphQLTypes.OBJECT - ) - ) - ), - }, + const { connectionType, edgeType } = connectionDefinitions({ + name: graphQLClassName, + connectionFields: { count: defaultGraphQLTypes.COUNT_ATT, }, + nodeType: classGraphQLOutputType || defaultGraphQLTypes.OBJECT, }); - classGraphQLFindResultType = parseGraphQLSchema.addGraphQLType( - classGraphQLFindResultType - ); + let classGraphQLFindResultType = undefined; + if ( + parseGraphQLSchema.addGraphQLType(edgeType) && + parseGraphQLSchema.addGraphQLType(connectionType, false, false, true) + ) { + classGraphQLFindResultType = connectionType; + } parseGraphQLSchema.parseClassTypes[className] = { classGraphQLPointerType, diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 3fe29ef7e4..c1179bb67f 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -1,3 +1,4 @@ +import Parse from 'parse/node'; import { fromGlobalId } from 'graphql-relay'; import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; import * as objectsMutations from '../helpers/objectsMutations'; @@ -82,8 +83,9 @@ const transformers = { { 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}` + throw new Parse.Error( + Parse.Error.INVALID_POINTER, + `You need to provide at least one operation on the relation mutation of field ${field}` ); const op = { @@ -160,7 +162,8 @@ const transformers = { { config, auth, info } ) => { if (Object.keys(value) > 1 || Object.keys(value) === 0) - throw new Error( + throw new Parse.Error( + Parse.Error.INVALID_POINTER, `You need to provide link OR createLink on the pointer mutation of field ${field}` ); From f6635cc7709668c5b7e3410b1bf04a411db4d96b Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 26 Sep 2019 12:01:36 -0700 Subject: [PATCH 22/27] Fix tests --- spec/ParseGraphQLServer.spec.js | 636 ++++++++++++--------- src/GraphQL/loaders/defaultGraphQLTypes.js | 15 - src/GraphQL/loaders/parseClassQueries.js | 2 +- src/GraphQL/transformers/outputType.js | 2 +- 4 files changed, 361 insertions(+), 294 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 9c0597a274..d5acd5fa7f 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -661,26 +661,6 @@ describe('ParseGraphQLServer', () => { ]); }); - it('should have FindResult object type', async () => { - const findResultType = (await apolloClient.query({ - query: gql` - query FindResultType { - __type(name: "FindResult") { - kind - fields { - name - } - } - } - `, - })).data['__type']; - expect(findResultType.kind).toEqual('OBJECT'); - expect(findResultType.fields.map(name => name.name).sort()).toEqual([ - 'count', - 'results', - ]); - }); - it('should have GraphQLUpload object type', async () => { const graphQLUploadType = (await apolloClient.query({ query: gql` @@ -714,7 +694,6 @@ describe('ParseGraphQLServer', () => { 'ParseObject', 'Date', 'FileInfo', - 'FindResult', 'ReadPreference', 'Upload', ]; @@ -1272,10 +1251,10 @@ describe('ParseGraphQLServer', () => { 'RoleWhereInput', 'CreateRoleFieldsInput', 'UpdateRoleFieldsInput', - 'RoleFindResult', + 'RoleConnection', 'User', 'UserWhereInput', - 'UserFindResult', + 'UserConnection', 'CreateUserFieldsInput', 'UpdateUserFieldsInput', ]; @@ -2025,8 +2004,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSuperCar { superCars(order: [engine_ASC]) { - results { - id + edges { + node { + id + } } } } @@ -2038,8 +2019,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSuperCar { superCars(order: [engine_DESC]) { - results { - id + edges { + node { + id + } } } } @@ -2051,8 +2034,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSuperCar { superCars(order: [mileage_DESC]) { - results { - id + edges { + node { + id + } } } } @@ -2065,8 +2050,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSuperCar { superCars(order: [mileage_ASC]) { - results { - id + edges { + node { + id + } } } } @@ -2078,8 +2065,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSuperCar { superCars(order: [doors_ASC]) { - results { - id + edges { + node { + id + } } } } @@ -2091,8 +2080,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSuperCar { superCars(order: [price_DESC]) { - results { - id + edges { + node { + id + } } } } @@ -2104,8 +2095,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSuperCar { superCars(order: [price_ASC, doors_DESC]) { - results { - id + edges { + node { + id + } } } } @@ -2181,19 +2174,21 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeClass { someClasses(order: [createdAt_ASC]) { - results { - id - objectId + edges { + node { + id + objectId + } } } } `, }); - expect(findResult.data.someClasses.results[0].objectId).toBe( + expect(findResult.data.someClasses.edges[0].node.objectId).toBe( obj1.id ); - expect(findResult.data.someClasses.results[1].objectId).toBe( + expect(findResult.data.someClasses.edges[1].node.objectId).toBe( obj2.id ); @@ -2217,18 +2212,18 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id1: findResult.data.someClasses.results[0].id, - id2: findResult.data.someClasses.results[1].id, + id1: findResult.data.someClasses.edges[0].node.id, + id2: findResult.data.someClasses.edges[1].node.id, }, }); expect(nodeResult.data.node1.id).toBe( - findResult.data.someClasses.results[0].id + findResult.data.someClasses.edges[0].node.id ); expect(nodeResult.data.node1.objectId).toBe(obj1.id); expect(nodeResult.data.node1.someField).toBe('some value 1'); expect(nodeResult.data.node2.id).toBe( - findResult.data.someClasses.results[1].id + findResult.data.someClasses.edges[1].node.id ); expect(nodeResult.data.node2.objectId).toBe(obj2.id); expect(nodeResult.data.node2.someField).toBe('some value 2'); @@ -2575,10 +2570,12 @@ describe('ParseGraphQLServer', () => { } order: [id_ASC, objectId_ASC] ) { - results { - id - objectId - someField + edges { + node { + id + objectId + someField + } } count } @@ -2611,20 +2608,22 @@ describe('ParseGraphQLServer', () => { findSecondaryObjectsResult.data.secondaryObjects.count ).toEqual(2); expect( - findSecondaryObjectsResult.data.secondaryObjects.results - .map(value => value.someField) + findSecondaryObjectsResult.data.secondaryObjects.edges + .map(value => value.node.someField) .sort() ).toEqual(['some value 22', 'some value 44']); expect( - findSecondaryObjectsResult.data.secondaryObjects.results[0].id + findSecondaryObjectsResult.data.secondaryObjects.edges[0].node + .id ).toBeLessThan( - findSecondaryObjectsResult.data.secondaryObjects.results[1].id + findSecondaryObjectsResult.data.secondaryObjects.edges[1].node + .id ); expect( - findSecondaryObjectsResult.data.secondaryObjects.results[0] + findSecondaryObjectsResult.data.secondaryObjects.edges[0].node .objectId ).toBeLessThan( - findSecondaryObjectsResult.data.secondaryObjects.results[1] + findSecondaryObjectsResult.data.secondaryObjects.edges[1].node .objectId ); @@ -2664,10 +2663,12 @@ describe('ParseGraphQLServer', () => { someField } relationField { - results { - id - objectId - someField + edges { + node { + id + objectId + someField + } } } } @@ -2728,10 +2729,12 @@ describe('ParseGraphQLServer', () => { someField } relationField { - results { - id - objectId - someField + edges { + node { + id + objectId + someField + } } } } @@ -2771,8 +2774,8 @@ describe('ParseGraphQLServer', () => { .pointerField.someField ).toEqual('some value 22'); expect( - createPrimaryObjectResult.data.createPrimaryObject.primaryObject.relationField.results - .map(value => value.someField) + createPrimaryObjectResult.data.createPrimaryObject.primaryObject.relationField.edges + .map(value => value.node.someField) .sort() ).toEqual(['some value 22', 'some value 44']); expect( @@ -2793,7 +2796,7 @@ describe('ParseGraphQLServer', () => { ).toEqual('some value 44'); expect( updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject - .relationField.results + .relationField.edges ).toEqual([]); } catch (e) { handleError(e); @@ -4639,25 +4642,27 @@ describe('ParseGraphQLServer', () => { query: gql` query FindCustomer { customers { - results { - objectId - someField - createdAt - updatedAt + edges { + node { + objectId + someField + createdAt + updatedAt + } } } } `, }); - expect(result.data.customers.results.length).toEqual(2); + expect(result.data.customers.edges.length).toEqual(2); - result.data.customers.results.forEach(resultObj => { - const obj = resultObj.objectId === obj1.id ? obj1 : obj2; - expect(resultObj.objectId).toEqual(obj.id); - expect(resultObj.someField).toEqual(obj.get('someField')); - expect(new Date(resultObj.createdAt)).toEqual(obj.createdAt); - expect(new Date(resultObj.updatedAt)).toEqual(obj.updatedAt); + result.data.customers.edges.forEach(resultObj => { + const obj = resultObj.node.objectId === obj1.id ? obj1 : obj2; + expect(resultObj.node.objectId).toEqual(obj.id); + expect(resultObj.node.someField).toEqual(obj.get('someField')); + expect(new Date(resultObj.node.createdAt)).toEqual(obj.createdAt); + expect(new Date(resultObj.node.updatedAt)).toEqual(obj.updatedAt); }); }); @@ -4674,9 +4679,11 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObjects { find: ${graphqlClassName} { - results { - id - someField + edges { + node { + id + someField + } } } } @@ -4690,62 +4697,62 @@ describe('ParseGraphQLServer', () => { } expect( - (await findObjects('GraphQLClass')).data.find.results.map( - object => object.someField + (await findObjects('GraphQLClass')).data.find.edges.map( + object => object.node.someField ) ).toEqual([]); expect( - (await findObjects('PublicClass')).data.find.results.map( - object => object.someField + (await findObjects('PublicClass')).data.find.edges.map( + object => object.node.someField ) ).toEqual(['someValue4']); expect( (await findObjects('GraphQLClass', { 'X-Parse-Master-Key': 'test', - })).data.find.results - .map(object => object.someField) + })).data.find.edges + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2', 'someValue3']); expect( (await findObjects('PublicClass', { 'X-Parse-Master-Key': 'test', - })).data.find.results.map(object => object.someField) + })).data.find.edges.map(object => object.node.someField) ).toEqual(['someValue4']); expect( (await findObjects('GraphQLClass', { 'X-Parse-Session-Token': user1.getSessionToken(), - })).data.find.results - .map(object => object.someField) + })).data.find.edges + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2', 'someValue3']); expect( (await findObjects('PublicClass', { 'X-Parse-Session-Token': user1.getSessionToken(), - })).data.find.results.map(object => object.someField) + })).data.find.edges.map(object => object.node.someField) ).toEqual(['someValue4']); expect( (await findObjects('GraphQLClass', { 'X-Parse-Session-Token': user2.getSessionToken(), - })).data.find.results - .map(object => object.someField) + })).data.find.edges + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2', 'someValue3']); expect( (await findObjects('GraphQLClass', { 'X-Parse-Session-Token': user3.getSessionToken(), - })).data.find.results - .map(object => object.someField) + })).data.find.edges + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue3']); expect( (await findObjects('GraphQLClass', { 'X-Parse-Session-Token': user4.getSessionToken(), - })).data.find.results.map(object => object.someField) + })).data.find.edges.map(object => object.node.someField) ).toEqual([]); expect( (await findObjects('GraphQLClass', { 'X-Parse-Session-Token': user5.getSessionToken(), - })).data.find.results.map(object => object.someField) + })).data.find.edges.map(object => object.node.someField) ).toEqual(['someValue3']); }); @@ -4758,8 +4765,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObjects($where: GraphQLClassWhereInput) { graphQLClasses(where: $where) { - results { - someField + edges { + node { + someField + } } } } @@ -4791,8 +4800,8 @@ describe('ParseGraphQLServer', () => { }); expect( - result.data.graphQLClasses.results - .map(object => object.someField) + result.data.graphQLClasses.edges + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue3']); }); @@ -4806,8 +4815,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObjects($where: GraphQLClassWhereInput) { graphQLClasses(where: $where) { - results { - someField + edges { + node { + someField + } } } } @@ -4826,9 +4837,9 @@ describe('ParseGraphQLServer', () => { }, }); - const { results } = result.data.graphQLClasses; - expect(results.length).toBe(1); - expect(results[0].someField).toEqual('someValue3'); + const { edges } = result.data.graphQLClasses; + expect(edges.length).toBe(1); + expect(edges[0].node.someField).toEqual('someValue3'); }); it('should support OR operation', async () => { @@ -4847,8 +4858,10 @@ describe('ParseGraphQLServer', () => { ] } ) { - results { - someField + edges { + node { + someField + } } } } @@ -4861,8 +4874,8 @@ describe('ParseGraphQLServer', () => { }); expect( - result.data.graphQLClasses.results - .map(object => object.someField) + result.data.graphQLClasses.edges + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2']); }); @@ -4882,8 +4895,10 @@ describe('ParseGraphQLServer', () => { $where: FullTextSearchTestWhereInput ) { fullTextSearchTests(where: $where) { - results { - objectId + edges { + node { + objectId + } } } } @@ -4907,14 +4922,14 @@ describe('ParseGraphQLServer', () => { }); expect( - result.data.fullTextSearchTests.results[0].objectId + result.data.fullTextSearchTests.edges[0].node.objectId ).toEqual(obj.id); } catch (e) { handleError(e); } }); - it('should support order, skip and limit arguments', async () => { + it('should support order, skip and first arguments', async () => { const promises = []; for (let i = 0; i < 100; i++) { const obj = new Parse.Object('SomeClass'); @@ -4932,16 +4947,18 @@ describe('ParseGraphQLServer', () => { $where: SomeClassWhereInput $order: [SomeClassOrder!] $skip: Int - $limit: Int + $first: Int ) { find: someClasses( where: $where order: $order skip: $skip - limit: $limit + first: $first ) { - results { - someField + edges { + node { + someField + } } } } @@ -4954,14 +4971,13 @@ describe('ParseGraphQLServer', () => { }, order: ['numberField_DESC', 'someField_ASC'], skip: 4, - limit: 2, + first: 2, }, }); - expect(result.data.find.results.map(obj => obj.someField)).toEqual([ - 'someValue14', - 'someValue17', - ]); + expect( + result.data.find.edges.map(obj => obj.node.someField) + ).toEqual(['someValue14', 'someValue17']); }); it('should support count', async () => { @@ -4991,11 +5007,13 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObjects( $where: GraphQLClassWhereInput - $limit: Int + $first: Int ) { - find: graphQLClasses(where: $where, limit: $limit) { - results { - id + find: graphQLClasses(where: $where, first: $first) { + edges { + node { + id + } } count } @@ -5003,7 +5021,7 @@ describe('ParseGraphQLServer', () => { `, variables: { where, - limit: 0, + first: 0, }, context: { headers: { @@ -5012,7 +5030,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.find.results).toEqual([]); + expect(result.data.find.edges).toEqual([]); expect(result.data.find.count).toEqual(2); }); @@ -5057,7 +5075,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.find.results).toBeUndefined(); + expect(result.data.find.edges).toBeUndefined(); expect(result.data.find.count).toEqual(2); }); @@ -5080,10 +5098,12 @@ describe('ParseGraphQLServer', () => { query FindSomeObjects($limit: Int) { find: someClasses( where: { id: { exists: true } } - limit: $limit + first: $limit ) { - results { - id + edges { + node { + id + } } count } @@ -5099,7 +5119,7 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.find.results.length).toEqual(10); + expect(result.data.find.edges.length).toEqual(10); expect(result.data.find.count).toEqual(100); }); @@ -5112,8 +5132,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObject($where: GraphQLClassWhereInput) { find: graphQLClasses(where: $where) { - results { - someField + edges { + node { + someField + } } } } @@ -5134,10 +5156,12 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObject($where: GraphQLClassWhereInput) { find: graphQLClasses(where: $where) { - results { - someField - pointerToUser { - username + edges { + node { + someField + pointerToUser { + username + } } } } @@ -5155,10 +5179,12 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result1.data.find.results[0].someField).toBeDefined(); - expect(result1.data.find.results[0].pointerToUser).toBeUndefined(); - expect(result2.data.find.results[0].someField).toBeDefined(); - expect(result2.data.find.results[0].pointerToUser).toBeDefined(); + expect(result1.data.find.edges[0].node.someField).toBeDefined(); + expect( + result1.data.find.edges[0].node.pointerToUser + ).toBeUndefined(); + expect(result2.data.find.edges[0].node.someField).toBeDefined(); + expect(result2.data.find.edges[0].node.pointerToUser).toBeDefined(); }); it('should support include argument', async () => { @@ -5176,9 +5202,11 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObject($where: GraphQLClassWhereInput) { find: graphQLClasses(where: $where) { - results { - pointerToUser { - id + edges { + node { + pointerToUser { + id + } } } } @@ -5198,9 +5226,11 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObject($where: GraphQLClassWhereInput) { find: graphQLClasses(where: $where) { - results { - pointerToUser { - username + edges { + node { + pointerToUser { + username + } } } } @@ -5216,10 +5246,10 @@ describe('ParseGraphQLServer', () => { }, }); expect( - result1.data.find.results[0].pointerToUser.username + result1.data.find.edges[0].node.pointerToUser.username ).toBeUndefined(); expect( - result2.data.find.results[0].pointerToUser.username + result2.data.find.edges[0].node.pointerToUser.username ).toBeDefined(); }); @@ -5240,9 +5270,11 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObjects { find: graphQLClasses { - results { - pointerToUser { - username + edges { + node { + pointerToUser { + username + } } } } @@ -5295,9 +5327,11 @@ describe('ParseGraphQLServer', () => { find: graphQLClasses( options: { readPreference: SECONDARY } ) { - results { - pointerToUser { - username + edges { + node { + pointerToUser { + username + } } } } @@ -5353,9 +5387,11 @@ describe('ParseGraphQLServer', () => { includeReadPreference: NEAREST } ) { - results { - pointerToUser { - username + edges { + node { + pointerToUser { + username + } } } } @@ -5413,8 +5449,10 @@ describe('ParseGraphQLServer', () => { subqueryReadPreference: NEAREST } ) { - results { - id + edges { + node { + id + } } } } @@ -6703,8 +6741,10 @@ describe('ParseGraphQLServer', () => { } } cars { - results { - id + edges { + node { + id + } } } } @@ -7029,8 +7069,10 @@ describe('ParseGraphQLServer', () => { someClasses( where: { someField: { equalTo: $someFieldValue } } ) { - results { - someField + edges { + node { + someField + } } } } @@ -7043,7 +7085,7 @@ describe('ParseGraphQLServer', () => { expect(typeof getResult.data.someClass.someField).toEqual('string'); expect(getResult.data.someClass.someField).toEqual(someFieldValue); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -7106,8 +7148,10 @@ describe('ParseGraphQLServer', () => { someClasses( where: { someField: { equalTo: $someFieldValue } } ) { - results { - someField + edges { + node { + someField + } } } } @@ -7120,7 +7164,7 @@ describe('ParseGraphQLServer', () => { expect(typeof getResult.data.someClass.someField).toEqual('number'); expect(getResult.data.someClass.someField).toEqual(someFieldValue); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -7183,8 +7227,10 @@ describe('ParseGraphQLServer', () => { someClasses( where: { someField: { equalTo: $someFieldValue } } ) { - results { - someField + edges { + node { + someField + } } } } @@ -7197,7 +7243,7 @@ describe('ParseGraphQLServer', () => { expect(typeof getResult.data.someClass.someField).toEqual('number'); expect(getResult.data.someClass.someField).toEqual(someFieldValue); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -7274,8 +7320,10 @@ describe('ParseGraphQLServer', () => { someFieldFalse: { equalTo: $someFieldValueFalse } } ) { - results { - id + edges { + node { + id + } } } } @@ -7295,7 +7343,7 @@ describe('ParseGraphQLServer', () => { ); expect(getResult.data.someClass.someFieldTrue).toEqual(true); expect(getResult.data.someClass.someFieldFalse).toEqual(false); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -7356,8 +7404,10 @@ describe('ParseGraphQLServer', () => { someField } someClasses(where: { someField: { exists: true } }) { - results { - id + edges { + node { + id + } } } } @@ -7370,7 +7420,7 @@ describe('ParseGraphQLServer', () => { expect(new Date(getResult.data.someClass.someField)).toEqual( someFieldValue ); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -7616,10 +7666,12 @@ describe('ParseGraphQLServer', () => { objectId name companies { - results { - id - objectId - name + edges { + node { + id + objectId + name + } } } } @@ -7646,15 +7698,15 @@ describe('ParseGraphQLServer', () => { expect(result.id).toBeDefined(); expect(result.name).toEqual('imACountry2'); - expect(result.companies.results.length).toEqual(3); + expect(result.companies.edges.length).toEqual(3); expect( - result.companies.results.some(o => o.objectId === company.id) + result.companies.edges.some(o => o.node.objectId === company.id) ).toBeTruthy(); expect( - result.companies.results.some(o => o.name === 'imACompany2') + result.companies.edges.some(o => o.node.name === 'imACompany2') ).toBeTruthy(); expect( - result.companies.results.some(o => o.name === 'imACompany3') + result.companies.edges.some(o => o.node.name === 'imACompany3') ).toBeTruthy(); } ); @@ -7688,13 +7740,17 @@ describe('ParseGraphQLServer', () => { id name companies { - results { - id - name - teams { - results { - id - name + edges { + node { + id + name + teams { + edges { + node { + id + name + } + } } } } @@ -7732,19 +7788,19 @@ describe('ParseGraphQLServer', () => { expect(result.id).toBeDefined(); expect(result.name).toEqual('imACountry2'); - expect(result.companies.results.length).toEqual(2); + expect(result.companies.edges.length).toEqual(2); expect( - result.companies.results.some( + result.companies.edges.some( c => - c.name === 'imACompany2' && - c.teams.results.some(t => t.name === 'imATeam2') + c.node.name === 'imACompany2' && + c.node.teams.edges.some(t => t.node.name === 'imATeam2') ) ).toBeTruthy(); expect( - result.companies.results.some( + result.companies.edges.some( c => - c.name === 'imACompany3' && - c.teams.results.some(t => t.name === 'imATeam3') + c.node.name === 'imACompany3' && + c.node.teams.edges.some(t => t.node.name === 'imATeam3') ) ).toBeTruthy(); }); @@ -7782,10 +7838,12 @@ describe('ParseGraphQLServer', () => { id objectId companies { - results { - id - objectId - name + edges { + node { + id + objectId + name + } } } } @@ -7809,15 +7867,15 @@ describe('ParseGraphQLServer', () => { }); expect(result.objectId).toEqual(country.id); - expect(result.companies.results.length).toEqual(2); + expect(result.companies.edges.length).toEqual(2); expect( - result.companies.results.some(o => o.objectId === company2.id) + result.companies.edges.some(o => o.node.objectId === company2.id) ).toBeTruthy(); expect( - result.companies.results.some(o => o.name === 'imACompany3') + result.companies.edges.some(o => o.node.name === 'imACompany3') ).toBeTruthy(); expect( - result.companies.results.some(o => o.objectId === company1.id) + result.companies.edges.some(o => o.node.objectId === company1.id) ).toBeFalsy(); } ); @@ -7851,9 +7909,11 @@ describe('ParseGraphQLServer', () => { id name companies(where: $where) { - results { - id - name + edges { + node { + id + name + } } } } @@ -7885,9 +7945,9 @@ describe('ParseGraphQLServer', () => { expect(result.id).toBeDefined(); expect(result.name).toEqual('imACountry2'); - expect(result.companies.results.length).toEqual(1); + expect(result.companies.edges.length).toEqual(1); expect( - result.companies.results.some(o => o.name === 'imACompany2') + result.companies.edges.some(o => o.node.name === 'imACompany2') ).toBeTruthy(); } ); @@ -7918,10 +7978,12 @@ describe('ParseGraphQLServer', () => { id objectId companies { - results { - id - objectId - name + edges { + node { + id + objectId + name + } } count } @@ -7934,12 +7996,12 @@ describe('ParseGraphQLServer', () => { }); expect(result1.objectId).toEqual(country.id); - expect(result1.companies.results.length).toEqual(2); + expect(result1.companies.edges.length).toEqual(2); expect( - result1.companies.results.some(o => o.objectId === company1.id) + result1.companies.edges.some(o => o.node.objectId === company1.id) ).toBeTruthy(); expect( - result1.companies.results.some(o => o.objectId === company2.id) + result1.companies.edges.some(o => o.node.objectId === company2.id) ).toBeTruthy(); // With where @@ -7952,10 +8014,12 @@ describe('ParseGraphQLServer', () => { id objectId companies(where: $where) { - results { - id - objectId - name + edges { + node { + id + objectId + name + } } } } @@ -7969,8 +8033,8 @@ describe('ParseGraphQLServer', () => { }, }); expect(result2.objectId).toEqual(country.id); - expect(result2.companies.results.length).toEqual(1); - expect(result2.companies.results[0].objectId).toEqual(company1.id); + expect(result2.companies.edges.length).toEqual(1); + expect(result2.companies.edges[0].node.objectId).toEqual(company1.id); }); it('should support files', async () => { @@ -8099,20 +8163,24 @@ describe('ParseGraphQLServer', () => { findSomeClass1: someClasses( where: { someField: { exists: true } } ) { - results { - someField { - name - url + edges { + node { + someField { + name + url + } } } } findSomeClass2: someClasses( where: { someField: { exists: true } } ) { - results { - someField { - name - url + edges { + node { + someField { + name + url + } } } } @@ -8130,8 +8198,8 @@ describe('ParseGraphQLServer', () => { expect(getResult.data.someClass.someField.url).toEqual( result.data.createFile.fileInfo.url ); - expect(getResult.data.findSomeClass1.results.length).toEqual(1); - expect(getResult.data.findSomeClass2.results.length).toEqual(1); + expect(getResult.data.findSomeClass1.edges.length).toEqual(1); + expect(getResult.data.findSomeClass2.edges.length).toEqual(1); res = await fetch(getResult.data.someClass.someField.url); @@ -8209,9 +8277,11 @@ describe('ParseGraphQLServer', () => { someField } someClasses(where: $where) { - results { - id - someField + edges { + node { + id + someField + } } } } @@ -8229,8 +8299,8 @@ describe('ParseGraphQLServer', () => { expect(someField).toEqual(someFieldValue); // Checks class query results - expect(someClasses.results.length).toEqual(1); - expect(someClasses.results[0].someField).toEqual(someFieldValue); + expect(someClasses.edges.length).toEqual(1); + expect(someClasses.edges[0].node.someField).toEqual(someFieldValue); } catch (e) { handleError(e); } @@ -8331,9 +8401,11 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObject($where: SomeClassWhereInput) { someClasses(where: $where) { - results { - id - someField + edges { + node { + id + someField + } } } } @@ -8347,14 +8419,14 @@ describe('ParseGraphQLServer', () => { const { someClasses } = findResult.data; // Checks class query results - const { results } = someClasses; - expect(results.length).toEqual(2); + const { edges } = someClasses; + expect(edges.length).toEqual(2); expect( - results.find(result => result.id === create1.someClass.id) + edges.find(result => result.node.id === create1.someClass.id).node .someField ).toEqual(someFieldValue); expect( - results.find(result => result.id === create2.someClass.id) + edges.find(result => result.node.id === create2.someClass.id).node .someField ).toEqual(someFieldValue2); } catch (e) { @@ -8427,11 +8499,13 @@ describe('ParseGraphQLServer', () => { } } someClasses(where: { someField: { exists: true } }) { - results { - id - someField { - ... on Element { - value + edges { + node { + id + someField { + ... on Element { + value + } } } } @@ -8448,7 +8522,7 @@ describe('ParseGraphQLServer', () => { expect(someField.map(element => element.value)).toEqual( someFieldValue ); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -8661,9 +8735,11 @@ describe('ParseGraphQLServer', () => { someClasses( where: { someField: { equalTo: $someFieldValue } } ) { - results { - id - someField + edges { + node { + id + someField + } } } } @@ -8676,7 +8752,7 @@ describe('ParseGraphQLServer', () => { expect(typeof getResult.data.someClass.someField).toEqual('string'); expect(getResult.data.someClass.someField).toEqual(someFieldValue); - expect(getResult.data.someClasses.results.length).toEqual(2); + expect(getResult.data.someClasses.edges.length).toEqual(2); } catch (e) { handleError(e); } @@ -8747,11 +8823,13 @@ describe('ParseGraphQLServer', () => { } } someClasses(where: { someField: { exists: true } }) { - results { - id - someField { - latitude - longitude + edges { + node { + id + someField { + latitude + longitude + } } } } @@ -8764,7 +8842,7 @@ describe('ParseGraphQLServer', () => { expect(typeof getResult.data.someClass.someField).toEqual('object'); expect(getResult.data.someClass.someField).toEqual(someFieldValue); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -8836,11 +8914,13 @@ describe('ParseGraphQLServer', () => { } } someClasses(where: { somePolygonField: { exists: true } }) { - results { - id - somePolygonField { - latitude - longitude + edges { + node { + id + somePolygonField { + latitude + longitude + } } } } @@ -8860,7 +8940,7 @@ describe('ParseGraphQLServer', () => { __typename: 'GeoPoint', })) ); - expect(getResult.data.someClasses.results.length).toEqual(1); + expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); } @@ -8951,8 +9031,10 @@ describe('ParseGraphQLServer', () => { query: gql` query FindSomeObject($where: SomeClassWhereInput!) { someClasses(where: $where) { - results { - id + edges { + node { + id + } } } } @@ -8965,9 +9047,9 @@ describe('ParseGraphQLServer', () => { }, }, }); - const findResults = findResult.data.someClasses.results; + const findResults = findResult.data.someClasses.edges; expect(findResults.length).toBe(1); - expect(findResults[0].id).toBe( + expect(findResults[0].node.id).toBe( createResult.data.createSomeClass.someClass.id ); }); diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index 653ef1c54b..e173e5d67b 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -1001,19 +1001,6 @@ const POLYGON_WHERE_INPUT = new GraphQLInputObjectType({ }, }); -const FIND_RESULT = new GraphQLObjectType({ - name: 'FindResult', - description: - 'The FindResult object type is used in the find queries to return the data of the matched objects.', - fields: { - results: { - description: 'This is the objects returned by the query', - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(OBJECT))), - }, - count: COUNT_ATT, - }, -}); - const ELEMENT = new GraphQLObjectType({ name: 'Element', description: "The Element object type is used to return array items' value.", @@ -1096,7 +1083,6 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(FILE_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_POINT_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(POLYGON_WHERE_INPUT, true); - parseGraphQLSchema.addGraphQLType(FIND_RESULT, true); parseGraphQLSchema.addGraphQLType(ELEMENT, true); }; @@ -1180,7 +1166,6 @@ export { FILE_WHERE_INPUT, GEO_POINT_WHERE_INPUT, POLYGON_WHERE_INPUT, - FIND_RESULT, ARRAY_RESULT, ELEMENT, load, diff --git a/src/GraphQL/loaders/parseClassQueries.js b/src/GraphQL/loaders/parseClassQueries.js index 9ec5b99967..1da3f2da3b 100644 --- a/src/GraphQL/loaders/parseClassQueries.js +++ b/src/GraphQL/loaders/parseClassQueries.js @@ -90,7 +90,7 @@ const load = function( description: `The ${findGraphQLQueryName} query can be used to find objects of the ${graphQLClassName} class.`, args: classGraphQLFindArgs, type: new GraphQLNonNull( - classGraphQLFindResultType || defaultGraphQLTypes.FIND_RESULT + classGraphQLFindResultType || defaultGraphQLTypes.OBJECT ), async resolve(_source, args, context, queryInfo) { try { diff --git a/src/GraphQL/transformers/outputType.js b/src/GraphQL/transformers/outputType.js index b8382cc1f2..0e34658ed9 100644 --- a/src/GraphQL/transformers/outputType.js +++ b/src/GraphQL/transformers/outputType.js @@ -45,7 +45,7 @@ const transformOutputTypeToGraphQL = ( parseClassTypes[targetClass].classGraphQLFindResultType ); } else { - return new GraphQLNonNull(defaultGraphQLTypes.FIND_RESULT); + return new GraphQLNonNull(defaultGraphQLTypes.OBJECT); } case 'File': return defaultGraphQLTypes.FILE_INFO; From b388751423e42a1de60d31f63c3cf8a081709913 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 26 Sep 2019 12:22:46 -0700 Subject: [PATCH 23/27] Add pagination test --- spec/ParseGraphQLServer.spec.js | 152 ++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index d5acd5fa7f..ef2a84b929 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -4980,6 +4980,158 @@ describe('ParseGraphQLServer', () => { ).toEqual(['someValue14', 'someValue17']); }); + fit('should support pagination', async () => { + const numberArray = (first, last) => { + const array = []; + for (let i = first; i <= last; i++) { + array.push(i); + } + return array; + }; + + const promises = []; + for (let i = 0; i < 100; i++) { + const obj = new Parse.Object('SomeClass'); + obj.set('numberField', i); + promises.push(obj.save()); + } + await Promise.all(promises); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const find = async ({ skip, after, first, before, last } = {}) => { + return await apolloClient.query({ + query: gql` + query FindSomeObjects( + $order: [SomeClassOrder!] + $skip: Int + $after: String + $first: Int + $before: String + $last: Int + ) { + someClasses( + order: $order + skip: $skip + after: $after + first: $first + before: $before + last: $last + ) { + edges { + cursor + node { + numberField + } + } + count + pageInfo { + hasPreviousPage + startCursor + endCursor + hasNextPage + } + } + } + `, + variables: { + order: ['numberField_ASC'], + skip, + after, + first, + before, + last, + }, + }); + }; + + let result = await find(); + expect( + result.data.someClasses.edges.map(edge => edge.node.numberField) + ).toEqual(numberArray(0, 99)); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( + false + ); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[99].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(false); + + result = await find({ first: 10 }); + expect( + result.data.someClasses.edges.map(edge => edge.node.numberField) + ).toEqual(numberArray(0, 9)); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( + false + ); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + + result = await find({ + first: 10, + after: result.data.someClasses.pageInfo.endCursor, + }); + expect( + result.data.someClasses.edges.map(edge => edge.node.numberField) + ).toEqual(numberArray(10, 19)); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( + true + ); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + + result = await find({ last: 10 }); + expect( + result.data.someClasses.edges.map(edge => edge.node.numberField) + ).toEqual(numberArray(90, 99)); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( + true + ); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(false); + + result = await find({ + last: 10, + before: result.data.someClasses.pageInfo.startCursor, + }); + expect( + result.data.someClasses.edges.map(edge => edge.node.numberField) + ).toEqual(numberArray(80, 89)); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( + true + ); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + }); + it('should support count', async () => { await prepareData(); From 928b831e38e741ab9c53dfae1bcceb408f41ef0d Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 26 Sep 2019 12:38:33 -0700 Subject: [PATCH 24/27] Fix file location --- spec/ParseGraphQLServer.spec.js | 2 +- src/GraphQL/loaders/defaultRelaySchema.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index ef2a84b929..dfe080be7e 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -4980,7 +4980,7 @@ describe('ParseGraphQLServer', () => { ).toEqual(['someValue14', 'someValue17']); }); - fit('should support pagination', async () => { + it('should support pagination', async () => { const numberArray = (first, last) => { const array = []; for (let i = first; i <= last; i++) { diff --git a/src/GraphQL/loaders/defaultRelaySchema.js b/src/GraphQL/loaders/defaultRelaySchema.js index 9e6aea1694..3837bd5b9f 100644 --- a/src/GraphQL/loaders/defaultRelaySchema.js +++ b/src/GraphQL/loaders/defaultRelaySchema.js @@ -1,7 +1,7 @@ import { nodeDefinitions, fromGlobalId } from 'graphql-relay'; import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; -import * as objectsQueries from './objectsQueries'; +import * as objectsQueries from '../helpers/objectsQueries'; import { extractKeysAndInclude } from './parseClassTypes'; const GLOBAL_ID_ATT = { From e374bde3a87444100284789fee3395d45f94bd9b Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Thu, 26 Sep 2019 13:32:14 -0700 Subject: [PATCH 25/27] Fix postgres tests --- spec/ParseGraphQLServer.spec.js | 999 ++++++++++++++++---------------- 1 file changed, 502 insertions(+), 497 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index dfe080be7e..26ada8f426 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -2229,579 +2229,584 @@ describe('ParseGraphQLServer', () => { expect(nodeResult.data.node2.someField).toBe('some value 2'); }); - it('Id inputs should work either with global id or object id', async () => { - try { - await apolloClient.mutate({ - mutation: gql` - mutation CreateClasses { - secondaryObject: createClass( - input: { - name: "SecondaryObject" - schemaFields: { addStrings: [{ name: "someField" }] } + it_only_db('mongo')( + 'Id inputs should work either with global id or object id', + async () => { + try { + await apolloClient.mutate({ + mutation: gql` + mutation CreateClasses { + secondaryObject: createClass( + input: { + name: "SecondaryObject" + schemaFields: { addStrings: [{ name: "someField" }] } + } + ) { + clientMutationId } - ) { - clientMutationId - } - primaryObject: createClass( - input: { - name: "PrimaryObject" - schemaFields: { - addStrings: [{ name: "stringField" }] - addArrays: [{ name: "arrayField" }] - addPointers: [ - { - name: "pointerField" - targetClassName: "SecondaryObject" - } - ] - addRelations: [ - { - name: "relationField" - targetClassName: "SecondaryObject" - } - ] + primaryObject: createClass( + input: { + name: "PrimaryObject" + schemaFields: { + addStrings: [{ name: "stringField" }] + addArrays: [{ name: "arrayField" }] + addPointers: [ + { + name: "pointerField" + targetClassName: "SecondaryObject" + } + ] + addRelations: [ + { + name: "relationField" + targetClassName: "SecondaryObject" + } + ] + } } + ) { + clientMutationId } - ) { - clientMutationId } - } - `, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, }, - }, - }); + }); - await resetGraphQLCache(); + await resetGraphQLCache(); - const createSecondaryObjectsResult = await apolloClient.mutate({ - mutation: gql` - mutation CreateSecondaryObjects { - secondaryObject1: createSecondaryObject( - input: { fields: { someField: "some value 1" } } - ) { - secondaryObject { - id - objectId - someField + const createSecondaryObjectsResult = await apolloClient.mutate({ + mutation: gql` + mutation CreateSecondaryObjects { + secondaryObject1: createSecondaryObject( + input: { fields: { someField: "some value 1" } } + ) { + secondaryObject { + id + objectId + someField + } } - } - secondaryObject2: createSecondaryObject( - input: { fields: { someField: "some value 2" } } - ) { - secondaryObject { - id - someField + secondaryObject2: createSecondaryObject( + input: { fields: { someField: "some value 2" } } + ) { + secondaryObject { + id + someField + } } - } - secondaryObject3: createSecondaryObject( - input: { fields: { someField: "some value 3" } } - ) { - secondaryObject { - objectId - someField + secondaryObject3: createSecondaryObject( + input: { fields: { someField: "some value 3" } } + ) { + secondaryObject { + objectId + someField + } } - } - secondaryObject4: createSecondaryObject( - input: { fields: { someField: "some value 4" } } - ) { - secondaryObject { - id - objectId + secondaryObject4: createSecondaryObject( + input: { fields: { someField: "some value 4" } } + ) { + secondaryObject { + id + objectId + } } - } - secondaryObject5: createSecondaryObject( - input: { fields: { someField: "some value 5" } } - ) { - secondaryObject { - id + secondaryObject5: createSecondaryObject( + input: { fields: { someField: "some value 5" } } + ) { + secondaryObject { + id + } } - } - secondaryObject6: createSecondaryObject( - input: { fields: { someField: "some value 6" } } - ) { - secondaryObject { - objectId + secondaryObject6: createSecondaryObject( + input: { fields: { someField: "some value 6" } } + ) { + secondaryObject { + objectId + } } - } - secondaryObject7: createSecondaryObject( - input: { fields: { someField: "some value 7" } } - ) { - secondaryObject { - someField + secondaryObject7: createSecondaryObject( + input: { fields: { someField: "some value 7" } } + ) { + secondaryObject { + someField + } } } - } - `, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, }, - }, - }); + }); - const updateSecondaryObjectsResult = await apolloClient.mutate({ - mutation: gql` - mutation UpdateSecondaryObjects( - $id1: ID! - $id2: ID! - $id3: ID! - $id4: ID! - $id5: ID! - $id6: ID! - ) { - secondaryObject1: updateSecondaryObject( - input: { - id: $id1 - fields: { someField: "some value 11" } - } + const updateSecondaryObjectsResult = await apolloClient.mutate({ + mutation: gql` + mutation UpdateSecondaryObjects( + $id1: ID! + $id2: ID! + $id3: ID! + $id4: ID! + $id5: ID! + $id6: ID! ) { - secondaryObject { - id - objectId - someField - } - } - secondaryObject2: updateSecondaryObject( - input: { - id: $id2 - fields: { someField: "some value 22" } + secondaryObject1: updateSecondaryObject( + input: { + id: $id1 + fields: { someField: "some value 11" } + } + ) { + secondaryObject { + id + objectId + someField + } } - ) { - secondaryObject { - id - someField + secondaryObject2: updateSecondaryObject( + input: { + id: $id2 + fields: { someField: "some value 22" } + } + ) { + secondaryObject { + id + someField + } } - } - secondaryObject3: updateSecondaryObject( - input: { - id: $id3 - fields: { someField: "some value 33" } + secondaryObject3: updateSecondaryObject( + input: { + id: $id3 + fields: { someField: "some value 33" } + } + ) { + secondaryObject { + objectId + someField + } } - ) { - secondaryObject { - objectId - someField + secondaryObject4: updateSecondaryObject( + input: { + id: $id4 + fields: { someField: "some value 44" } + } + ) { + secondaryObject { + id + objectId + } } - } - secondaryObject4: updateSecondaryObject( - input: { - id: $id4 - fields: { someField: "some value 44" } + secondaryObject5: updateSecondaryObject( + input: { + id: $id5 + fields: { someField: "some value 55" } + } + ) { + secondaryObject { + id + } } - ) { - secondaryObject { - id - objectId + secondaryObject6: updateSecondaryObject( + input: { + id: $id6 + fields: { someField: "some value 66" } + } + ) { + secondaryObject { + objectId + } } } - secondaryObject5: updateSecondaryObject( - input: { - id: $id5 - fields: { someField: "some value 55" } - } + `, + variables: { + id1: + createSecondaryObjectsResult.data.secondaryObject1 + .secondaryObject.id, + id2: + createSecondaryObjectsResult.data.secondaryObject2 + .secondaryObject.id, + id3: + createSecondaryObjectsResult.data.secondaryObject3 + .secondaryObject.objectId, + id4: + createSecondaryObjectsResult.data.secondaryObject4 + .secondaryObject.objectId, + id5: + createSecondaryObjectsResult.data.secondaryObject5 + .secondaryObject.id, + id6: + createSecondaryObjectsResult.data.secondaryObject6 + .secondaryObject.objectId, + }, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); + + const deleteSecondaryObjectsResult = await apolloClient.mutate({ + mutation: gql` + mutation DeleteSecondaryObjects( + $id1: ID! + $id3: ID! + $id5: ID! + $id6: ID! ) { - secondaryObject { - id + secondaryObject1: deleteSecondaryObject( + input: { id: $id1 } + ) { + secondaryObject { + id + objectId + someField + } } - } - secondaryObject6: updateSecondaryObject( - input: { - id: $id6 - fields: { someField: "some value 66" } + secondaryObject3: deleteSecondaryObject( + input: { id: $id3 } + ) { + secondaryObject { + objectId + someField + } } - ) { - secondaryObject { - objectId + secondaryObject5: deleteSecondaryObject( + input: { id: $id5 } + ) { + secondaryObject { + id + } + } + secondaryObject6: deleteSecondaryObject( + input: { id: $id6 } + ) { + secondaryObject { + objectId + } } } - } - `, - variables: { - id1: - createSecondaryObjectsResult.data.secondaryObject1 - .secondaryObject.id, - id2: - createSecondaryObjectsResult.data.secondaryObject2 - .secondaryObject.id, - id3: - createSecondaryObjectsResult.data.secondaryObject3 - .secondaryObject.objectId, - id4: - createSecondaryObjectsResult.data.secondaryObject4 - .secondaryObject.objectId, - id5: - createSecondaryObjectsResult.data.secondaryObject5 - .secondaryObject.id, - id6: - createSecondaryObjectsResult.data.secondaryObject6 - .secondaryObject.objectId, - }, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + variables: { + id1: + updateSecondaryObjectsResult.data.secondaryObject1 + .secondaryObject.id, + id3: + updateSecondaryObjectsResult.data.secondaryObject3 + .secondaryObject.objectId, + id5: + updateSecondaryObjectsResult.data.secondaryObject5 + .secondaryObject.id, + id6: + updateSecondaryObjectsResult.data.secondaryObject6 + .secondaryObject.objectId, }, - }, - }); + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); - const deleteSecondaryObjectsResult = await apolloClient.mutate({ - mutation: gql` - mutation DeleteSecondaryObjects( - $id1: ID! - $id3: ID! - $id5: ID! - $id6: ID! - ) { - secondaryObject1: deleteSecondaryObject( - input: { id: $id1 } - ) { - secondaryObject { + const getSecondaryObjectsResult = await apolloClient.query({ + query: gql` + query GetSecondaryObjects($id2: ID!, $id4: ID!) { + secondaryObject2: secondaryObject(id: $id2) { id objectId someField } - } - secondaryObject3: deleteSecondaryObject( - input: { id: $id3 } - ) { - secondaryObject { + secondaryObject4: secondaryObject(id: $id4) { objectId someField } } - secondaryObject5: deleteSecondaryObject( - input: { id: $id5 } - ) { - secondaryObject { - id - } - } - secondaryObject6: deleteSecondaryObject( - input: { id: $id6 } - ) { - secondaryObject { - objectId - } - } - } - `, - variables: { - id1: - updateSecondaryObjectsResult.data.secondaryObject1 - .secondaryObject.id, - id3: - updateSecondaryObjectsResult.data.secondaryObject3 - .secondaryObject.objectId, - id5: - updateSecondaryObjectsResult.data.secondaryObject5 - .secondaryObject.id, - id6: - updateSecondaryObjectsResult.data.secondaryObject6 - .secondaryObject.objectId, - }, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + variables: { + id2: + updateSecondaryObjectsResult.data.secondaryObject2 + .secondaryObject.id, + id4: + updateSecondaryObjectsResult.data.secondaryObject4 + .secondaryObject.objectId, }, - }, - }); - - const getSecondaryObjectsResult = await apolloClient.query({ - query: gql` - query GetSecondaryObjects($id2: ID!, $id4: ID!) { - secondaryObject2: secondaryObject(id: $id2) { - id - objectId - someField - } - secondaryObject4: secondaryObject(id: $id4) { - objectId - someField - } - } - `, - variables: { - id2: - updateSecondaryObjectsResult.data.secondaryObject2 - .secondaryObject.id, - id4: - updateSecondaryObjectsResult.data.secondaryObject4 - .secondaryObject.objectId, - }, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, }, - }, - }); + }); - const findSecondaryObjectsResult = await apolloClient.query({ - query: gql` - query FindSecondaryObjects( - $id1: ID! - $id2: ID! - $id3: ID! - $id4: ID! - $id5: ID! - $id6: ID! - ) { - secondaryObjects( - where: { - AND: [ - { - OR: [ - { id: { equalTo: $id2 } } - { - AND: [ - { id: { equalTo: $id4 } } - { objectId: { equalTo: $id4 } } - ] - } - ] - } - { id: { notEqualTo: $id1 } } - { id: { notEqualTo: $id3 } } - { objectId: { notEqualTo: $id2 } } - { objectId: { notIn: [$id5, $id6] } } - { id: { in: [$id2, $id4] } } - ] - } - order: [id_ASC, objectId_ASC] + const findSecondaryObjectsResult = await apolloClient.query({ + query: gql` + query FindSecondaryObjects( + $id1: ID! + $id2: ID! + $id3: ID! + $id4: ID! + $id5: ID! + $id6: ID! ) { - edges { - node { - id - objectId - someField + secondaryObjects( + where: { + AND: [ + { + OR: [ + { id: { equalTo: $id2 } } + { + AND: [ + { id: { equalTo: $id4 } } + { objectId: { equalTo: $id4 } } + ] + } + ] + } + { id: { notEqualTo: $id1 } } + { id: { notEqualTo: $id3 } } + { objectId: { notEqualTo: $id2 } } + { objectId: { notIn: [$id5, $id6] } } + { id: { in: [$id2, $id4] } } + ] + } + order: [id_ASC, objectId_ASC] + ) { + edges { + node { + id + objectId + someField + } } + count } - count } - } - `, - variables: { - id1: - deleteSecondaryObjectsResult.data.secondaryObject1 - .secondaryObject.objectId, - id2: getSecondaryObjectsResult.data.secondaryObject2.id, - id3: - deleteSecondaryObjectsResult.data.secondaryObject3 - .secondaryObject.objectId, - id4: getSecondaryObjectsResult.data.secondaryObject4.objectId, - id5: - deleteSecondaryObjectsResult.data.secondaryObject5 - .secondaryObject.id, - id6: - deleteSecondaryObjectsResult.data.secondaryObject6 - .secondaryObject.objectId, - }, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + variables: { + id1: + deleteSecondaryObjectsResult.data.secondaryObject1 + .secondaryObject.objectId, + id2: getSecondaryObjectsResult.data.secondaryObject2.id, + id3: + deleteSecondaryObjectsResult.data.secondaryObject3 + .secondaryObject.objectId, + id4: + getSecondaryObjectsResult.data.secondaryObject4.objectId, + id5: + deleteSecondaryObjectsResult.data.secondaryObject5 + .secondaryObject.id, + id6: + deleteSecondaryObjectsResult.data.secondaryObject6 + .secondaryObject.objectId, }, - }, - }); - - expect( - findSecondaryObjectsResult.data.secondaryObjects.count - ).toEqual(2); - expect( - findSecondaryObjectsResult.data.secondaryObjects.edges - .map(value => value.node.someField) - .sort() - ).toEqual(['some value 22', 'some value 44']); - expect( - findSecondaryObjectsResult.data.secondaryObjects.edges[0].node - .id - ).toBeLessThan( - findSecondaryObjectsResult.data.secondaryObjects.edges[1].node - .id - ); - expect( - findSecondaryObjectsResult.data.secondaryObjects.edges[0].node - .objectId - ).toBeLessThan( - findSecondaryObjectsResult.data.secondaryObjects.edges[1].node - .objectId - ); + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); - const createPrimaryObjectResult = await apolloClient.mutate({ - mutation: gql` - mutation CreatePrimaryObject( - $pointer: Any - $secondaryObject2: ID! - $secondaryObject4: ID! - ) { - createPrimaryObject( - input: { - fields: { - stringField: "some value" - arrayField: [1, "abc", $pointer] - pointerField: { link: $secondaryObject2 } - relationField: { - add: [$secondaryObject2, $secondaryObject4] - } - } - } + expect( + findSecondaryObjectsResult.data.secondaryObjects.count + ).toEqual(2); + expect( + findSecondaryObjectsResult.data.secondaryObjects.edges + .map(value => value.node.someField) + .sort() + ).toEqual(['some value 22', 'some value 44']); + expect( + findSecondaryObjectsResult.data.secondaryObjects.edges[0].node + .id + ).toBeLessThan( + findSecondaryObjectsResult.data.secondaryObjects.edges[1].node + .id + ); + expect( + findSecondaryObjectsResult.data.secondaryObjects.edges[0].node + .objectId + ).toBeLessThan( + findSecondaryObjectsResult.data.secondaryObjects.edges[1].node + .objectId + ); + + const createPrimaryObjectResult = await apolloClient.mutate({ + mutation: gql` + mutation CreatePrimaryObject( + $pointer: Any + $secondaryObject2: ID! + $secondaryObject4: ID! ) { - primaryObject { - id - stringField - arrayField { - ... on Element { - value - } - ... on SecondaryObject { - someField + createPrimaryObject( + input: { + fields: { + stringField: "some value" + arrayField: [1, "abc", $pointer] + pointerField: { link: $secondaryObject2 } + relationField: { + add: [$secondaryObject2, $secondaryObject4] + } } } - pointerField { + ) { + primaryObject { id - objectId - someField - } - relationField { - edges { - node { - id - objectId + stringField + arrayField { + ... on Element { + value + } + ... on SecondaryObject { someField } } + pointerField { + id + objectId + someField + } + relationField { + edges { + node { + id + objectId + someField + } + } + } } } } - } - `, - variables: { - pointer: { - __type: 'Pointer', - className: 'SecondaryObject', - objectId: + `, + variables: { + pointer: { + __type: 'Pointer', + className: 'SecondaryObject', + objectId: + getSecondaryObjectsResult.data.secondaryObject4 + .objectId, + }, + secondaryObject2: + getSecondaryObjectsResult.data.secondaryObject2.id, + secondaryObject4: getSecondaryObjectsResult.data.secondaryObject4.objectId, }, - secondaryObject2: - getSecondaryObjectsResult.data.secondaryObject2.id, - secondaryObject4: - getSecondaryObjectsResult.data.secondaryObject4.objectId, - }, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, }, - }, - }); + }); - const updatePrimaryObjectResult = await apolloClient.mutate({ - mutation: gql` - mutation UpdatePrimaryObject( - $id: ID! - $secondaryObject2: ID! - $secondaryObject4: ID! - ) { - updatePrimaryObject( - input: { - id: $id - fields: { - pointerField: { link: $secondaryObject4 } - relationField: { - remove: [$secondaryObject2, $secondaryObject4] - } - } - } + const updatePrimaryObjectResult = await apolloClient.mutate({ + mutation: gql` + mutation UpdatePrimaryObject( + $id: ID! + $secondaryObject2: ID! + $secondaryObject4: ID! ) { - primaryObject { - id - stringField - arrayField { - ... on Element { - value - } - ... on SecondaryObject { - someField + updatePrimaryObject( + input: { + id: $id + fields: { + pointerField: { link: $secondaryObject4 } + relationField: { + remove: [$secondaryObject2, $secondaryObject4] + } } } - pointerField { + ) { + primaryObject { id - objectId - someField - } - relationField { - edges { - node { - id - objectId + stringField + arrayField { + ... on Element { + value + } + ... on SecondaryObject { someField } } + pointerField { + id + objectId + someField + } + relationField { + edges { + node { + id + objectId + someField + } + } + } } } } - } - `, - variables: { - id: - createPrimaryObjectResult.data.createPrimaryObject - .primaryObject.id, - secondaryObject2: - getSecondaryObjectsResult.data.secondaryObject2.id, - secondaryObject4: - getSecondaryObjectsResult.data.secondaryObject4.objectId, - }, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + variables: { + id: + createPrimaryObjectResult.data.createPrimaryObject + .primaryObject.id, + secondaryObject2: + getSecondaryObjectsResult.data.secondaryObject2.id, + secondaryObject4: + getSecondaryObjectsResult.data.secondaryObject4.objectId, }, - }, - }); + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); - expect( - createPrimaryObjectResult.data.createPrimaryObject.primaryObject - .stringField - ).toEqual('some value'); - expect( - createPrimaryObjectResult.data.createPrimaryObject.primaryObject - .arrayField - ).toEqual([ - { __typename: 'Element', value: 1 }, - { __typename: 'Element', value: 'abc' }, - { __typename: 'SecondaryObject', someField: 'some value 44' }, - ]); - expect( - createPrimaryObjectResult.data.createPrimaryObject.primaryObject - .pointerField.someField - ).toEqual('some value 22'); - expect( - createPrimaryObjectResult.data.createPrimaryObject.primaryObject.relationField.edges - .map(value => value.node.someField) - .sort() - ).toEqual(['some value 22', 'some value 44']); - expect( - updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject - .stringField - ).toEqual('some value'); - expect( - updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject - .arrayField - ).toEqual([ - { __typename: 'Element', value: 1 }, - { __typename: 'Element', value: 'abc' }, - { __typename: 'SecondaryObject', someField: 'some value 44' }, - ]); - expect( - updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject - .pointerField.someField - ).toEqual('some value 44'); - expect( - updatePrimaryObjectResult.data.updatePrimaryObject.primaryObject - .relationField.edges - ).toEqual([]); - } catch (e) { - handleError(e); + expect( + createPrimaryObjectResult.data.createPrimaryObject + .primaryObject.stringField + ).toEqual('some value'); + expect( + createPrimaryObjectResult.data.createPrimaryObject + .primaryObject.arrayField + ).toEqual([ + { __typename: 'Element', value: 1 }, + { __typename: 'Element', value: 'abc' }, + { __typename: 'SecondaryObject', someField: 'some value 44' }, + ]); + expect( + createPrimaryObjectResult.data.createPrimaryObject + .primaryObject.pointerField.someField + ).toEqual('some value 22'); + expect( + createPrimaryObjectResult.data.createPrimaryObject.primaryObject.relationField.edges + .map(value => value.node.someField) + .sort() + ).toEqual(['some value 22', 'some value 44']); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject + .primaryObject.stringField + ).toEqual('some value'); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject + .primaryObject.arrayField + ).toEqual([ + { __typename: 'Element', value: 1 }, + { __typename: 'Element', value: 'abc' }, + { __typename: 'SecondaryObject', someField: 'some value 44' }, + ]); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject + .primaryObject.pointerField.someField + ).toEqual('some value 44'); + expect( + updatePrimaryObjectResult.data.updatePrimaryObject + .primaryObject.relationField.edges + ).toEqual([]); + } catch (e) { + handleError(e); + } } - }); + ); }); }); From 35683d13a9d40e826992c2907b703126ca8f6de9 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 10 Nov 2019 22:10:17 -0800 Subject: [PATCH 26/27] Add comments --- package-lock.json | 6 ++-- src/GraphQL/helpers/objectsQueries.js | 47 +++++++++++++++++++++------ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 496cec9e94..f72ba6a085 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8334,9 +8334,9 @@ "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==" }, "object-inspect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", - "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" }, "object-keys": { "version": "1.1.1", diff --git a/src/GraphQL/helpers/objectsQueries.js b/src/GraphQL/helpers/objectsQueries.js index ae9a12187a..d374db586c 100644 --- a/src/GraphQL/helpers/objectsQueries.js +++ b/src/GraphQL/helpers/objectsQueries.js @@ -96,14 +96,16 @@ const findObjects = async ( if (Object.keys(where).length > 0 && subqueryReadPreference) { preCountOptions.subqueryReadPreference = subqueryReadPreference; } - preCount = (await rest.find( - config, - auth, - className, - where, - preCountOptions, - info.clientSDK - )).count; + preCount = ( + await rest.find( + config, + auth, + className, + where, + preCountOptions, + info.clientSDK + ) + ).count; if ((skip || 0) + limit < preCount) { skip = preCount - limit; } @@ -214,6 +216,8 @@ const calculateSkipAndLimit = ( let skip = undefined; let limit = undefined; let needToPreCount = false; + + // Validates the skip input if (skipInput || skipInput === 0) { if (skipInput < 0) { throw new Parse.Error( @@ -223,6 +227,8 @@ const calculateSkipAndLimit = ( } skip = skipInput; } + + // Validates the after param if (after) { after = cursorToOffset(after); if ((!after && after !== 0) || after < 0) { @@ -231,8 +237,12 @@ const calculateSkipAndLimit = ( 'After is not a valid cursor' ); } + + // If skip and after are passed, a new skip is calculated by adding them skip = (skip || 0) + (after + 1); } + + // Validates the first param if (first || first === 0) { if (first < 0) { throw new Parse.Error( @@ -240,9 +250,14 @@ const calculateSkipAndLimit = ( 'First should be a positive number' ); } + + // The first param is translated to the limit param of the Parse legacy API limit = first; } + + // Validates the before param if (before || before === 0) { + // This method converts the cursor to the index of the object before = cursorToOffset(before); if ((!before && before !== 0) || before < 0) { throw new Parse.Error( @@ -250,12 +265,17 @@ const calculateSkipAndLimit = ( 'Before is not a valid cursor' ); } + if ((skip || 0) >= before) { + // If the before index is less then the skip, no objects will be returned limit = 0; } else if ((!limit && limit !== 0) || (skip || 0) + limit > before) { + // If there is no limit set, the limit is calculated. Or, if the limit (plus skip) is bigger than the before index, the new limit is set. limit = before - (skip || 0); } } + + // Validates the last param if (last || last === 0) { if (last < 0) { throw new Parse.Error( @@ -263,17 +283,24 @@ const calculateSkipAndLimit = ( 'Last should be a positive number' ); } + if (last > maxLimit) { + // Last can't be bigger than Parse server maxLimit config. last = maxLimit; } + if (limit || limit === 0) { + // If there is a previous limit set, it may be adjusted if (last < limit) { - skip = (skip || 0) + (limit - last); - limit = last; + // if last is less than the current limit + skip = (skip || 0) + (limit - last); // The skip is adjusted + limit = last; // the limit is adjusted } } else if (last === 0) { + // No objects will be returned limit = 0; } else { + // No previous limit set, the limit will be equal to last and pre count is needed. limit = last; needToPreCount = true; } From ee449328de6b66b978ae2e1dd26271dca6a4fa63 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 10 Nov 2019 22:51:44 -0800 Subject: [PATCH 27/27] Tests to calculateSkipAndLimit --- spec/graphQLObjectsQueries.js | 158 ++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 spec/graphQLObjectsQueries.js diff --git a/spec/graphQLObjectsQueries.js b/spec/graphQLObjectsQueries.js new file mode 100644 index 0000000000..40bdca8b6d --- /dev/null +++ b/spec/graphQLObjectsQueries.js @@ -0,0 +1,158 @@ +const { offsetToCursor } = require('graphql-relay'); +const { + calculateSkipAndLimit, +} = require('../lib/GraphQL/helpers/objectsQueries'); + +describe('GraphQL objectsQueries', () => { + describe('calculateSkipAndLimit', () => { + it('should fail with invalid params', () => { + expect(() => calculateSkipAndLimit(-1)).toThrow( + jasmine.stringMatching('Skip should be a positive number') + ); + expect(() => calculateSkipAndLimit(1, -1)).toThrow( + jasmine.stringMatching('First should be a positive number') + ); + expect(() => calculateSkipAndLimit(1, 1, offsetToCursor(-1))).toThrow( + jasmine.stringMatching('After is not a valid curso') + ); + expect(() => calculateSkipAndLimit(1, 1, offsetToCursor(1), -1)).toThrow( + jasmine.stringMatching('Last should be a positive number') + ); + expect(() => + calculateSkipAndLimit(1, 1, offsetToCursor(1), 1, offsetToCursor(-1)) + ).toThrow(jasmine.stringMatching('Before is not a valid curso')); + }); + + it('should work only with skip', () => { + expect(calculateSkipAndLimit(10)).toEqual({ + skip: 10, + limit: undefined, + needToPreCount: false, + }); + }); + + it('should work only with after', () => { + expect( + calculateSkipAndLimit(undefined, undefined, offsetToCursor(9)) + ).toEqual({ + skip: 10, + limit: undefined, + needToPreCount: false, + }); + }); + + it('should work with limit and after', () => { + expect(calculateSkipAndLimit(10, undefined, offsetToCursor(9))).toEqual({ + skip: 20, + limit: undefined, + needToPreCount: false, + }); + }); + + it('first alone should set the limit', () => { + expect(calculateSkipAndLimit(10, 30, offsetToCursor(9))).toEqual({ + skip: 20, + limit: 30, + needToPreCount: false, + }); + }); + + it('if before cursor is less than skipped items, no objects will be returned', () => { + expect( + calculateSkipAndLimit( + 10, + 30, + offsetToCursor(9), + undefined, + offsetToCursor(5) + ) + ).toEqual({ + skip: 20, + limit: 0, + needToPreCount: false, + }); + }); + + it('if before cursor is greater than returned objects set by limit, nothing is changed', () => { + expect( + calculateSkipAndLimit( + 10, + 30, + offsetToCursor(9), + undefined, + offsetToCursor(100) + ) + ).toEqual({ + skip: 20, + limit: 30, + needToPreCount: false, + }); + }); + + it('if before cursor is less than returned objects set by limit, limit is adjusted', () => { + expect( + calculateSkipAndLimit( + 10, + 30, + offsetToCursor(9), + undefined, + offsetToCursor(40) + ) + ).toEqual({ + skip: 20, + limit: 20, + needToPreCount: false, + }); + }); + + it('last should work alone but requires pre count', () => { + expect( + calculateSkipAndLimit(undefined, undefined, undefined, 10) + ).toEqual({ + skip: undefined, + limit: 10, + needToPreCount: true, + }); + }); + + it('last should be adjusted to max limit', () => { + expect( + calculateSkipAndLimit(undefined, undefined, undefined, 10, undefined, 5) + ).toEqual({ + skip: undefined, + limit: 5, + needToPreCount: true, + }); + }); + + it('no objects will be returned if last is equal to 0', () => { + expect(calculateSkipAndLimit(undefined, undefined, undefined, 0)).toEqual( + { + skip: undefined, + limit: 0, + needToPreCount: false, + } + ); + }); + + it('nothing changes if last is bigger than the calculared limit', () => { + expect( + calculateSkipAndLimit(10, 30, offsetToCursor(9), 30, offsetToCursor(40)) + ).toEqual({ + skip: 20, + limit: 20, + needToPreCount: false, + }); + }); + + it('If last is small than limit, new limit is calculated', () => { + expect( + calculateSkipAndLimit(10, 30, offsetToCursor(9), 10, offsetToCursor(40)) + ).toEqual({ + skip: 30, + limit: 10, + needToPreCount: false, + }); + }); + }); +});