GraphQL: Nested File Upload (#6372)
* wip * wip * tested * wip * tested
This commit is contained in:
committed by
Antonio Davi Macedo Coelho de Castro
parent
df3fa029bc
commit
30a5aa0b61
@@ -9228,6 +9228,7 @@ describe('ParseGraphQLServer', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const someFieldValue = result.data.createFile.fileInfo.name;
|
const someFieldValue = result.data.createFile.fileInfo.name;
|
||||||
|
const someFieldObjectValue = result.data.createFile.fileInfo;
|
||||||
|
|
||||||
await apolloClient.mutate({
|
await apolloClient.mutate({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
@@ -9253,17 +9254,25 @@ describe('ParseGraphQLServer', () => {
|
|||||||
|
|
||||||
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
|
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
|
||||||
|
|
||||||
const createResult = await apolloClient.mutate({
|
const body2 = new FormData();
|
||||||
mutation: gql`
|
body2.append(
|
||||||
|
'operations',
|
||||||
|
JSON.stringify({
|
||||||
|
query: `
|
||||||
mutation CreateSomeObject(
|
mutation CreateSomeObject(
|
||||||
$fields1: CreateSomeClassFieldsInput
|
$fields1: CreateSomeClassFieldsInput
|
||||||
$fields2: CreateSomeClassFieldsInput
|
$fields2: CreateSomeClassFieldsInput
|
||||||
|
$fields3: CreateSomeClassFieldsInput
|
||||||
) {
|
) {
|
||||||
createSomeClass1: createSomeClass(
|
createSomeClass1: createSomeClass(
|
||||||
input: { fields: $fields1 }
|
input: { fields: $fields1 }
|
||||||
) {
|
) {
|
||||||
someClass {
|
someClass {
|
||||||
id
|
id
|
||||||
|
someField {
|
||||||
|
name
|
||||||
|
url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createSomeClass2: createSomeClass(
|
createSomeClass2: createSomeClass(
|
||||||
@@ -9271,20 +9280,79 @@ describe('ParseGraphQLServer', () => {
|
|||||||
) {
|
) {
|
||||||
someClass {
|
someClass {
|
||||||
id
|
id
|
||||||
|
someField {
|
||||||
|
name
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createSomeClass3: createSomeClass(
|
||||||
|
input: { fields: $fields3 }
|
||||||
|
) {
|
||||||
|
someClass {
|
||||||
|
id
|
||||||
|
someField {
|
||||||
|
name
|
||||||
|
url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: {
|
variables: {
|
||||||
fields1: {
|
fields1: {
|
||||||
someField: someFieldValue,
|
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();
|
const schema = await new Parse.Schema('SomeClass').get();
|
||||||
expect(schema.fields.someField.type).toEqual('File');
|
expect(schema.fields.someField.type).toEqual('File');
|
||||||
|
|
||||||
@@ -9324,7 +9392,7 @@ describe('ParseGraphQLServer', () => {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: {
|
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(
|
expect(getResult.data.someClass.someField.url).toEqual(
|
||||||
result.data.createFile.fileInfo.url
|
result.data.createFile.fileInfo.url
|
||||||
);
|
);
|
||||||
expect(getResult.data.findSomeClass1.edges.length).toEqual(1);
|
expect(getResult.data.findSomeClass1.edges.length).toEqual(3);
|
||||||
expect(getResult.data.findSomeClass2.edges.length).toEqual(1);
|
expect(getResult.data.findSomeClass2.edges.length).toEqual(3);
|
||||||
|
|
||||||
res = await fetch(getResult.data.someClass.someField.url);
|
res = await fetch(getResult.data.someClass.someField.url);
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
const GEO_POINT_FIELDS = {
|
||||||
latitude: {
|
latitude: {
|
||||||
description: 'This is the latitude.',
|
description: 'This is the latitude.',
|
||||||
@@ -1244,6 +1258,7 @@ const load = parseGraphQLSchema => {
|
|||||||
parseGraphQLSchema.addGraphQLType(BYTES, true);
|
parseGraphQLSchema.addGraphQLType(BYTES, true);
|
||||||
parseGraphQLSchema.addGraphQLType(FILE, true);
|
parseGraphQLSchema.addGraphQLType(FILE, true);
|
||||||
parseGraphQLSchema.addGraphQLType(FILE_INFO, true);
|
parseGraphQLSchema.addGraphQLType(FILE_INFO, true);
|
||||||
|
parseGraphQLSchema.addGraphQLType(FILE_INPUT, true);
|
||||||
parseGraphQLSchema.addGraphQLType(GEO_POINT_INPUT, true);
|
parseGraphQLSchema.addGraphQLType(GEO_POINT_INPUT, true);
|
||||||
parseGraphQLSchema.addGraphQLType(GEO_POINT, true);
|
parseGraphQLSchema.addGraphQLType(GEO_POINT, true);
|
||||||
parseGraphQLSchema.addGraphQLType(PARSE_OBJECT, true);
|
parseGraphQLSchema.addGraphQLType(PARSE_OBJECT, true);
|
||||||
@@ -1301,6 +1316,7 @@ export {
|
|||||||
SELECT_INPUT,
|
SELECT_INPUT,
|
||||||
FILE,
|
FILE,
|
||||||
FILE_INFO,
|
FILE_INFO,
|
||||||
|
FILE_INPUT,
|
||||||
GEO_POINT_FIELDS,
|
GEO_POINT_FIELDS,
|
||||||
GEO_POINT_INPUT,
|
GEO_POINT_INPUT,
|
||||||
GEO_POINT,
|
GEO_POINT,
|
||||||
|
|||||||
@@ -5,6 +5,53 @@ import Parse from 'parse/node';
|
|||||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||||
import logger from '../../logger';
|
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 load = parseGraphQLSchema => {
|
||||||
const createMutation = mutationWithClientMutationId({
|
const createMutation = mutationWithClientMutationId({
|
||||||
name: 'CreateFile',
|
name: 'CreateFile',
|
||||||
@@ -26,57 +73,7 @@ const load = parseGraphQLSchema => {
|
|||||||
try {
|
try {
|
||||||
const { upload } = args;
|
const { upload } = args;
|
||||||
const { config } = context;
|
const { config } = context;
|
||||||
|
return handleUpload(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}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
parseGraphQLSchema.handleError(e);
|
parseGraphQLSchema.handleError(e);
|
||||||
}
|
}
|
||||||
@@ -97,4 +94,4 @@ const load = parseGraphQLSchema => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { load };
|
export { load, handleUpload };
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const transformInputTypeToGraphQL = (
|
|||||||
return defaultGraphQLTypes.OBJECT;
|
return defaultGraphQLTypes.OBJECT;
|
||||||
}
|
}
|
||||||
case 'File':
|
case 'File':
|
||||||
return defaultGraphQLTypes.FILE;
|
return defaultGraphQLTypes.FILE_INPUT;
|
||||||
case 'GeoPoint':
|
case 'GeoPoint':
|
||||||
return defaultGraphQLTypes.GEO_POINT_INPUT;
|
return defaultGraphQLTypes.GEO_POINT_INPUT;
|
||||||
case 'Polygon':
|
case 'Polygon':
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Parse from 'parse/node';
|
import Parse from 'parse/node';
|
||||||
import { fromGlobalId } from 'graphql-relay';
|
import { fromGlobalId } from 'graphql-relay';
|
||||||
|
import { handleUpload } from '../loaders/filesMutations';
|
||||||
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
|
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
|
||||||
import * as objectsMutations from '../helpers/objectsMutations';
|
import * as objectsMutations from '../helpers/objectsMutations';
|
||||||
|
|
||||||
@@ -40,6 +41,9 @@ const transformTypes = async (
|
|||||||
case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT:
|
case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT:
|
||||||
fields[field] = transformers.polygon(fields[field]);
|
fields[field] = transformers.polygon(fields[field]);
|
||||||
break;
|
break;
|
||||||
|
case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT:
|
||||||
|
fields[field] = await transformers.file(fields[field], req);
|
||||||
|
break;
|
||||||
case parseClass.fields[field].type === 'Relation':
|
case parseClass.fields[field].type === 'Relation':
|
||||||
fields[field] = await transformers.relation(
|
fields[field] = await transformers.relation(
|
||||||
parseClass.fields[field].targetClass,
|
parseClass.fields[field].targetClass,
|
||||||
@@ -68,6 +72,15 @@ const transformTypes = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const transformers = {
|
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 => ({
|
polygon: value => ({
|
||||||
__type: 'Polygon',
|
__type: 'Polygon',
|
||||||
coordinates: value.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]),
|
coordinates: value.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]),
|
||||||
@@ -122,22 +135,24 @@ const transformers = {
|
|||||||
let nestedObjectsToAdd = [];
|
let nestedObjectsToAdd = [];
|
||||||
|
|
||||||
if (value.createAndAdd) {
|
if (value.createAndAdd) {
|
||||||
nestedObjectsToAdd = (await Promise.all(
|
nestedObjectsToAdd = (
|
||||||
value.createAndAdd.map(async input => {
|
await Promise.all(
|
||||||
const parseFields = await transformTypes('create', input, {
|
value.createAndAdd.map(async input => {
|
||||||
className: targetClass,
|
const parseFields = await transformTypes('create', input, {
|
||||||
parseGraphQLSchema,
|
className: targetClass,
|
||||||
req: { config, auth, info },
|
parseGraphQLSchema,
|
||||||
});
|
req: { config, auth, info },
|
||||||
return objectsMutations.createObject(
|
});
|
||||||
targetClass,
|
return objectsMutations.createObject(
|
||||||
parseFields,
|
targetClass,
|
||||||
config,
|
parseFields,
|
||||||
auth,
|
config,
|
||||||
info
|
auth,
|
||||||
);
|
info
|
||||||
})
|
);
|
||||||
)).map(object => ({
|
})
|
||||||
|
)
|
||||||
|
).map(object => ({
|
||||||
__type: 'Pointer',
|
__type: 'Pointer',
|
||||||
className: targetClass,
|
className: targetClass,
|
||||||
objectId: object.objectId,
|
objectId: object.objectId,
|
||||||
|
|||||||
Reference in New Issue
Block a user