From 59d8f0f75fd60f6f69690c08a2faa820c95c982f Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 7 Aug 2019 19:49:07 +0200 Subject: [PATCH 1/3] Return specific Type on specific Mutation --- spec/ParseGraphQLServer.spec.js | 94 ++++++++++++++++------ src/GraphQL/loaders/parseClassMutations.js | 74 ++++++++++++++--- src/GraphQL/loaders/parseClassTypes.js | 19 +++++ src/GraphQL/loaders/usersMutations.js | 37 +++++---- 4 files changed, 170 insertions(+), 54 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 5d62b658fc..cbd16300d9 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -1004,7 +1004,9 @@ describe('ParseGraphQLServer', () => { query: gql` mutation DeleteCustomer($objectId: ID!) { objects { - deleteCustomer(objectId: $objectId) + deleteCustomer(objectId: $objectId) { + objectId + } } } `, @@ -1094,7 +1096,9 @@ describe('ParseGraphQLServer', () => { query: gql` mutation DeleteSuperCar($objectId: ID!) { objects { - deleteSuperCar(objectId: $objectId) + deleteSuperCar(objectId: $objectId) { + objectId + } } } `, @@ -3150,7 +3154,7 @@ describe('ParseGraphQLServer', () => { expect(obj.get('someField')).toEqual('someValue'); }); - it('should return CreateResult object using class specific mutation', async () => { + it('should return specific type object using class specific mutation', async () => { const customerSchema = new Parse.Schema('Customer'); customerSchema.addString('someField'); await customerSchema.save(); @@ -3164,6 +3168,7 @@ describe('ParseGraphQLServer', () => { createCustomer(fields: $fields) { objectId createdAt + someField } } } @@ -3176,6 +3181,9 @@ describe('ParseGraphQLServer', () => { }); expect(result.data.objects.createCustomer.objectId).toBeDefined(); + expect(result.data.objects.createCustomer.someField).toEqual( + 'someValue' + ); const customer = await new Parse.Query('Customer').get( result.data.objects.createCustomer.objectId @@ -3313,7 +3321,7 @@ describe('ParseGraphQLServer', () => { expect(obj.get('someField2')).toEqual('someField2Value1'); }); - it('should return UpdateResult object using class specific mutation', async () => { + it('should return specific type object using class specific mutation', async () => { const obj = new Parse.Object('Customer'); obj.set('someField1', 'someField1Value1'); obj.set('someField2', 'someField2Value1'); @@ -3330,6 +3338,8 @@ describe('ParseGraphQLServer', () => { objects { updateCustomer(objectId: $objectId, fields: $fields) { updatedAt + someField1 + someField2 } } } @@ -3343,6 +3353,12 @@ describe('ParseGraphQLServer', () => { }); expect(result.data.objects.updateCustomer.updatedAt).toBeDefined(); + expect(result.data.objects.updateCustomer.someField1).toEqual( + 'someField1Value2' + ); + expect(result.data.objects.updateCustomer.someField2).toEqual( + 'someField2Value1' + ); await obj.fetch(); @@ -3737,8 +3753,10 @@ describe('ParseGraphQLServer', () => { ).toBeRejectedWith(jasmine.stringMatching('Object not found')); }); - it('should return a boolean confirmation using class specific mutation', async () => { + it('should return a specific type using class specific mutation', async () => { const obj = new Parse.Object('Customer'); + obj.set('someField1', 'someField1Value1'); + obj.set('someField2', 'someField2Value1'); await obj.save(); await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); @@ -3747,7 +3765,11 @@ describe('ParseGraphQLServer', () => { mutation: gql` mutation DeleteCustomer($objectId: ID!) { objects { - deleteCustomer(objectId: $objectId) + deleteCustomer(objectId: $objectId) { + objectId + someField1 + someField2 + } } } `, @@ -3756,7 +3778,13 @@ describe('ParseGraphQLServer', () => { }, }); - expect(result.data.objects.deleteCustomer).toEqual(true); + expect(result.data.objects.deleteCustomer.objectId).toEqual(obj.id); + expect(result.data.objects.deleteCustomer.someField1).toEqual( + 'someField1Value1' + ); + expect(result.data.objects.deleteCustomer.someField2).toEqual( + 'someField2Value1' + ); await expectAsync( obj.fetch({ useMasterKey: true }) @@ -3855,7 +3883,9 @@ describe('ParseGraphQLServer', () => { $objectId: ID! ) { objects { - delete${className}(objectId: $objectId) + delete${className}(objectId: $objectId) { + objectId + } } } `, @@ -3893,32 +3923,32 @@ describe('ParseGraphQLServer', () => { expect( (await deleteObject(object4.className, object4.id)).data.objects[ `delete${object4.className}` - ] - ).toEqual(true); + ].objectId + ).toEqual(object4.id); await expectAsync( object4.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); expect( (await deleteObject(object1.className, object1.id, { 'X-Parse-Master-Key': 'test', - })).data.objects[`delete${object1.className}`] - ).toEqual(true); + })).data.objects[`delete${object1.className}`].objectId + ).toEqual(object1.id); await expectAsync( object1.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); expect( (await deleteObject(object2.className, object2.id, { 'X-Parse-Session-Token': user2.getSessionToken(), - })).data.objects[`delete${object2.className}`] - ).toEqual(true); + })).data.objects[`delete${object2.className}`].objectId + ).toEqual(object2.id); await expectAsync( object2.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); expect( (await deleteObject(object3.className, object3.id, { 'X-Parse-Session-Token': user5.getSessionToken(), - })).data.objects[`delete${object3.className}`] - ).toEqual(true); + })).data.objects[`delete${object3.className}`].objectId + ).toEqual(object3.id); await expectAsync( object3.fetch({ useMasterKey: true }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); @@ -4078,12 +4108,17 @@ describe('ParseGraphQLServer', () => { describe('Users Mutations', () => { it('should sign user up', async () => { + 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: _UserSignUpFields) { users { signUp(fields: $fields) { sessionToken + someField } } } @@ -4092,11 +4127,13 @@ describe('ParseGraphQLServer', () => { fields: { username: 'user1', password: 'user1', + someField: 'someValue', }, }, }); expect(result.data.users.signUp.sessionToken).toBeDefined(); + expect(result.data.users.signUp.someField).toEqual('someValue'); expect(typeof result.data.users.signUp.sessionToken).toBe('string'); }); @@ -4104,26 +4141,31 @@ describe('ParseGraphQLServer', () => { const user = new Parse.User(); user.setUsername('user1'); user.setPassword('user1'); + user.set('someField', 'someValue'); await user.signUp(); await Parse.User.logOut(); - + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const result = await apolloClient.mutate({ mutation: gql` - mutation LogInUser($username: String!, $password: String!) { + mutation LogInUser($input: _UserLoginFields) { users { - logIn(username: $username, password: $password) { + logIn(input: $input) { sessionToken + someField } } } `, variables: { - username: 'user1', - password: 'user1', + input: { + username: 'user1', + password: 'user1', + }, }, }); expect(result.data.users.logIn.sessionToken).toBeDefined(); + expect(result.data.users.logIn.someField).toEqual('someValue'); expect(typeof result.data.users.logIn.sessionToken).toBe('string'); }); @@ -4136,17 +4178,19 @@ describe('ParseGraphQLServer', () => { const logIn = await apolloClient.mutate({ mutation: gql` - mutation LogInUser($username: String!, $password: String!) { + mutation LogInUser($input: _UserLoginFields) { users { - logIn(username: $username, password: $password) { + logIn(input: $input) { sessionToken } } } `, variables: { - username: 'user1', - password: 'user1', + input: { + username: 'user1', + password: 'user1', + }, }, }); diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index c1c75ba127..7ad54497f9 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -1,6 +1,9 @@ -import { GraphQLNonNull, GraphQLBoolean } from 'graphql'; +import { GraphQLNonNull } from 'graphql'; +import getFieldNames from 'graphql-list-fields'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; +import * as parseClassTypes from './parseClassTypes'; import * as objectsMutations from './objectsMutations'; +import * as objectsQueries from './objectsQueries'; import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLController'; const getParseClassMutationConfig = function( @@ -24,6 +27,7 @@ const load = function( const { classGraphQLCreateType, classGraphQLUpdateType, + classGraphQLOutputType, } = parseGraphQLSchema.parseClassTypes[className]; const createFields = { @@ -78,21 +82,36 @@ const load = function( args: { fields: createFields, }, - type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT), - async resolve(_source, args, context) { + type: new GraphQLNonNull(classGraphQLOutputType), + async resolve(_source, args, context, mutationInfo) { try { const { fields } = args; const { config, auth, info } = context; transformTypes('create', fields); - - return await objectsMutations.createObject( + const { objectId } = await objectsMutations.createObject( className, fields, config, auth, info ); + const selectedFields = getFieldNames(mutationInfo); + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + ); + + return await objectsQueries.getObject( + className, + objectId, + keys, + include, + undefined, + undefined, + config, + auth, + info + ); } catch (e) { parseGraphQLSchema.handleError(e); } @@ -108,15 +127,15 @@ const load = function( objectId: defaultGraphQLTypes.OBJECT_ID_ATT, fields: updateFields, }, - type: defaultGraphQLTypes.UPDATE_RESULT, - async resolve(_source, args, context) { + type: new GraphQLNonNull(classGraphQLOutputType), + async resolve(_source, args, context, mutationInfo) { try { const { objectId, fields } = args; const { config, auth, info } = context; transformTypes('update', fields); - return await objectsMutations.updateObject( + await objectsMutations.updateObject( className, objectId, fields, @@ -124,6 +143,22 @@ const load = function( auth, info ); + const selectedFields = getFieldNames(mutationInfo); + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + ); + + return await objectsQueries.getObject( + className, + objectId, + keys, + include, + undefined, + undefined, + config, + auth, + info + ); } catch (e) { parseGraphQLSchema.handleError(e); } @@ -138,19 +173,34 @@ const load = function( args: { objectId: defaultGraphQLTypes.OBJECT_ID_ATT, }, - type: new GraphQLNonNull(GraphQLBoolean), - async resolve(_source, args, context) { + type: new GraphQLNonNull(classGraphQLOutputType), + async resolve(_source, args, context, mutationInfo) { try { const { objectId } = args; const { config, auth, info } = context; - - return await objectsMutations.deleteObject( + const selectedFields = getFieldNames(mutationInfo); + const { keys, include } = parseClassTypes.extractKeysAndInclude( + selectedFields + ); + const object = await objectsQueries.getObject( + className, + objectId, + keys, + include, + undefined, + undefined, + config, + auth, + info + ); + await objectsMutations.deleteObject( className, objectId, config, auth, info ); + return object; } catch (e) { parseGraphQLSchema.handleError(e); } diff --git a/src/GraphQL/loaders/parseClassTypes.js b/src/GraphQL/loaders/parseClassTypes.js index d32adc845b..893c786f0c 100644 --- a/src/GraphQL/loaders/parseClassTypes.js +++ b/src/GraphQL/loaders/parseClassTypes.js @@ -715,9 +715,28 @@ const load = ( } }, {}), }); + + const userLogInInputTypeName = '_UserLoginFields'; + 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.parseClassTypes[ '_User' ].signUpInputType = userSignUpInputType; + parseGraphQLSchema.parseClassTypes[ + '_User' + ].logInInputType = userLogInInputType; parseGraphQLSchema.graphQLTypes.push(userSignUpInputType); } }; diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 71c0c46670..cb2482eb67 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -1,11 +1,5 @@ -import { - GraphQLBoolean, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString, -} from 'graphql'; +import { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType } from 'graphql'; import UsersRouter from '../../Routers/UsersRouter'; -import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as objectsMutations from './objectsMutations'; const usersRouter = new UsersRouter(); @@ -24,19 +18,30 @@ const load = parseGraphQLSchema => { type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType, }, }, - type: new GraphQLNonNull(defaultGraphQLTypes.SIGN_UP_RESULT), + type: new GraphQLNonNull(parseGraphQLSchema.meType), async resolve(_source, args, context) { try { const { fields } = args; const { config, auth, info } = context; - return await objectsMutations.createObject( + await objectsMutations.createObject( '_User', fields, config, auth, info ); + + return (await usersRouter.handleLogIn({ + body: { + username: fields.username, + password: fields.password, + }, + query: {}, + config, + auth, + info, + })).response; } catch (e) { parseGraphQLSchema.handleError(e); } @@ -46,19 +51,17 @@ const load = parseGraphQLSchema => { fields.logIn = { description: 'The logIn mutation can be used to log the user in.', args: { - 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), + input: { + description: 'This is data needed to login', + type: parseGraphQLSchema.parseClassTypes['_User'].logInInputType, }, }, type: new GraphQLNonNull(parseGraphQLSchema.meType), async resolve(_source, args, context) { try { - const { username, password } = args; + const { + input: { username, password }, + } = args; const { config, auth, info } = context; return (await usersRouter.handleLogIn({ From b237debea82c51fd3ce721b23dcb0eb3747b9584 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 9 Aug 2019 20:28:45 +0200 Subject: [PATCH 2/3] Add Optimization on Mutation --- src/GraphQL/loaders/parseClassMutations.js | 117 +++++++++++++++------ 1 file changed, 84 insertions(+), 33 deletions(-) diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 7ad54497f9..eed18d39da 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -12,6 +12,28 @@ const getParseClassMutationConfig = function( return (parseClassConfig && parseClassConfig.mutation) || {}; }; +const getOnlyRequiredFields = ( + updatedFields, + selectedFieldsString, + includedFieldsString, + nativeObjectFields +) => { + const includedFields = includedFieldsString.split(','); + const selectedFields = selectedFieldsString.split(','); + const missingFields = selectedFields + .filter( + field => + (!updatedFields[field] && !nativeObjectFields.includes(field)) || + includedFields.includes(field) + ) + .join(','); + if (!missingFields.length) { + return { needGet: false, keys: '' }; + } else { + return { needGet: true, keys: missingFields }; + } +}; + const load = function( parseGraphQLSchema, parseClass, @@ -85,11 +107,11 @@ const load = function( type: new GraphQLNonNull(classGraphQLOutputType), async resolve(_source, args, context, mutationInfo) { try { - const { fields } = args; + let { fields } = args; + if (!fields) fields = {}; const { config, auth, info } = context; - transformTypes('create', fields); - const { objectId } = await objectsMutations.createObject( + const createdObject = await objectsMutations.createObject( className, fields, config, @@ -100,18 +122,32 @@ const load = function( const { keys, include } = parseClassTypes.extractKeysAndInclude( selectedFields ); - - return await objectsQueries.getObject( - className, - objectId, + const { keys: requiredKeys, needGet } = getOnlyRequiredFields( + fields, keys, include, - undefined, - undefined, - config, - auth, - info + ['objectId', 'createdAt', 'updatedAt'] ); + let optimizedObject = {}; + if (needGet) { + optimizedObject = await objectsQueries.getObject( + className, + createdObject.objectId, + requiredKeys, + include, + undefined, + undefined, + config, + auth, + info + ); + } + return { + ...createdObject, + updatedAt: createdObject.createdAt, + ...fields, + ...optimizedObject, + }; } catch (e) { parseGraphQLSchema.handleError(e); } @@ -135,7 +171,7 @@ const load = function( transformTypes('update', fields); - await objectsMutations.updateObject( + const updatedObject = await objectsMutations.updateObject( className, objectId, fields, @@ -148,17 +184,27 @@ const load = function( selectedFields ); - return await objectsQueries.getObject( - className, - objectId, + const { keys: requiredKeys, needGet } = getOnlyRequiredFields( + fields, keys, include, - undefined, - undefined, - config, - auth, - info + ['objectId', 'updatedAt'] ); + let optimizedObject = {}; + if (needGet) { + optimizedObject = await objectsQueries.getObject( + className, + objectId, + requiredKeys, + include, + undefined, + undefined, + config, + auth, + info + ); + } + return { ...updatedObject, ...fields, ...optimizedObject }; } catch (e) { parseGraphQLSchema.handleError(e); } @@ -182,17 +228,22 @@ const load = function( const { keys, include } = parseClassTypes.extractKeysAndInclude( selectedFields ); - const object = await objectsQueries.getObject( - className, - objectId, - keys, - include, - undefined, - undefined, - config, - auth, - info - ); + + let optimizedObject = {}; + const splitedKeys = keys.split(','); + if (splitedKeys.length > 1 || splitedKeys[0] !== 'objectId') { + optimizedObject = await objectsQueries.getObject( + className, + objectId, + keys, + include, + undefined, + undefined, + config, + auth, + info + ); + } await objectsMutations.deleteObject( className, objectId, @@ -200,7 +251,7 @@ const load = function( auth, info ); - return object; + return { objectId: objectId, ...optimizedObject }; } catch (e) { parseGraphQLSchema.handleError(e); } From aadc9bd599d70b39494e9747856e077ca48b96ed Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 9 Aug 2019 20:49:10 +0200 Subject: [PATCH 3/3] Optimize SignUp --- src/GraphQL/loaders/usersMutations.js | 18 ++---- src/GraphQL/loaders/usersQueries.js | 81 ++++++++++++++------------- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index cb2482eb67..57349b2a11 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -1,6 +1,7 @@ import { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType } from 'graphql'; import UsersRouter from '../../Routers/UsersRouter'; import * as objectsMutations from './objectsMutations'; +import { getUserFromSessionToken } from './usersQueries'; const usersRouter = new UsersRouter(); @@ -19,12 +20,12 @@ const load = parseGraphQLSchema => { }, }, type: new GraphQLNonNull(parseGraphQLSchema.meType), - async resolve(_source, args, context) { + async resolve(_source, args, context, mutationInfo) { try { const { fields } = args; const { config, auth, info } = context; - await objectsMutations.createObject( + const { sessionToken } = await objectsMutations.createObject( '_User', fields, config, @@ -32,16 +33,9 @@ const load = parseGraphQLSchema => { info ); - return (await usersRouter.handleLogIn({ - body: { - username: fields.username, - password: fields.password, - }, - query: {}, - config, - auth, - info, - })).response; + info.sessionToken = sessionToken; + + return await getUserFromSessionToken(config, info, mutationInfo); } catch (e) { parseGraphQLSchema.handleError(e); } diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index f5f6c3443e..a761035cca 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -5,6 +5,46 @@ import rest from '../../rest'; import Auth from '../../Auth'; import { extractKeysAndInclude } from './parseClassTypes'; +const getUserFromSessionToken = async (config, info, queryInfo) => { + if (!info || !info.sessionToken) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); + } + const sessionToken = info.sessionToken; + const selectedFields = getFieldNames(queryInfo); + + const { include } = extractKeysAndInclude(selectedFields); + const response = await rest.find( + config, + Auth.master(config), + '_Session', + { sessionToken }, + { + include: include + .split(',') + .map(included => `user.${included}`) + .join(','), + }, + info.clientVersion + ); + if ( + !response.results || + response.results.length == 0 || + !response.results[0].user + ) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); + } else { + const user = response.results[0].user; + user.sessionToken = sessionToken; + return user; + } +}; + const load = parseGraphQLSchema => { if (parseGraphQLSchema.isUsersClassDisabled) { return; @@ -17,44 +57,7 @@ const load = parseGraphQLSchema => { async resolve(_source, _args, context, queryInfo) { try { const { config, info } = context; - - if (!info || !info.sessionToken) { - throw new Parse.Error( - Parse.Error.INVALID_SESSION_TOKEN, - 'Invalid session token' - ); - } - const sessionToken = info.sessionToken; - const selectedFields = getFieldNames(queryInfo); - - const { include } = extractKeysAndInclude(selectedFields); - const response = await rest.find( - config, - Auth.master(config), - '_Session', - { sessionToken }, - { - include: include - .split(',') - .map(included => `user.${included}`) - .join(','), - }, - info.clientVersion - ); - if ( - !response.results || - response.results.length == 0 || - !response.results[0].user - ) { - throw new Parse.Error( - Parse.Error.INVALID_SESSION_TOKEN, - 'Invalid session token' - ); - } else { - const user = response.results[0].user; - user.sessionToken = sessionToken; - return user; - } + return await getUserFromSessionToken(config, info, queryInfo); } catch (e) { parseGraphQLSchema.handleError(e); } @@ -75,4 +78,4 @@ const load = parseGraphQLSchema => { }; }; -export { load }; +export { load, getUserFromSessionToken };