From 907ad4267c228d26cfcefe7848b30ce85ba7ff8f Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 20 Mar 2024 23:32:56 +0100 Subject: [PATCH] fix: `Required` option not handled correctly for special fields (File, GeoPoint, Polygon) on GraphQL API mutations (#8915) --- spec/AuthenticationAdaptersV2.spec.js | 2 +- spec/ParseGraphQLServer.spec.js | 65 +++++++++++++++++++++++++++ src/GraphQL/transformers/mutation.js | 18 ++++---- 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index 41c76b1f2b..e9486187ef 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -964,7 +964,7 @@ describe('Auth Adapter features', () => { allowExpiredAuthDataToken: false, }); logger = require('../lib/logger').logger; - spyOn(logger, 'error').and.callFake(() => { }); + spyOn(logger, 'error').and.callFake(() => {}); user = new Parse.User(); await user.save({ authData: { modernAdapter: { id: 'modernAdapter' } } }); const user2 = new Parse.User(); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 45d21a2b9b..dafe1a3ee2 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -9548,6 +9548,71 @@ describe('ParseGraphQLServer', () => { } }); + it('should support files on required file', async () => { + try { + parseServer = await global.reconfigureServer({ + publicServerURL: 'http://localhost:13377/parse', + }); + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SomeClassWithRequiredFile', { + someField: { type: 'File', required: true }, + }); + await resetGraphQLCache(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + + const body = new FormData(); + body.append( + 'operations', + JSON.stringify({ + query: ` + mutation CreateSomeObject( + $fields: CreateSomeClassWithRequiredFileFieldsInput + ) { + createSomeClassWithRequiredFile( + input: { fields: $fields } + ) { + someClassWithRequiredFile { + id + someField { + name + url + } + } + } + } + `, + variables: { + fields: { + someField: { upload: null }, + }, + }, + }) + ); + body.append('map', JSON.stringify({ 1: ['variables.fields.someField.upload'] })); + body.append('1', 'My File Content', { + filename: 'myFileName.txt', + contentType: 'text/plain', + }); + + const res = await fetch('http://localhost:13377/graphql', { + method: 'POST', + headers, + body, + }); + expect(res.status).toEqual(200); + const resText = await res.text(); + const result = JSON.parse(resText); + expect( + result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.name + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + expect( + result.data.createSomeClassWithRequiredFile.someClassWithRequiredFile.someField.url + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + } catch (e) { + handleError(e); + } + }); + it('should support file upload for on fly creation through pointer and relation', async () => { parseServer = await global.reconfigureServer({ publicServerURL: 'http://localhost:13377/parse', diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 17dd6a8d4b..1f1fd16ead 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -1,7 +1,6 @@ import Parse from 'parse/node'; import { fromGlobalId } from 'graphql-relay'; import { handleUpload } from '../loaders/filesMutations'; -import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes'; import * as objectsMutations from '../helpers/objectsMutations'; const transformTypes = async ( @@ -28,27 +27,28 @@ const transformTypes = async ( inputTypeField = classGraphQLUpdateTypeFields[field]; } if (inputTypeField) { - switch (true) { - case inputTypeField.type === defaultGraphQLTypes.GEO_POINT_INPUT: + const parseFieldType = parseClass.fields[field].type; + switch (parseFieldType) { + case 'GeoPoint': if (fields[field] === null) { fields[field] = { __op: 'Delete' }; break; } fields[field] = transformers.geoPoint(fields[field]); break; - case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT: + case 'Polygon': if (fields[field] === null) { fields[field] = { __op: 'Delete' }; break; } fields[field] = transformers.polygon(fields[field]); break; - case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT: - // Use `originalFields` to handle file upload since fields are a deepcopy and do not - // keep the file object + case 'File': + // We need to use the originalFields to handle the file upload + // since fields are a deepcopy and do not keep the file object fields[field] = await transformers.file(originalFields[field], req); break; - case parseClass.fields[field].type === 'Relation': + case 'Relation': fields[field] = await transformers.relation( parseClass.fields[field].targetClass, field, @@ -58,7 +58,7 @@ const transformTypes = async ( req ); break; - case parseClass.fields[field].type === 'Pointer': + case 'Pointer': if (fields[field] === null) { fields[field] = { __op: 'Delete' }; break;