Skip to content

Commit 30a5aa0

Browse files
Moumoulsdavimacedo
authored andcommitted
GraphQL: Nested File Upload (#6372)
* wip * wip * tested * wip * tested
1 parent df3fa02 commit 30a5aa0

File tree

5 files changed

+178
-82
lines changed

5 files changed

+178
-82
lines changed

spec/ParseGraphQLServer.spec.js

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9228,6 +9228,7 @@ describe('ParseGraphQLServer', () => {
92289228
);
92299229

92309230
const someFieldValue = result.data.createFile.fileInfo.name;
9231+
const someFieldObjectValue = result.data.createFile.fileInfo;
92319232

92329233
await apolloClient.mutate({
92339234
mutation: gql`
@@ -9253,38 +9254,105 @@ describe('ParseGraphQLServer', () => {
92539254

92549255
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
92559256

9256-
const createResult = await apolloClient.mutate({
9257-
mutation: gql`
9257+
const body2 = new FormData();
9258+
body2.append(
9259+
'operations',
9260+
JSON.stringify({
9261+
query: `
92589262
mutation CreateSomeObject(
92599263
$fields1: CreateSomeClassFieldsInput
92609264
$fields2: CreateSomeClassFieldsInput
9265+
$fields3: CreateSomeClassFieldsInput
92619266
) {
92629267
createSomeClass1: createSomeClass(
92639268
input: { fields: $fields1 }
92649269
) {
92659270
someClass {
92669271
id
9272+
someField {
9273+
name
9274+
url
9275+
}
92679276
}
92689277
}
92699278
createSomeClass2: createSomeClass(
92709279
input: { fields: $fields2 }
92719280
) {
92729281
someClass {
92739282
id
9283+
someField {
9284+
name
9285+
url
9286+
}
9287+
}
9288+
}
9289+
createSomeClass3: createSomeClass(
9290+
input: { fields: $fields3 }
9291+
) {
9292+
someClass {
9293+
id
9294+
someField {
9295+
name
9296+
url
9297+
}
92749298
}
92759299
}
92769300
}
9277-
`,
9278-
variables: {
9279-
fields1: {
9280-
someField: someFieldValue,
9281-
},
9282-
fields2: {
9283-
someField: someFieldValue.name,
9301+
`,
9302+
variables: {
9303+
fields1: {
9304+
someField: { file: someFieldValue },
9305+
},
9306+
fields2: {
9307+
someField: {
9308+
file: {
9309+
name: someFieldObjectValue.name,
9310+
url: someFieldObjectValue.url,
9311+
__type: 'File',
9312+
},
9313+
},
9314+
},
9315+
fields3: {
9316+
someField: { upload: null },
9317+
},
92849318
},
9285-
},
9319+
})
9320+
);
9321+
body2.append(
9322+
'map',
9323+
JSON.stringify({ 1: ['variables.fields3.someField.upload'] })
9324+
);
9325+
body2.append('1', 'My File Content', {
9326+
filename: 'myFileName.txt',
9327+
contentType: 'text/plain',
92869328
});
92879329

9330+
res = await fetch('http://localhost:13377/graphql', {
9331+
method: 'POST',
9332+
headers,
9333+
body: body2,
9334+
});
9335+
expect(res.status).toEqual(200);
9336+
const result2 = JSON.parse(await res.text());
9337+
expect(
9338+
result2.data.createSomeClass1.someClass.someField.name
9339+
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
9340+
expect(
9341+
result2.data.createSomeClass1.someClass.someField.url
9342+
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
9343+
expect(
9344+
result2.data.createSomeClass2.someClass.someField.name
9345+
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
9346+
expect(
9347+
result2.data.createSomeClass2.someClass.someField.url
9348+
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
9349+
expect(
9350+
result2.data.createSomeClass3.someClass.someField.name
9351+
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
9352+
expect(
9353+
result2.data.createSomeClass3.someClass.someField.url
9354+
).toEqual(jasmine.stringMatching(/_myFileName.txt$/));
9355+
92889356
const schema = await new Parse.Schema('SomeClass').get();
92899357
expect(schema.fields.someField.type).toEqual('File');
92909358

@@ -9324,7 +9392,7 @@ describe('ParseGraphQLServer', () => {
93249392
}
93259393
`,
93269394
variables: {
9327-
id: createResult.data.createSomeClass1.someClass.id,
9395+
id: result2.data.createSomeClass1.someClass.id,
93289396
},
93299397
});
93309398

@@ -9335,8 +9403,8 @@ describe('ParseGraphQLServer', () => {
93359403
expect(getResult.data.someClass.someField.url).toEqual(
93369404
result.data.createFile.fileInfo.url
93379405
);
9338-
expect(getResult.data.findSomeClass1.edges.length).toEqual(1);
9339-
expect(getResult.data.findSomeClass2.edges.length).toEqual(1);
9406+
expect(getResult.data.findSomeClass1.edges.length).toEqual(3);
9407+
expect(getResult.data.findSomeClass2.edges.length).toEqual(3);
93409408

93419409
res = await fetch(getResult.data.someClass.someField.url);
93429410

src/GraphQL/loaders/defaultGraphQLTypes.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,20 @@ const FILE_INFO = new GraphQLObjectType({
366366
},
367367
});
368368

369+
const FILE_INPUT = new GraphQLInputObjectType({
370+
name: 'FileInput',
371+
fields: {
372+
file: {
373+
description: 'A File Scalar can be an url or a FileInfo object.',
374+
type: FILE,
375+
},
376+
upload: {
377+
description: 'Use this field if you want to create a new file.',
378+
type: GraphQLUpload,
379+
},
380+
},
381+
});
382+
369383
const GEO_POINT_FIELDS = {
370384
latitude: {
371385
description: 'This is the latitude.',
@@ -1244,6 +1258,7 @@ const load = parseGraphQLSchema => {
12441258
parseGraphQLSchema.addGraphQLType(BYTES, true);
12451259
parseGraphQLSchema.addGraphQLType(FILE, true);
12461260
parseGraphQLSchema.addGraphQLType(FILE_INFO, true);
1261+
parseGraphQLSchema.addGraphQLType(FILE_INPUT, true);
12471262
parseGraphQLSchema.addGraphQLType(GEO_POINT_INPUT, true);
12481263
parseGraphQLSchema.addGraphQLType(GEO_POINT, true);
12491264
parseGraphQLSchema.addGraphQLType(PARSE_OBJECT, true);
@@ -1301,6 +1316,7 @@ export {
13011316
SELECT_INPUT,
13021317
FILE,
13031318
FILE_INFO,
1319+
FILE_INPUT,
13041320
GEO_POINT_FIELDS,
13051321
GEO_POINT_INPUT,
13061322
GEO_POINT,

src/GraphQL/loaders/filesMutations.js

Lines changed: 49 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,53 @@ import Parse from 'parse/node';
55
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
66
import logger from '../../logger';
77

8+
const handleUpload = async (upload, config) => {
9+
const { createReadStream, filename, mimetype } = await upload;
10+
let data = null;
11+
if (createReadStream) {
12+
const stream = createReadStream();
13+
data = await new Promise((resolve, reject) => {
14+
const chunks = [];
15+
stream
16+
.on('error', reject)
17+
.on('data', chunk => chunks.push(chunk))
18+
.on('end', () => resolve(Buffer.concat(chunks)));
19+
});
20+
}
21+
22+
if (!data || !data.length) {
23+
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.');
24+
}
25+
26+
if (filename.length > 128) {
27+
throw new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.');
28+
}
29+
30+
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
31+
throw new Parse.Error(
32+
Parse.Error.INVALID_FILE_NAME,
33+
'Filename contains invalid characters.'
34+
);
35+
}
36+
37+
try {
38+
return {
39+
fileInfo: await config.filesController.createFile(
40+
config,
41+
filename,
42+
data,
43+
mimetype
44+
),
45+
};
46+
} catch (e) {
47+
logger.error('Error creating a file: ', e);
48+
throw new Parse.Error(
49+
Parse.Error.FILE_SAVE_ERROR,
50+
`Could not store file: ${filename}.`
51+
);
52+
}
53+
};
54+
855
const load = parseGraphQLSchema => {
956
const createMutation = mutationWithClientMutationId({
1057
name: 'CreateFile',
@@ -26,57 +73,7 @@ const load = parseGraphQLSchema => {
2673
try {
2774
const { upload } = args;
2875
const { config } = context;
29-
30-
const { createReadStream, filename, mimetype } = await upload;
31-
let data = null;
32-
if (createReadStream) {
33-
const stream = createReadStream();
34-
data = await new Promise((resolve, reject) => {
35-
const chunks = [];
36-
stream
37-
.on('error', reject)
38-
.on('data', chunk => chunks.push(chunk))
39-
.on('end', () => resolve(Buffer.concat(chunks)));
40-
});
41-
}
42-
43-
if (!data || !data.length) {
44-
throw new Parse.Error(
45-
Parse.Error.FILE_SAVE_ERROR,
46-
'Invalid file upload.'
47-
);
48-
}
49-
50-
if (filename.length > 128) {
51-
throw new Parse.Error(
52-
Parse.Error.INVALID_FILE_NAME,
53-
'Filename too long.'
54-
);
55-
}
56-
57-
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
58-
throw new Parse.Error(
59-
Parse.Error.INVALID_FILE_NAME,
60-
'Filename contains invalid characters.'
61-
);
62-
}
63-
64-
try {
65-
return {
66-
fileInfo: await config.filesController.createFile(
67-
config,
68-
filename,
69-
data,
70-
mimetype
71-
),
72-
};
73-
} catch (e) {
74-
logger.error('Error creating a file: ', e);
75-
throw new Parse.Error(
76-
Parse.Error.FILE_SAVE_ERROR,
77-
`Could not store file: ${filename}.`
78-
);
79-
}
76+
return handleUpload(upload, config);
8077
} catch (e) {
8178
parseGraphQLSchema.handleError(e);
8279
}
@@ -97,4 +94,4 @@ const load = parseGraphQLSchema => {
9794
);
9895
};
9996

100-
export { load };
97+
export { load, handleUpload };

src/GraphQL/transformers/inputType.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const transformInputTypeToGraphQL = (
4545
return defaultGraphQLTypes.OBJECT;
4646
}
4747
case 'File':
48-
return defaultGraphQLTypes.FILE;
48+
return defaultGraphQLTypes.FILE_INPUT;
4949
case 'GeoPoint':
5050
return defaultGraphQLTypes.GEO_POINT_INPUT;
5151
case 'Polygon':

src/GraphQL/transformers/mutation.js

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Parse from 'parse/node';
22
import { fromGlobalId } from 'graphql-relay';
3+
import { handleUpload } from '../loaders/filesMutations';
34
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
45
import * as objectsMutations from '../helpers/objectsMutations';
56

@@ -40,6 +41,9 @@ const transformTypes = async (
4041
case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT:
4142
fields[field] = transformers.polygon(fields[field]);
4243
break;
44+
case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT:
45+
fields[field] = await transformers.file(fields[field], req);
46+
break;
4347
case parseClass.fields[field].type === 'Relation':
4448
fields[field] = await transformers.relation(
4549
parseClass.fields[field].targetClass,
@@ -68,6 +72,15 @@ const transformTypes = async (
6872
};
6973

7074
const transformers = {
75+
file: async ({ file, upload }, { config }) => {
76+
if (upload) {
77+
const { fileInfo } = await handleUpload(upload, config);
78+
return { name: fileInfo.name, __type: 'File' };
79+
} else if (file && file.name) {
80+
return { name: file.name, __type: 'File' };
81+
}
82+
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.');
83+
},
7184
polygon: value => ({
7285
__type: 'Polygon',
7386
coordinates: value.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]),
@@ -122,22 +135,24 @@ const transformers = {
122135
let nestedObjectsToAdd = [];
123136

124137
if (value.createAndAdd) {
125-
nestedObjectsToAdd = (await Promise.all(
126-
value.createAndAdd.map(async input => {
127-
const parseFields = await transformTypes('create', input, {
128-
className: targetClass,
129-
parseGraphQLSchema,
130-
req: { config, auth, info },
131-
});
132-
return objectsMutations.createObject(
133-
targetClass,
134-
parseFields,
135-
config,
136-
auth,
137-
info
138-
);
139-
})
140-
)).map(object => ({
138+
nestedObjectsToAdd = (
139+
await Promise.all(
140+
value.createAndAdd.map(async input => {
141+
const parseFields = await transformTypes('create', input, {
142+
className: targetClass,
143+
parseGraphQLSchema,
144+
req: { config, auth, info },
145+
});
146+
return objectsMutations.createObject(
147+
targetClass,
148+
parseFields,
149+
config,
150+
auth,
151+
info
152+
);
153+
})
154+
)
155+
).map(object => ({
141156
__type: 'Pointer',
142157
className: targetClass,
143158
objectId: object.objectId,

0 commit comments

Comments
 (0)