From 30a5aa0b61de7490389449a1cbaaf4d83ea62151 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Tue, 28 Jan 2020 04:16:53 +0100 Subject: [PATCH] GraphQL: Nested File Upload (#6372) * wip * wip * tested * wip * tested --- spec/ParseGraphQLServer.spec.js | 94 ++++++++++++++++--- src/GraphQL/loaders/defaultGraphQLTypes.js | 16 ++++ src/GraphQL/loaders/filesMutations.js | 101 ++++++++++----------- src/GraphQL/transformers/inputType.js | 2 +- src/GraphQL/transformers/mutation.js | 47 ++++++---- 5 files changed, 178 insertions(+), 82 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index d6bd1836..ca08f227 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -9228,6 +9228,7 @@ describe('ParseGraphQLServer', () => { ); const someFieldValue = result.data.createFile.fileInfo.name; + const someFieldObjectValue = result.data.createFile.fileInfo; await apolloClient.mutate({ mutation: gql` @@ -9253,17 +9254,25 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); - const createResult = await apolloClient.mutate({ - mutation: gql` + const body2 = new FormData(); + body2.append( + 'operations', + JSON.stringify({ + query: ` mutation CreateSomeObject( $fields1: CreateSomeClassFieldsInput $fields2: CreateSomeClassFieldsInput + $fields3: CreateSomeClassFieldsInput ) { createSomeClass1: createSomeClass( input: { fields: $fields1 } ) { someClass { id + someField { + name + url + } } } createSomeClass2: createSomeClass( @@ -9271,20 +9280,79 @@ describe('ParseGraphQLServer', () => { ) { someClass { id + someField { + name + url + } + } + } + createSomeClass3: createSomeClass( + input: { fields: $fields3 } + ) { + someClass { + id + someField { + name + url + } } } } - `, - variables: { - fields1: { - someField: someFieldValue, + `, + variables: { + fields1: { + someField: { file: someFieldValue }, + }, + fields2: { + someField: { + file: { + name: someFieldObjectValue.name, + url: someFieldObjectValue.url, + __type: 'File', + }, + }, + }, + fields3: { + someField: { upload: null }, + }, }, - fields2: { - someField: someFieldValue.name, - }, - }, + }) + ); + body2.append( + 'map', + JSON.stringify({ 1: ['variables.fields3.someField.upload'] }) + ); + body2.append('1', 'My File Content', { + filename: 'myFileName.txt', + contentType: 'text/plain', }); + res = await fetch('http://localhost:13377/graphql', { + method: 'POST', + headers, + body: body2, + }); + expect(res.status).toEqual(200); + const result2 = JSON.parse(await res.text()); + expect( + result2.data.createSomeClass1.someClass.someField.name + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + expect( + result2.data.createSomeClass1.someClass.someField.url + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + expect( + result2.data.createSomeClass2.someClass.someField.name + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + expect( + result2.data.createSomeClass2.someClass.someField.url + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + expect( + result2.data.createSomeClass3.someClass.someField.name + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + expect( + result2.data.createSomeClass3.someClass.someField.url + ).toEqual(jasmine.stringMatching(/_myFileName.txt$/)); + const schema = await new Parse.Schema('SomeClass').get(); expect(schema.fields.someField.type).toEqual('File'); @@ -9324,7 +9392,7 @@ describe('ParseGraphQLServer', () => { } `, variables: { - id: createResult.data.createSomeClass1.someClass.id, + id: result2.data.createSomeClass1.someClass.id, }, }); @@ -9335,8 +9403,8 @@ describe('ParseGraphQLServer', () => { expect(getResult.data.someClass.someField.url).toEqual( result.data.createFile.fileInfo.url ); - expect(getResult.data.findSomeClass1.edges.length).toEqual(1); - expect(getResult.data.findSomeClass2.edges.length).toEqual(1); + expect(getResult.data.findSomeClass1.edges.length).toEqual(3); + expect(getResult.data.findSomeClass2.edges.length).toEqual(3); res = await fetch(getResult.data.someClass.someField.url); diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index d9ddd67d..581a59a4 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -366,6 +366,20 @@ const FILE_INFO = new GraphQLObjectType({ }, }); +const FILE_INPUT = new GraphQLInputObjectType({ + name: 'FileInput', + fields: { + file: { + description: 'A File Scalar can be an url or a FileInfo object.', + type: FILE, + }, + upload: { + description: 'Use this field if you want to create a new file.', + type: GraphQLUpload, + }, + }, +}); + const GEO_POINT_FIELDS = { latitude: { description: 'This is the latitude.', @@ -1244,6 +1258,7 @@ const load = parseGraphQLSchema => { parseGraphQLSchema.addGraphQLType(BYTES, true); parseGraphQLSchema.addGraphQLType(FILE, true); parseGraphQLSchema.addGraphQLType(FILE_INFO, true); + parseGraphQLSchema.addGraphQLType(FILE_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_POINT_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_POINT, true); parseGraphQLSchema.addGraphQLType(PARSE_OBJECT, true); @@ -1301,6 +1316,7 @@ export { SELECT_INPUT, FILE, FILE_INFO, + FILE_INPUT, GEO_POINT_FIELDS, GEO_POINT_INPUT, GEO_POINT, diff --git a/src/GraphQL/loaders/filesMutations.js b/src/GraphQL/loaders/filesMutations.js index cf906a68..7e3cef78 100644 --- a/src/GraphQL/loaders/filesMutations.js +++ b/src/GraphQL/loaders/filesMutations.js @@ -5,6 +5,53 @@ import Parse from 'parse/node'; import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import logger from '../../logger'; +const handleUpload = async (upload, config) => { + 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 (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.' + ); + } + + 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}.` + ); + } +}; + const load = parseGraphQLSchema => { const createMutation = mutationWithClientMutationId({ name: 'CreateFile', @@ -26,57 +73,7 @@ const load = parseGraphQLSchema => { 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))); - }); - } - - 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.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { - throw new Parse.Error( - Parse.Error.INVALID_FILE_NAME, - 'Filename contains invalid characters.' - ); - } - - 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}.` - ); - } + return handleUpload(upload, config); } catch (e) { parseGraphQLSchema.handleError(e); } @@ -97,4 +94,4 @@ const load = parseGraphQLSchema => { ); }; -export { load }; +export { load, handleUpload }; diff --git a/src/GraphQL/transformers/inputType.js b/src/GraphQL/transformers/inputType.js index 87f690df..29c91a65 100644 --- a/src/GraphQL/transformers/inputType.js +++ b/src/GraphQL/transformers/inputType.js @@ -45,7 +45,7 @@ const transformInputTypeToGraphQL = ( return defaultGraphQLTypes.OBJECT; } case 'File': - return defaultGraphQLTypes.FILE; + return defaultGraphQLTypes.FILE_INPUT; case 'GeoPoint': return defaultGraphQLTypes.GEO_POINT_INPUT; case 'Polygon': diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 7bf55eee..6d5dbc60 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -1,5 +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'; @@ -40,6 +41,9 @@ const transformTypes = async ( case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT: fields[field] = transformers.polygon(fields[field]); break; + case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT: + fields[field] = await transformers.file(fields[field], req); + break; case parseClass.fields[field].type === 'Relation': fields[field] = await transformers.relation( parseClass.fields[field].targetClass, @@ -68,6 +72,15 @@ const transformTypes = async ( }; const transformers = { + file: async ({ file, upload }, { config }) => { + if (upload) { + const { fileInfo } = await handleUpload(upload, config); + return { name: fileInfo.name, __type: 'File' }; + } else if (file && file.name) { + return { name: file.name, __type: 'File' }; + } + throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.'); + }, polygon: value => ({ __type: 'Polygon', coordinates: value.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]), @@ -122,22 +135,24 @@ const transformers = { let nestedObjectsToAdd = []; if (value.createAndAdd) { - nestedObjectsToAdd = (await Promise.all( - value.createAndAdd.map(async input => { - const parseFields = await transformTypes('create', input, { - className: targetClass, - parseGraphQLSchema, - req: { config, auth, info }, - }); - return objectsMutations.createObject( - targetClass, - parseFields, - config, - auth, - info - ); - }) - )).map(object => ({ + nestedObjectsToAdd = ( + await Promise.all( + value.createAndAdd.map(async input => { + const parseFields = await transformTypes('create', input, { + className: targetClass, + parseGraphQLSchema, + req: { config, auth, info }, + }); + return objectsMutations.createObject( + targetClass, + parseFields, + config, + auth, + info + ); + }) + ) + ).map(object => ({ __type: 'Pointer', className: targetClass, objectId: object.objectId,