GraphQL: Nested File Upload (#6372)

* wip

* wip

* tested

* wip

* tested
This commit is contained in:
Antoine Cormouls
2020-01-28 04:16:53 +01:00
committed by Antonio Davi Macedo Coelho de Castro
parent df3fa029bc
commit 30a5aa0b61
5 changed files with 178 additions and 82 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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':

View File

@@ -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,