From 2b7882bfb0ec1c651b4474f664b6ba25a5a7fb8f Mon Sep 17 00:00:00 2001 From: eric-burel Date: Wed, 10 Jul 2019 12:19:02 +0200 Subject: [PATCH 01/18] update to Apollo 2 --- lib/client/interface.js | 1 + lib/client/main.js | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/client/interface.js b/lib/client/interface.js index 40b90f9..8c58518 100644 --- a/lib/client/interface.js +++ b/lib/client/interface.js @@ -68,6 +68,7 @@ function serializeFormData(obj, formDataObj = {}, namespace) { return formDataObj; } +// DEPRECATED with apollo 2 export const fileUploadMiddleware = { applyMiddleware({ request, options }, next) { if (isUpload(request)) { diff --git a/lib/client/main.js b/lib/client/main.js index 215a981..2d45161 100644 --- a/lib/client/main.js +++ b/lib/client/main.js @@ -1,12 +1,10 @@ -import { withRenderContext } from 'meteor/vulcan:lib'; -import { fileUploadMiddleware } from './interface'; +import { registerTerminatingLink } from 'meteor/vulcan:lib'; +import { createUploadLink } from 'apollo-upload-client'; + +registerTerminatingLink(createUploadLink()) + export * from '../modules'; export { default as generateFieldSchema } from '../modules/generateFieldSchemaBase'; export { default as createFSCollection } from './createFSCollectionStub'; -Meteor.startup(() => { - withRenderContext(renderContext => { - renderContext.apolloClient.networkInterface.use([fileUploadMiddleware]); - }); -}); From d1b084935e0db7a6c07093691d26925b34d3c2d6 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Thu, 11 Jul 2019 17:29:32 +0200 Subject: [PATCH 02/18] use new callback to register the upload middleware on /graphql --- lib/server/main.js | 1 + lib/server/uploadMiddleware.js | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/server/main.js b/lib/server/main.js index e788584..157a3b5 100644 --- a/lib/server/main.js +++ b/lib/server/main.js @@ -1,3 +1,4 @@ +console.log("HERE") import './uploadMiddleware'; import './addGraphQLSchemaAndResolvers'; diff --git a/lib/server/uploadMiddleware.js b/lib/server/uploadMiddleware.js index bfee311..c7a81aa 100644 --- a/lib/server/uploadMiddleware.js +++ b/lib/server/uploadMiddleware.js @@ -1,4 +1,4 @@ -import { webAppConnectHandlersUse } from 'meteor/vulcan:lib'; +import { addCallback } from 'meteor/vulcan:core'; import objectPath from 'object-path'; import brackets2Dots from 'brackets2dots'; import asyncBusboy from 'async-busboy'; @@ -11,10 +11,13 @@ function isUpload(req) { async function graphqlUploadMiddleware(req, res, next) { try { + console.log("run middleware") if (!isUpload(req)) { return next(); } + console.log("isUpload") const { files, fields } = await asyncBusboy(req); + console.log("files", files, fields) // console.log(''); // console.log('## graphqlUploadMiddleware ##########################################'); @@ -33,8 +36,6 @@ async function graphqlUploadMiddleware(req, res, next) { return next(); } -Meteor.startup(() => { - webAppConnectHandlersUse('graphql-upload-middleware', '/graphql', graphqlUploadMiddleware, { - order: 1 - }); -}); +addCallback('graphql.middlewares.setup', (WebApp) => { + WebApp.connectHandlers.use('/graphql', graphqlUploadMiddleware); +}) \ No newline at end of file From de218e26f818d0ea4cb41fc170c03f5b3925d6a8 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Thu, 11 Jul 2019 17:30:39 +0200 Subject: [PATCH 03/18] cleanup callback --- lib/server/uploadMiddleware.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/server/uploadMiddleware.js b/lib/server/uploadMiddleware.js index c7a81aa..ef94e9d 100644 --- a/lib/server/uploadMiddleware.js +++ b/lib/server/uploadMiddleware.js @@ -11,13 +11,10 @@ function isUpload(req) { async function graphqlUploadMiddleware(req, res, next) { try { - console.log("run middleware") if (!isUpload(req)) { return next(); } - console.log("isUpload") const { files, fields } = await asyncBusboy(req); - console.log("files", files, fields) // console.log(''); // console.log('## graphqlUploadMiddleware ##########################################'); @@ -36,6 +33,6 @@ async function graphqlUploadMiddleware(req, res, next) { return next(); } -addCallback('graphql.middlewares.setup', (WebApp) => { +addCallback('graphql.middlewares.setup', function useGqlUploadMiddleware(WebApp) { WebApp.connectHandlers.use('/graphql', graphqlUploadMiddleware); }) \ No newline at end of file From 8b614555f94494142220d49b247c4809586518ce Mon Sep 17 00:00:00 2001 From: eric-burel Date: Thu, 11 Jul 2019 18:22:08 +0200 Subject: [PATCH 04/18] set variables correctly server side --- lib/server/main.js | 1 - lib/server/uploadMiddleware.js | 13 ++++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/server/main.js b/lib/server/main.js index 157a3b5..e788584 100644 --- a/lib/server/main.js +++ b/lib/server/main.js @@ -1,4 +1,3 @@ -console.log("HERE") import './uploadMiddleware'; import './addGraphQLSchemaAndResolvers'; diff --git a/lib/server/uploadMiddleware.js b/lib/server/uploadMiddleware.js index ef94e9d..dbb4d54 100644 --- a/lib/server/uploadMiddleware.js +++ b/lib/server/uploadMiddleware.js @@ -11,16 +11,27 @@ function isUpload(req) { async function graphqlUploadMiddleware(req, res, next) { try { + console.log("run middleware") if (!isUpload(req)) { return next(); } + console.log("isUpload") const { files, fields } = await asyncBusboy(req); + fields.map = JSON.parse(fields.map) + console.log(fields.operations) + fields.operations = JSON.parse(fields.operations) + console.log("files", files, Object.keys(fields), typeof fields.map, typeof fields.operations) // console.log(''); // console.log('## graphqlUploadMiddleware ##########################################'); files.forEach(file => { - objectPath.set(fields, brackets2Dots(file.fieldname), file); + console.log(file.fieldname, typeof file.fieldname) + const fieldName = file.fieldname + const fieldPath = fields.map[fieldName][0] + console.log(fieldPath) + console.log(fields.map, fields.map[file.fieldname]) + objectPath.set(fields, `operations.${fieldPath}`, file); }); req.body = fields.data; From 21b1847893012321d0ef7510266443aefd6129a2 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Thu, 11 Jul 2019 18:47:25 +0200 Subject: [PATCH 05/18] try to update editHandler --- lib/server/createEditHandler.js | 7 +++--- lib/server/helpers/addFileFromReadable.js | 2 ++ lib/server/uploadMiddleware.js | 26 +++++++++++++++++------ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/server/createEditHandler.js b/lib/server/createEditHandler.js index e558bee..7aec793 100644 --- a/lib/server/createEditHandler.js +++ b/lib/server/createEditHandler.js @@ -4,10 +4,11 @@ import { addFileFromReadable } from './helpers'; const createEditHandler = (fieldName, FSCollection, getValue) => async function editHandler(modifier, document, currentUser) { + console.log("edit", document) if (modifier.$set && modifier.$set[fieldName]) { - const fieldValue = modifier.$set[fieldName]; - return fieldValue instanceof Readable - ? getValue(await addFileFromReadable(FSCollection, fieldValue)) + const fieldValue = await modifier.$set[fieldName]; + return fieldValue.createReadStream + ? getValue(await addFileFromReadable(FSCollection, fieldValue.createReadStream())) : fieldValue; } return undefined; diff --git a/lib/server/helpers/addFileFromReadable.js b/lib/server/helpers/addFileFromReadable.js index 3aa89b3..f5516aa 100644 --- a/lib/server/helpers/addFileFromReadable.js +++ b/lib/server/helpers/addFileFromReadable.js @@ -10,6 +10,7 @@ export default function addFileFromReadable(FSCollection, readable) { // ----------------------------------------------------- // Generate temporary file path const filePath = generateTemporaryFilePath(readable); + console.log("filePath", filePath) // Create a writable stream to where store the path const wstream = fs.createWriteStream(filePath); // Stream the file and save it.... @@ -18,6 +19,7 @@ export default function addFileFromReadable(FSCollection, readable) { // The collection method will delete the temporary files // ----------------------------------------------------- wstream.on('finish', args => { + console.log("adding file to FSCollection") FSCollection.addFile( filePath, { fileName: readable.filename }, diff --git a/lib/server/uploadMiddleware.js b/lib/server/uploadMiddleware.js index dbb4d54..19d55c9 100644 --- a/lib/server/uploadMiddleware.js +++ b/lib/server/uploadMiddleware.js @@ -16,27 +16,41 @@ async function graphqlUploadMiddleware(req, res, next) { return next(); } console.log("isUpload") - const { files, fields } = await asyncBusboy(req); + console.log('req', req) + // get fieles and fields from query, convert to dynamic JSON object to allow updates + //const { files, fields } = await asyncBusboy(req); + /* + console.log("before", fields) + /* fields.map = JSON.parse(fields.map) - console.log(fields.operations) fields.operations = JSON.parse(fields.operations) - console.log("files", files, Object.keys(fields), typeof fields.map, typeof fields.operations) + /* // console.log(''); // console.log('## graphqlUploadMiddleware ##########################################'); + // inject files in relevant fields files.forEach(file => { console.log(file.fieldname, typeof file.fieldname) const fieldName = file.fieldname const fieldPath = fields.map[fieldName][0] - console.log(fieldPath) - console.log(fields.map, fields.map[file.fieldname]) objectPath.set(fields, `operations.${fieldPath}`, file); }); - req.body = fields.data; + + // back to strings + */ + + /* + fields.map = JSON.stringify(fields.map) + fields.operations = JSON.stringify(fields.operations) + console.log("after", fields) + /* + /* + req.body = fields//fields.data; // console.log('files:', files); // console.log('req:', req); + */ } catch (e) { // console.error('error:', e); return next(e); From 3a4149314a264edc9b6a4dda7dcfc4fb706ff447 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Thu, 11 Jul 2019 19:06:17 +0200 Subject: [PATCH 06/18] update to newest Vulcan syntax --- lib/client/interface.js | 104 ++++++++--------- lib/server/createEditHandler.js | 17 --- lib/server/createEditHandlerMultiple.js | 22 ---- lib/server/createHandlers.js | 47 -------- lib/server/createInsertHandler.js | 14 --- lib/server/createInsertHandlerMultiple.js | 24 ---- lib/server/createResolverMultiple.js | 16 --- lib/server/createResolverSingle.js | 17 --- .../{createResolver.js => fieldResolver.js} | 36 +++++- lib/server/fieldUploadHandlers.js | 109 ++++++++++++++++++ lib/server/generateFieldSchema.js | 8 +- lib/server/main.js | 1 - lib/server/uploadMiddleware.js | 63 ---------- 13 files changed, 198 insertions(+), 280 deletions(-) delete mode 100644 lib/server/createEditHandler.js delete mode 100644 lib/server/createEditHandlerMultiple.js delete mode 100644 lib/server/createHandlers.js delete mode 100644 lib/server/createInsertHandler.js delete mode 100644 lib/server/createInsertHandlerMultiple.js delete mode 100644 lib/server/createResolverMultiple.js delete mode 100644 lib/server/createResolverSingle.js rename lib/server/{createResolver.js => fieldResolver.js} (54%) create mode 100644 lib/server/fieldUploadHandlers.js delete mode 100644 lib/server/uploadMiddleware.js diff --git a/lib/client/interface.js b/lib/client/interface.js index 8c58518..5c340f5 100644 --- a/lib/client/interface.js +++ b/lib/client/interface.js @@ -69,58 +69,58 @@ function serializeFormData(obj, formDataObj = {}, namespace) { } // DEPRECATED with apollo 2 -export const fileUploadMiddleware = { - applyMiddleware({ request, options }, next) { - if (isUpload(request)) { - const body = new FormData(); - const data = []; - const printed = printRequest(request); - data.push({ - operationName: printed.operationName || undefined, - debugName: printed.debugName || undefined, - query: printed.query, - variables: request.variables || {} - }); - const serialised = serializeFormData({ data }); - Object.entries(serialised).forEach(([name, value]) => { - if (typeof value !== typeof undefined) { - body.set(name, value); - } - }); - options.headers = options.headers || new Headers(); - options.headers.Accept = '*/*'; - options.headers['Content-Type'] = undefined; - options.body = body; - } - next(); - }, - applyBatchMiddleware({ requests, options }, next) { - if (isUpload(requests)) { - const body = new FormData(); - const data = []; - requests.forEach((request, i) => { - const printed = printRequest(request); - data.push({ - operationName: printed.operationName || undefined, - debugName: printed.debugName || undefined, - query: printed.query, - variables: request.variables || {} - }); - }); - const serialised = serializeFormData({ data }); - Object.entries(serialised).forEach(([name, value]) => { - if (typeof value !== typeof undefined) { - body.set(name, value); - } - }); - options.headers = options.headers || new Headers(); - options.headers.Accept = '*/*'; - options.headers['Content-Type'] = undefined; - options.body = body; - } - next(); - } -}; +//export const fileUploadMiddleware = { +// applyMiddleware({ request, options }, next) { +// if (isUpload(request)) { +// const body = new FormData(); +// const data = []; +// const printed = printRequest(request); +// data.push({ +// operationName: printed.operationName || undefined, +// debugName: printed.debugName || undefined, +// query: printed.query, +// variables: request.variables || {} +// }); +// const serialised = serializeFormData({ data }); +// Object.entries(serialised).forEach(([name, value]) => { +// if (typeof value !== typeof undefined) { +// body.set(name, value); +// } +// }); +// options.headers = options.headers || new Headers(); +// options.headers.Accept = '*/*'; +// options.headers['Content-Type'] = undefined; +// options.body = body; +// } +// next(); +// }, +// applyBatchMiddleware({ requests, options }, next) { +// if (isUpload(requests)) { +// const body = new FormData(); +// const data = []; +// requests.forEach((request, i) => { +// const printed = printRequest(request); +// data.push({ +// operationName: printed.operationName || undefined, +// debugName: printed.debugName || undefined, +// query: printed.query, +// variables: request.variables || {} +// }); +// }); +// const serialised = serializeFormData({ data }); +// Object.entries(serialised).forEach(([name, value]) => { +// if (typeof value !== typeof undefined) { +// body.set(name, value); +// } +// }); +// options.headers = options.headers || new Headers(); +// options.headers.Accept = '*/*'; +// options.headers['Content-Type'] = undefined; +// options.body = body; +// } +// next(); +// } +//}; export function createUploadNetworkInterface(opts) { const batchedInterface = createBatchingNetworkInterface(opts); diff --git a/lib/server/createEditHandler.js b/lib/server/createEditHandler.js deleted file mode 100644 index 7aec793..0000000 --- a/lib/server/createEditHandler.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Readable } from 'stream'; - -import { addFileFromReadable } from './helpers'; - -const createEditHandler = (fieldName, FSCollection, getValue) => - async function editHandler(modifier, document, currentUser) { - console.log("edit", document) - if (modifier.$set && modifier.$set[fieldName]) { - const fieldValue = await modifier.$set[fieldName]; - return fieldValue.createReadStream - ? getValue(await addFileFromReadable(FSCollection, fieldValue.createReadStream())) - : fieldValue; - } - return undefined; - }; - -export default createEditHandler; diff --git a/lib/server/createEditHandlerMultiple.js b/lib/server/createEditHandlerMultiple.js deleted file mode 100644 index b4435d5..0000000 --- a/lib/server/createEditHandlerMultiple.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Readable } from 'stream'; -import get from 'lodash/get'; - -import { addFileFromReadable } from './helpers'; - -const createEditHandler = (fieldName, FSCollection, getValue) => - async function editHandler(modifier, document, currentUser) { - const fieldValue = get(modifier, ['$set', fieldName]); - if (Array.isArray(fieldValue)) { - return Promise.all( - fieldValue.map(async value => { - if (value instanceof Readable) { - return getValue(await addFileFromReadable(FSCollection, value)); - } - return Promise.resolve(value); - }), - ); - } - return undefined; - }; - -export default createEditHandler; diff --git a/lib/server/createHandlers.js b/lib/server/createHandlers.js deleted file mode 100644 index 4c1152f..0000000 --- a/lib/server/createHandlers.js +++ /dev/null @@ -1,47 +0,0 @@ -import createInsertHandler from './createInsertHandler'; -import createEditHandler from './createEditHandler'; -import createInsertHandlerMultiple from './createInsertHandlerMultiple'; -import createEditHandlerMultiple from './createEditHandlerMultiple'; - -/** - * Retrieves the id of the file document. - * - * @param {Object} file - * @return {String} - * @function createHandlers~defaultGetValue - */ -const defaultGetValue = file => file._id; - -/** - * Creates field's insert and edit handlers. - * - * @param {Object} options Options - * @param {String} options.fieldName - * Field name - * @param {String} options.FSCollection - * FSCollection where to store the uploaded files as documents - * @param {Boolean=} options.multiple=false - * Whether the field will be multiple or not - * @param {Function=} options.getValue=createHandlers~defaultGetValue - * Function used to retrieve the value that will be stored in the field from - * the file document. Default to {@link createHandlers~defaultGetValue} - * @return {{onInsert: Function, onEdit: Function}} - */ -export default function createHandlers(options) { - const { - fieldName, - FSCollection, - multiple = false, - getValue = defaultGetValue, - } = options; - - const curryOnInsert = multiple - ? createInsertHandlerMultiple - : createInsertHandler; - const curryOnEdit = multiple ? createEditHandlerMultiple : createEditHandler; - - return { - onInsert: curryOnInsert(fieldName, FSCollection, getValue), - onEdit: curryOnEdit(fieldName, FSCollection, getValue), - }; -} diff --git a/lib/server/createInsertHandler.js b/lib/server/createInsertHandler.js deleted file mode 100644 index f490af3..0000000 --- a/lib/server/createInsertHandler.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Readable } from 'stream'; - -import { addFileFromReadable } from './helpers'; - -const createInsertHandler = (fieldName, FSCollection, getValue) => - async function insertHandler(document, currentUser) { - const fieldValue = document[fieldName]; - if (fieldValue instanceof Readable) { - return getValue(await addFileFromReadable(FSCollection, fieldValue)); - } - return fieldValue; - }; - -export default createInsertHandler; diff --git a/lib/server/createInsertHandlerMultiple.js b/lib/server/createInsertHandlerMultiple.js deleted file mode 100644 index 9499eda..0000000 --- a/lib/server/createInsertHandlerMultiple.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Readable } from 'stream'; - -import { addFileFromReadable } from './helpers'; - -export default function createInsertHandlerMultiple( - fieldName, - FSCollection, - getValue, -) { - return async function insertHandlerMultiple(document, currentUser) { - const fieldValue = document[fieldName]; - if (Array.isArray(fieldValue)) { - return Promise.all( - fieldValue.map(async value => { - if (value instanceof Readable) { - return getValue(await addFileFromReadable(FSCollection, value)); - } - return Promise.resolve(value); - }), - ); - } - return fieldValue; - }; -} diff --git a/lib/server/createResolverMultiple.js b/lib/server/createResolverMultiple.js deleted file mode 100644 index d736f95..0000000 --- a/lib/server/createResolverMultiple.js +++ /dev/null @@ -1,16 +0,0 @@ -import castArray from 'lodash/castArray'; -import compact from 'lodash/compact'; - -export default function createResolverMultiple( - fieldName, - FSCollection, - resolveId, -) { - return async document => { - const fileIds = compact(castArray(document[fieldName])).map(resolveId); - if (fileIds.length === 0) return []; - - const files = await FSCollection.loader.loadMany(fileIds); - return compact(files); - }; -} diff --git a/lib/server/createResolverSingle.js b/lib/server/createResolverSingle.js deleted file mode 100644 index 2aadc00..0000000 --- a/lib/server/createResolverSingle.js +++ /dev/null @@ -1,17 +0,0 @@ -import createResolverMultiple from './createResolverMultiple'; - -export default function createResolverSingle( - fieldName, - FSCollection, - resolveId, -) { - const resolverMultiple = createResolverMultiple( - fieldName, - FSCollection, - resolveId, - ); - return async document => { - const files = await resolverMultiple(document); - return files[0] || null; - } -} diff --git a/lib/server/createResolver.js b/lib/server/fieldResolver.js similarity index 54% rename from lib/server/createResolver.js rename to lib/server/fieldResolver.js index a230ddd..781ce53 100644 --- a/lib/server/createResolver.js +++ b/lib/server/fieldResolver.js @@ -1,9 +1,9 @@ -import createResolverMultiple from './createResolverMultiple'; -import createResolverSingle from './createResolverSingle'; import defaultResolveId from '../modules/defaultResolveId'; +import castArray from 'lodash/castArray'; +import compact from 'lodash/compact'; /** - * Creates field's resolver. + * Creates field's resolver to retrieve the file. * * @param {Object} options Options * @param {String} options.fieldName @@ -29,3 +29,33 @@ export default function createResolver(options) { ? createResolverMultiple(fieldName, FSCollection, resolveId) : createResolverSingle(fieldName, FSCollection, resolveId); } + +function createResolverMultiple( + fieldName, + FSCollection, + resolveId, +) { + return async document => { + const fileIds = compact(castArray(document[fieldName])).map(resolveId); + if (fileIds.length === 0) return []; + + const files = await FSCollection.loader.loadMany(fileIds); + return compact(files); + }; +} + +function createResolverSingle( + fieldName, + FSCollection, + resolveId, +) { + const resolverMultiple = createResolverMultiple( + fieldName, + FSCollection, + resolveId, + ); + return async document => { + const files = await resolverMultiple(document); + return files[0] || null; + } +} \ No newline at end of file diff --git a/lib/server/fieldUploadHandlers.js b/lib/server/fieldUploadHandlers.js new file mode 100644 index 0000000..4b32bb9 --- /dev/null +++ b/lib/server/fieldUploadHandlers.js @@ -0,0 +1,109 @@ +/** + * Create Vulcan field callbacks for file upload and edition + */ +import { Readable } from 'stream'; +import { addFileFromReadable } from './helpers'; +import get from 'lodash/get'; + +/** + * Retrieves the id of the file document. + * + * @param {Object} file + * @return {String} + * @function createHandlers~defaultGetValue + */ +const defaultGetValue = file => file._id; + +/** + * Creates field's create and update handlers. + * + * @param {Object} options Options + * @param {String} options.fieldName + * Field name + * @param {String} options.FSCollection + * FSCollection where to store the uploaded files as documents + * @param {Boolean=} options.multiple=false + * Whether the field will be multiple or not + * @param {Function=} options.getValue=createHandlers~defaultGetValue + * Function used to retrieve the value that will be stored in the field from + * the file document. Default to {@link createHandlers~defaultGetValue} + * @return {{onCreate: Function, onUpdate: Function}} + */ +export default function createUploadHandlers(options) { + const { + fieldName, + FSCollection, + multiple = false, + getValue = defaultGetValue, + } = options; + + const curryOnCreate = multiple + ? createHandlerMultiple + : createHandler; + const curryOnUpdate = multiple ? updateHandlerMultiple : updateHandler; + + return { + onCreate: curryOnCreate(fieldName, FSCollection, getValue), + onUpdate: curryOnUpdate(fieldName, FSCollection, getValue), + }; +} + + +export const createHandler = (fieldName, FSCollection, getValue) => + async function createHandler({ newDocument, currentUser }) { + const fieldValue = newDocument[fieldName]; + if (fieldValue instanceof Readable) { + return getValue(await addFileFromReadable(FSCollection, fieldValue)); + } + return fieldValue; + }; + +export const createHandlerMultiple = ( + fieldName, + FSCollection, + getValue, +) => + async function createHandlerMultiple({ newDocument, currentUser }) { + const fieldValue = newDocument[fieldName]; + if (Array.isArray(fieldValue)) { + return Promise.all( + fieldValue.map(async value => { + if (value instanceof Readable) { + return getValue(await addFileFromReadable(FSCollection, value)); + } + return Promise.resolve(value); + }), + ); + } + return fieldValue; + }; + + +export const updateHandler = (fieldName, FSCollection, getValue) => + async function updateHandler({ data, document, currentUser }) { + console.log(data) + console.log("update", document) + if (data.$set && data.$set[fieldName]) { + const fieldValue = await data.$set[fieldName]; + return fieldValue.createReadStream + ? getValue(await addFileFromReadable(FSCollection, fieldValue.createReadStream())) + : fieldValue; + } + return undefined; + }; + +export const updateHandlerMultiple = (fieldName, FSCollection, getValue) => + async function updateHandler({ data, document, currentUser }) { + const fieldValue = get(data, ['$set', fieldName]); + if (Array.isArray(fieldValue)) { + return Promise.all( + fieldValue.map(async value => { + if (value instanceof Readable) { + return getValue(await addFileFromReadable(FSCollection, value)); + } + return Promise.resolve(value); + }), + ); + } + return undefined; + }; \ No newline at end of file diff --git a/lib/server/generateFieldSchema.js b/lib/server/generateFieldSchema.js index 9638ba6..5bff72a 100644 --- a/lib/server/generateFieldSchema.js +++ b/lib/server/generateFieldSchema.js @@ -1,8 +1,8 @@ import merge from 'lodash/merge'; import generateFieldSchemaBase from '../modules/generateFieldSchemaBase'; -import createHandlers from './createHandlers'; -import createResolver from './createResolver'; +import fieldUploadHandlers from './fieldUploadHandlers'; +import fieldResolver from './fieldResolver'; /** * Server's function to generate a field schema. What this basically does is to @@ -23,9 +23,9 @@ export default (options = {}) => { return generateFieldSchemaBase({ ...options, fieldSchema: merge({}, fieldSchema, { - ...createHandlers(options), + ...fieldUploadHandlers(options), // onCreate / onUpdate callbacks to upload the file resolveAs: { - resolver: createResolver(options), + resolver: fieldResolver(options), }, }), }); diff --git a/lib/server/main.js b/lib/server/main.js index e788584..4442bb6 100644 --- a/lib/server/main.js +++ b/lib/server/main.js @@ -1,4 +1,3 @@ -import './uploadMiddleware'; import './addGraphQLSchemaAndResolvers'; export * from '../modules'; diff --git a/lib/server/uploadMiddleware.js b/lib/server/uploadMiddleware.js deleted file mode 100644 index 19d55c9..0000000 --- a/lib/server/uploadMiddleware.js +++ /dev/null @@ -1,63 +0,0 @@ -import { addCallback } from 'meteor/vulcan:core'; -import objectPath from 'object-path'; -import brackets2Dots from 'brackets2dots'; -import asyncBusboy from 'async-busboy'; - -function isUpload(req) { - return ( - req.headers['content-type'] && req.headers['content-type'].startsWith('multipart/form-data') - ); -} - -async function graphqlUploadMiddleware(req, res, next) { - try { - console.log("run middleware") - if (!isUpload(req)) { - return next(); - } - console.log("isUpload") - console.log('req', req) - // get fieles and fields from query, convert to dynamic JSON object to allow updates - //const { files, fields } = await asyncBusboy(req); - /* - console.log("before", fields) - /* - fields.map = JSON.parse(fields.map) - fields.operations = JSON.parse(fields.operations) - /* - - // console.log(''); - // console.log('## graphqlUploadMiddleware ##########################################'); - - // inject files in relevant fields - files.forEach(file => { - console.log(file.fieldname, typeof file.fieldname) - const fieldName = file.fieldname - const fieldPath = fields.map[fieldName][0] - objectPath.set(fields, `operations.${fieldPath}`, file); - }); - - // back to strings - */ - - /* - fields.map = JSON.stringify(fields.map) - fields.operations = JSON.stringify(fields.operations) - console.log("after", fields) - /* - /* - req.body = fields//fields.data; - - // console.log('files:', files); - // console.log('req:', req); - */ - } catch (e) { - // console.error('error:', e); - return next(e); - } - return next(); -} - -addCallback('graphql.middlewares.setup', function useGqlUploadMiddleware(WebApp) { - WebApp.connectHandlers.use('/graphql', graphqlUploadMiddleware); -}) \ No newline at end of file From 1d10d9a8fa4fa570fb353d61bce88fc9d5234c70 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Thu, 11 Jul 2019 19:28:18 +0200 Subject: [PATCH 07/18] update onUpdate to 1.12 syntax --- lib/server/fieldUploadHandlers.js | 35 +++++++++++++---------- lib/server/helpers/addFileFromReadable.js | 2 -- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/server/fieldUploadHandlers.js b/lib/server/fieldUploadHandlers.js index 4b32bb9..77a6a89 100644 --- a/lib/server/fieldUploadHandlers.js +++ b/lib/server/fieldUploadHandlers.js @@ -3,7 +3,6 @@ */ import { Readable } from 'stream'; import { addFileFromReadable } from './helpers'; -import get from 'lodash/get'; /** * Retrieves the id of the file document. @@ -81,10 +80,11 @@ export const createHandlerMultiple = ( export const updateHandler = (fieldName, FSCollection, getValue) => async function updateHandler({ data, document, currentUser }) { - console.log(data) - console.log("update", document) - if (data.$set && data.$set[fieldName]) { - const fieldValue = await data.$set[fieldName]; + if (typeof data[fieldName] === 'undefined') return undefined // no change + if (!data[fieldName]) { + // TODO trigger file removal + } else { + const fieldValue = await data[fieldName]; return fieldValue.createReadStream ? getValue(await addFileFromReadable(FSCollection, fieldValue.createReadStream())) : fieldValue; @@ -94,16 +94,21 @@ export const updateHandler = (fieldName, FSCollection, getValue) => export const updateHandlerMultiple = (fieldName, FSCollection, getValue) => async function updateHandler({ data, document, currentUser }) { - const fieldValue = get(data, ['$set', fieldName]); - if (Array.isArray(fieldValue)) { - return Promise.all( - fieldValue.map(async value => { - if (value instanceof Readable) { - return getValue(await addFileFromReadable(FSCollection, value)); - } - return Promise.resolve(value); - }), - ); + if (typeof data[fieldName] === 'undefined') return undefined // no change + if (!data[fieldName]) { + // TODO trigger file removal + } else { + const fieldValue = data[fieldName]; + if (Array.isArray(fieldValue)) { + return Promise.all( + fieldValue.map(async value => { + if (value instanceof Readable) { + return getValue(await addFileFromReadable(FSCollection, value)); + } + return Promise.resolve(value); + }), + ); + } } return undefined; }; \ No newline at end of file diff --git a/lib/server/helpers/addFileFromReadable.js b/lib/server/helpers/addFileFromReadable.js index f5516aa..3aa89b3 100644 --- a/lib/server/helpers/addFileFromReadable.js +++ b/lib/server/helpers/addFileFromReadable.js @@ -10,7 +10,6 @@ export default function addFileFromReadable(FSCollection, readable) { // ----------------------------------------------------- // Generate temporary file path const filePath = generateTemporaryFilePath(readable); - console.log("filePath", filePath) // Create a writable stream to where store the path const wstream = fs.createWriteStream(filePath); // Stream the file and save it.... @@ -19,7 +18,6 @@ export default function addFileFromReadable(FSCollection, readable) { // The collection method will delete the temporary files // ----------------------------------------------------- wstream.on('finish', args => { - console.log("adding file to FSCollection") FSCollection.addFile( filePath, { fileName: readable.filename }, From 324608f7cc5471dde07403143196111b6946b993 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Fri, 12 Jul 2019 14:49:54 +0200 Subject: [PATCH 08/18] update UploadInput with Components pattern --- lib/modules/components/Upload.jsx | 241 ----------------- lib/modules/components/UploadInput.jsx | 359 +++++++++++++++++++++++++ lib/modules/components/index.js | 2 +- lib/modules/containers/Upload.js | 32 --- lib/modules/index.js | 2 - lib/modules/register.js | 5 - 6 files changed, 360 insertions(+), 281 deletions(-) delete mode 100644 lib/modules/components/Upload.jsx create mode 100644 lib/modules/components/UploadInput.jsx delete mode 100644 lib/modules/containers/Upload.js delete mode 100644 lib/modules/register.js diff --git a/lib/modules/components/Upload.jsx b/lib/modules/components/Upload.jsx deleted file mode 100644 index bbe7c8a..0000000 --- a/lib/modules/components/Upload.jsx +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Stolen from vulcan:forms. Modified to work with GraphQL `File` scalars. - */ -import getContext from 'recompose/getContext'; -import upperFirst from 'lodash/upperFirst'; -import map from 'lodash/map'; -import reject from 'lodash/reject'; -import isEmpty from 'lodash/isEmpty'; -import reduce from 'lodash/reduce'; -import get from 'lodash/get'; -import isString from 'lodash/get'; -import stubTrue from 'lodash/stubTrue'; -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import Dropzone from 'react-dropzone'; -import 'isomorphic-fetch'; // patch for browser which don't have fetch implemented - -/* -Remove the nth item from an array -*/ -const removeNthItem = (array, n) => [ - ..._.first(array, n), - ..._.rest(array, n + 1), -]; - -/* -File Upload component -*/ -class Upload extends PureComponent { - static propTypes = { - name: PropTypes.string, - value: PropTypes.any, - label: PropTypes.string, - fileCheck: PropTypes.func, - FileRender: PropTypes.func.isRequired, - previewFromValue: PropTypes.func, - previewFromFile: PropTypes.func, - }; - - static defaultProps = { - fileCheck: stubTrue, - previewFromValue: () => '', - previewFromFile: value => ({ - name: get(value, 'name', ''), - url: get(value, 'preview', ''), - }), - }; - - - constructor(props, context) { - super(props, context); - - this.state = { - uploading: false, - errorMessage: null, - }; - } - - /* - When an file is uploaded - */ - onDrop = files => { - // Reset error state - this.setState({ - errorMessage: null, - }); - - // Check that files are valid - const errors = reject( - map(files, file => this.props.fileCheck(file)), - check => check === true - ); - // TODO add max files - - if (isEmpty(errors)) { - this.props.updateCurrentValues({ - [this.props.name]: this.enableMultiple() - ? [...this.getValue(), ...files] - : files[0], - }); - } else { - // TODO better error handling - // Set error message - const { - errorFilesTooBig = 'your file is too big', - errorFilesNotAllowedType = 'your file type is invalid', - } = this.props; - - this.setState({ - errorMessage: upperFirst( - reduce( - errors, - (result, error) => { - switch (error) { - case 'invalid-file-type': - return result + errorFilesNotAllowedType; - case 'exceed-max-allowed-size': - return result + errorFilesTooBig; - default: - return null; - } - }, - '' - ) - ), // TODO translate - }); - } - }; - - /* - Check the field's type to decide if the component should handle - multiple file uploads or not - */ - enableMultiple = () => ( - get(this.props, 'datatype.definitions[0].type') || - get(this.props, 'datatype[0].type') - ) === Array; - - getValue = () => this.props.value || (this.enableMultiple() ? [] : ''); - - /* - Remove the file at `index` (or just remove file if no index is passed) - */ - clearFile = index => { - const value = this.enableMultiple() - ? get(this.props.value, index) - : this.props.value; - if (!value) { - return; - } - - const url = get(this.preview(value, index), 'url'); - if (url) { - window.URL.revokeObjectURL(url); - } - - this.props.updateCurrentValues({ - [this.props.name]: this.enableMultiple() - ? removeNthItem(this.props.value, index) - : null, - }); - }; - - preview = (value, index = null) => { - return this.props.previewFromValue(value, index, this.props) || - this.props.previewFromFile(value, index, this.props); - }; - - render() { - const { - FileRender, - selectOrDropFilesMessage = 'Drop a file here, or click to select an file to upload.', - uploadingMessage = 'Uploading…', - ...props - } = this.props; - const value = this.getValue(); - const { uploading } = this.state; - - return ( -
- {this.props.label ? ( -
- {upperFirst(this.props.label)} -
- ) : null} -
-
- {isEmpty(value) || this.enableMultiple() ? ( - -
- {selectOrDropFilesMessage} - {/* Translate */} -
-
- ) : null} - - {!isEmpty(value) ? ( -
- {uploading ? {uploadingMessage} : null} -
- {this.enableMultiple() ? ( - value.map((value, index) => ( - - )) - ) : ( - - )} -
-
- ) : null} - {this.state.errorMessage ? ( -
- {this.state.errorMessage} -
- ) : null} -
-
-
- ); - } -} - -export default getContext({ - updateCurrentValues: PropTypes.func, - getDocument: PropTypes.func, -})(Upload); diff --git a/lib/modules/components/UploadInput.jsx b/lib/modules/components/UploadInput.jsx new file mode 100644 index 0000000..4e912b2 --- /dev/null +++ b/lib/modules/components/UploadInput.jsx @@ -0,0 +1,359 @@ +/** + * Stolen from vulcan:forms. Modified to work with GraphQL `File` scalars. + */ +import getContext from "recompose/getContext"; +import upperFirst from "lodash/upperFirst"; +import map from "lodash/map"; +import reject from "lodash/reject"; +import isEmpty from "lodash/isEmpty"; +import reduce from "lodash/reduce"; +import get from "lodash/get"; +import isString from "lodash/get"; +import stubTrue from "lodash/stubTrue"; +import React, { PureComponent } from "react"; +import PropTypes from "prop-types"; +import Dropzone from "react-dropzone"; +import "isomorphic-fetch"; // patch for browser which don't have fetch implemented +import { withComponents, registerComponent } from "meteor/vulcan:core"; +import withProps from "recompose/withProps"; +import { injectIntl } from "react-intl"; +import { compose } from "recompose"; + +// Visual components +const UploadInputLabel = ({ label }) => ( +
{upperFirst(label)}
+); +const UploadInputDropZoneContent = ({ selectOrDropFilesMessage }) => ( +
+ {selectOrDropFilesMessage} + {/* Translate */} +
+); +const UploadInputErrorMessage = ({ errrorMessage }) => ( +
+ {errorMessage} +
+); + +// Full input +const UploadInput = props => { + const { + FileRender, + selectOrDropFilesMessage = "Drop a file here, or click to select an file to upload.", + uploadingMessage = "Uploading…", + + label, + value, + enableMultiple, + uploading, + errorMessage, + onDrop, + preview, + clearFile, + Components + } = props; + + const { + UploadInputLabel, + UploadInputDropZoneContent, + UploadInputErrorMessage + } = Components; + + return ( +
+ {label ? : null} +
+
+ {isEmpty(value) || enableMultiple ? ( + + + + ) : null} + + {!isEmpty(value) ? ( +
+ {uploading ? {uploadingMessage} : null} +
+ {enableMultiple ? ( + value.map((value, index) => ( + + )) + ) : ( + + )} +
+
+ ) : null} + {errorMessage ? ( + + ) : null} +
+
+
+ ); +}; +UploadInput.propTypes = { + FileRender: PropTypes.any, + + selectOrDropFieldsMessage: PropTypes.string, + uploadingMessage: PropTypes.string, + label: PropTypes.string, + value: PropTypes.any, + enableMultiple: PropTypes.bool, + + preview: PropTypes.func.isRequired, + onDrop: PropTypes.func.isRequired, + clearFile: PropTypes.func.isRequired, + + uploading: PropTypes.bool, + errorMessage: PropTypes.string +}; + +/* +Remove the nth item from an array +*/ +const removeNthItem = (array, n) => [ + ..._.first(array, n), + ..._.rest(array, n + 1) +]; + +// Container with logic +/* +File Upload component +*/ +class UploadInputContainer extends PureComponent { + static propTypes = { + name: PropTypes.string, + value: PropTypes.any, + label: PropTypes.string, + fileCheck: PropTypes.func, + FileRender: PropTypes.func.isRequired, + previewFromValue: PropTypes.func, + previewFromFile: PropTypes.func + }; + + static defaultProps = { + fileCheck: stubTrue, + previewFromValue: () => "", + previewFromFile: value => ({ + name: get(value, "name", ""), + url: get(value, "preview", "") + }) + }; + + constructor(props, context) { + super(props, context); + + this.state = { + uploading: false, + errorMessage: null + }; + } + + /* + When an file is uploaded + */ + onDrop = files => { + // Reset error state + this.setState({ + errorMessage: null + }); + + // Check that files are valid + const errors = reject( + map(files, file => this.props.fileCheck(file)), + check => check === true + ); + // TODO add max files + + if (isEmpty(errors)) { + this.props.updateCurrentValues({ + [this.props.name]: this.enableMultiple() + ? [...this.getValue(), ...files] + : files[0] + }); + } else { + // TODO better error handling + // Set error message + const { + errorFilesTooBig = "your file is too big", + errorFilesNotAllowedType = "your file type is invalid" + } = this.props; + + this.setState({ + errorMessage: upperFirst( + reduce( + errors, + (result, error) => { + switch (error) { + case "invalid-file-type": + return result + errorFilesNotAllowedType; + case "exceed-max-allowed-size": + return result + errorFilesTooBig; + default: + return null; + } + }, + "" + ) + ) // TODO translate + }); + } + }; + + /* + Check the field's type to decide if the component should handle + multiple file uploads or not + */ + enableMultiple = () => + (get(this.props, "datatype.definitions[0].type") || + get(this.props, "datatype[0].type")) === Array; + + getValue = () => this.props.value || (this.enableMultiple() ? [] : ""); + + /* + Remove the file at `index` (or just remove file if no index is passed) + */ + clearFile = index => { + const value = this.enableMultiple() + ? get(this.props.value, index) + : this.props.value; + if (!value) { + return; + } + + const url = get(this.preview(value, index), "url"); + if (url) { + window.URL.revokeObjectURL(url); + } + + this.props.updateCurrentValues({ + [this.props.name]: this.enableMultiple() + ? removeNthItem(this.props.value, index) + : null + }); + }; + + preview = (value, index = null) => { + return ( + this.props.previewFromValue(value, index, this.props) || + this.props.previewFromFile(value, index, this.props) + ); + }; + + render() { + const { + FileRender, + selectOrDropFilesMessage = "Drop a file here, or click to select an file to upload.", + uploadingMessage = "Uploading…", + Components + } = this.props; + const { uploading, errorMessage } = this.state; + const { UploadInputInner } = Components; + return ( + + ); + } +} + +// add intel and context +const WrappedUploadInputContainer = compose( + injectIntl, + withProps(({ intl }) => ({ + selectOrDropFilesMessage: intl.formatMessage({ + id: "fileUpload.selectOrDropFilesMessage", + defaultMessage: "Drop a file here, or click to select an file to upload." + }), + + uploadingMessage: intl.formatMessage({ + id: "fileUpload.uploadingMessage", + defaultMessage: "Uploading..." + }), + errorFilesNotAllowedType: intl.formatMessage({ + id: "fileUpload.errorFilesNotAllowedType", + defaultMessage: "your file type is invalid" + }), + errorFilesTooBig: intl.formatMessage({ + id: "fileUpload.errorFilesTooBig", + defaultMessage: "your file is too big" + }), + removeMessage: intl.formatMessage({ + id: "remove", + defaultMessage: "remove" + }) + })), + getContext({ + updateCurrentValues: PropTypes.func, + getDocument: PropTypes.func + }) +)(UploadInputContainer); + +// registeration +registerComponent({ name: "UploadInputLabel", component: UploadInputLabel }); +registerComponent({ + name: "UploadInputDropZoneContent", + component: UploadInputDropZoneContent +}); +registerComponent({ + name: "UploadInputErrorMessage", + component: UploadInputErrorMessage +}); +registerComponent({ + name: "UploadInputInner", + component: UploadInput, + hocs: [withComponents] +}); +export default registerComponent({ + name: "UploadInput", + component: WrappedUploadInputContainer, + hocs: [withComponents] +}); diff --git a/lib/modules/components/index.js b/lib/modules/components/index.js index 4e714c5..105d3fd 100644 --- a/lib/modules/components/index.js +++ b/lib/modules/components/index.js @@ -1,3 +1,3 @@ -export { default as Upload } from './Upload.jsx'; +export { default as UploadInput } from './UploadInput.jsx'; export { default as Image } from './Image.jsx'; export { default as BasicFile } from './BasicFile.jsx'; diff --git a/lib/modules/containers/Upload.js b/lib/modules/containers/Upload.js deleted file mode 100644 index db5f77b..0000000 --- a/lib/modules/containers/Upload.js +++ /dev/null @@ -1,32 +0,0 @@ -import compose from 'recompose/compose'; -import withProps from 'recompose/withProps'; -import { injectIntl } from 'react-intl'; - -import Upload from '../components/Upload'; - -export default compose( - injectIntl, - withProps(({ intl }) => ({ - selectOrDropFilesMessage: intl.formatMessage({ - id: 'fileUpload.selectOrDropFilesMessage', - defaultMessage: 'Drop a file here, or click to select an file to upload.', - }), - - uploadingMessage: intl.formatMessage({ - id: 'fileUpload.uploadingMessage', - defaultMessage: 'Uploading...', - }), - errorFilesNotAllowedType: intl.formatMessage({ - id: 'fileUpload.errorFilesNotAllowedType', - defaultMessage: 'your file type is invalid', - }), - errorFilesTooBig: intl.formatMessage({ - id: 'fileUpload.errorFilesTooBig', - defaultMessage: 'your file is too big', - }), - removeMessage: intl.formatMessage({ - id: 'remove', - defaultMessage: 'remove' - }), - })) -)(Upload); diff --git a/lib/modules/index.js b/lib/modules/index.js index 4100eaf..a914b5a 100644 --- a/lib/modules/index.js +++ b/lib/modules/index.js @@ -1,4 +1,2 @@ -import './register'; - export * from './graphql'; export * from './components'; diff --git a/lib/modules/register.js b/lib/modules/register.js deleted file mode 100644 index 215d5b4..0000000 --- a/lib/modules/register.js +++ /dev/null @@ -1,5 +0,0 @@ -import { registerComponent } from 'meteor/vulcan:core'; - -import Upload from './containers/Upload'; - -registerComponent('Upload', Upload); From 4a2ef1203ae6c9127ea25d5d786a63e60972e8d5 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Fri, 12 Jul 2019 15:29:50 +0200 Subject: [PATCH 09/18] working on file deletion --- lib/server/fieldUploadHandlers.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/server/fieldUploadHandlers.js b/lib/server/fieldUploadHandlers.js index 77a6a89..f7c5b2d 100644 --- a/lib/server/fieldUploadHandlers.js +++ b/lib/server/fieldUploadHandlers.js @@ -79,11 +79,16 @@ export const createHandlerMultiple = ( export const updateHandler = (fieldName, FSCollection, getValue) => - async function updateHandler({ data, document, currentUser }) { + async function updateHandler({ data, document, currentUser, oldDocument }) { if (typeof data[fieldName] === 'undefined') return undefined // no change - if (!data[fieldName]) { - // TODO trigger file removal - } else { + // remove the old file in any case since the field is modified (either updated to a new file or delete) + if (oldDocument[fieldName]) { + // TODO: remove previous value + const fileId = oldDocument[fieldName] + FSCollection.remove(fileId) + } + // update new file (if there is a new file) + if (data[fieldName]) { const fieldValue = await data[fieldName]; return fieldValue.createReadStream ? getValue(await addFileFromReadable(FSCollection, fieldValue.createReadStream())) @@ -95,9 +100,10 @@ export const updateHandler = (fieldName, FSCollection, getValue) => export const updateHandlerMultiple = (fieldName, FSCollection, getValue) => async function updateHandler({ data, document, currentUser }) { if (typeof data[fieldName] === 'undefined') return undefined // no change - if (!data[fieldName]) { - // TODO trigger file removal - } else { + if (oldDocument[fieldName]) { + // TODO delete all values + } + if (data[fieldName]) { const fieldValue = data[fieldName]; if (Array.isArray(fieldValue)) { return Promise.all( From aa65972601d633dd83aa272f830f16a2c8cd3a83 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Wed, 17 Jul 2019 18:13:17 +0200 Subject: [PATCH 10/18] can delete files for single upload/creation --- lib/server/fieldUploadHandlers.js | 82 ++++++++++++++++++------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/lib/server/fieldUploadHandlers.js b/lib/server/fieldUploadHandlers.js index f7c5b2d..de7f02e 100644 --- a/lib/server/fieldUploadHandlers.js +++ b/lib/server/fieldUploadHandlers.js @@ -4,6 +4,14 @@ import { Readable } from 'stream'; import { addFileFromReadable } from './helpers'; +const uploadFromField = async (getValue, FSCollection, fieldValue) => { + const file = await fieldValue + if (!file.createReadStream) return file + const addFileResult = await addFileFromReadable(FSCollection, file.createReadStream()) + return getValue(addFileResult) +} + + /** * Retrieves the id of the file document. * @@ -51,10 +59,10 @@ export default function createUploadHandlers(options) { export const createHandler = (fieldName, FSCollection, getValue) => async function createHandler({ newDocument, currentUser }) { const fieldValue = newDocument[fieldName]; - if (fieldValue instanceof Readable) { - return getValue(await addFileFromReadable(FSCollection, fieldValue)); + if (fieldValue) { + return uploadFromField(getValue, FSCollection, fieldValue) } - return fieldValue; + return undefined }; export const createHandlerMultiple = ( @@ -63,57 +71,63 @@ export const createHandlerMultiple = ( getValue, ) => async function createHandlerMultiple({ newDocument, currentUser }) { - const fieldValue = newDocument[fieldName]; + const fieldValues = newDocument[fieldName]; if (Array.isArray(fieldValue)) { - return Promise.all( - fieldValue.map(async value => { - if (value instanceof Readable) { - return getValue(await addFileFromReadable(FSCollection, value)); - } - return Promise.resolve(value); - }), - ); + return uploadFromFields(getValue, FSCollection, fieldValues) } - return fieldValue; + return fieldValues; }; + +// if the user want to upload a file, the field will contain a promise +const hasUploadedFile = fieldValue => fieldValue && fieldValue.then +const isEmpty = fieldValue => fieldValue === null + export const updateHandler = (fieldName, FSCollection, getValue) => async function updateHandler({ data, document, currentUser, oldDocument }) { - if (typeof data[fieldName] === 'undefined') return undefined // no change - // remove the old file in any case since the field is modified (either updated to a new file or delete) + const fieldValue = data[fieldName] + // null => file has been deleted or never existed in the first place + // fieldValue.then is defined => field is a Promise, a new file has been added + if (!(isEmpty(fieldValue) || hasUploadedFile(fieldValue))) return undefined // no change or deletion + // remove the old file (if appliable) whenever the field is modified (either updated to a new file or deleted) if (oldDocument[fieldName]) { // TODO: remove previous value const fileId = oldDocument[fieldName] - FSCollection.remove(fileId) + await FSCollection.remove(fileId) } // update new file (if there is a new file) - if (data[fieldName]) { - const fieldValue = await data[fieldName]; - return fieldValue.createReadStream - ? getValue(await addFileFromReadable(FSCollection, fieldValue.createReadStream())) - : fieldValue; + if (fieldValue) { + return await uploadFromField(getValue, FSCollection, fieldValue) } return undefined; }; +const uploadFromFields = async (getValue, FSCollection, fieldValues) => { + return Promise.all( + fieldValues + .filter(fieldValue => !!fieldValue) // filter out non defined values + .map(async fieldValue => { + return uploadFromField(getValue, FSCollection, fieldValue) + }) + ); +} export const updateHandlerMultiple = (fieldName, FSCollection, getValue) => + // TODO: not tested, not sure about the structure of "fieldValues" and how to detect deletion + // Implementation considers that if one file of the list is modified, then + // fieldValues will list all files async function updateHandler({ data, document, currentUser }) { - if (typeof data[fieldName] === 'undefined') return undefined // no change + const fieldValues = data[fieldName] + if (!(isEmpty(fieldValues) || _any(fieldValues, fieldValue => hasUploadedFile(fieldValue)))) return undefined // no change or deletion + // remove old files if (oldDocument[fieldName]) { - // TODO delete all values + const oldFileIds = oldDocument[fieldName] + await Promise.all(oldFileIds.map(FSCollection.remove)) } - if (data[fieldName]) { - const fieldValue = data[fieldName]; - if (Array.isArray(fieldValue)) { - return Promise.all( - fieldValue.map(async value => { - if (value instanceof Readable) { - return getValue(await addFileFromReadable(FSCollection, value)); - } - return Promise.resolve(value); - }), - ); + // create files + if (fieldValues) { + if (Array.isArray(fieldValues)) { + return uploadFromFields(getValue, FSCollection, fieldValues) } } return undefined; From 2fe2333d9174a1b00aacdab909652c68adac708d Mon Sep 17 00:00:00 2001 From: eric-burel Date: Wed, 17 Jul 2019 19:47:41 +0200 Subject: [PATCH 11/18] allow to pass down dropZone props --- lib/modules/components/UploadInput.jsx | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/modules/components/UploadInput.jsx b/lib/modules/components/UploadInput.jsx index 4e912b2..c156603 100644 --- a/lib/modules/components/UploadInput.jsx +++ b/lib/modules/components/UploadInput.jsx @@ -56,6 +56,7 @@ const UploadInput = props => { onDrop, preview, clearFile, + dropZoneProps, Components } = props; @@ -88,6 +89,7 @@ const UploadInput = props => { color: "darkslategrey", cursor: "pointer" }} + {...dropZoneProps} > { /> )) ) : ( - - )} + + )} ) : null} @@ -143,7 +145,9 @@ UploadInput.propTypes = { clearFile: PropTypes.func.isRequired, uploading: PropTypes.bool, - errorMessage: PropTypes.string + errorMessage: PropTypes.string, + + dropZoneProps: PropTypes.object, // additionnal props passed to react-dropzone }; /* @@ -281,9 +285,12 @@ class UploadInputContainer extends PureComponent { render() { const { FileRender, - selectOrDropFilesMessage = "Drop a file here, or click to select an file to upload.", - uploadingMessage = "Uploading…", - Components + selectOrDropFilesMessage, + uploadingMessage, + removeMessage, + dropZoneProps = {}, + label, + Components, } = this.props; const { uploading, errorMessage } = this.state; const { UploadInputInner } = Components; @@ -299,7 +306,10 @@ class UploadInputContainer extends PureComponent { onDrop={this.onDrop} preview={this.preview} errorMessage={errorMessage} + removeMessage={removeMessage} clearFile={this.clearFile} + dropZoneProps={dropZoneProps} + label={label} /> ); } @@ -327,7 +337,7 @@ const WrappedUploadInputContainer = compose( defaultMessage: "your file is too big" }), removeMessage: intl.formatMessage({ - id: "remove", + id: "fileUpload.remove", defaultMessage: "remove" }) })), From 4391bfbe90f0a0a0d3fc8fa9640d7e2f2589a0f6 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Wed, 17 Jul 2019 21:58:25 +0200 Subject: [PATCH 12/18] lighter colors as a default --- lib/modules/components/UploadInput.jsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/modules/components/UploadInput.jsx b/lib/modules/components/UploadInput.jsx index c156603..d86de75 100644 --- a/lib/modules/components/UploadInput.jsx +++ b/lib/modules/components/UploadInput.jsx @@ -20,6 +20,7 @@ import { injectIntl } from "react-intl"; import { compose } from "recompose"; // Visual components +const UploadInputLayout = ({ children }) => (
{children}
) const UploadInputLabel = ({ label }) => (
{upperFirst(label)}
); @@ -63,11 +64,12 @@ const UploadInput = props => { const { UploadInputLabel, UploadInputDropZoneContent, - UploadInputErrorMessage + UploadInputErrorMessage, + UploadInputLayout } = Components; return ( -
+ {label ? : null}
@@ -82,7 +84,10 @@ const UploadInput = props => { rejectClassName="dropzone-reject" style={{ minHeight: "100px", - backgroundColor: "lightgrey", + border: "2px dashed grey", + marginTop: "4px", + marginBottom: "4px", + backgroundColor: "#e1e1e1", display: "flex", justifyContent: "center", alignItems: "center", @@ -128,7 +133,7 @@ const UploadInput = props => { ) : null}
-
+ ); }; UploadInput.propTypes = { @@ -348,6 +353,7 @@ const WrappedUploadInputContainer = compose( )(UploadInputContainer); // registeration +registerComponent({ name: "UploadInputLayout", component: UploadInputLayout }); registerComponent({ name: "UploadInputLabel", component: UploadInputLabel }); registerComponent({ name: "UploadInputDropZoneContent", From 5c121b2fb7ccca1be58208d95fee4f6353d68fc8 Mon Sep 17 00:00:00 2001 From: eric-burel Date: Wed, 17 Jul 2019 22:02:28 +0200 Subject: [PATCH 13/18] update npm dependencies in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d89f7f8..494ad10 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ In your project's root folder run: To avoid using `Npm.depends` this package does not include any NPM module, so you will have to install them yourself by running the following command: ``` -meteor npm install async-busboy@^0.6.2 brackets2dots@^1.1.0 lodash@^4.0.0 object-path@^0.11.4 randomstring@^1.1.5 react-dropzone@^3.12.2 recursive-iterator@^3.3.0 knox@^0.9.2 gm@^1.23.1 +meteor npm install lodash@^4.0.0 randomstring@^1.1.5 react-dropzone@^3.12.2 recursive-iterator@^3.3.0 apollo-upload-client@^11.0.0 ``` Alternatively, here you have a list of the packages and versions required, so you can add them to your project's `package.json`: From ad6394b04eaec8d53932148ff999eb1b46bf62fa Mon Sep 17 00:00:00 2001 From: eric-burel Date: Thu, 18 Jul 2019 16:13:56 +0200 Subject: [PATCH 14/18] fix packages list --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 494ad10..ad3489c 100644 --- a/README.md +++ b/README.md @@ -18,22 +18,19 @@ In your project's root folder run: To avoid using `Npm.depends` this package does not include any NPM module, so you will have to install them yourself by running the following command: ``` -meteor npm install lodash@^4.0.0 randomstring@^1.1.5 react-dropzone@^3.12.2 recursive-iterator@^3.3.0 apollo-upload-client@^11.0.0 +meteor npm install gm@^1.23.1 lodash@^4.0.0 randomstring@^1.1.5 react-dropzone@^3.12.2 recursive-iterator@^3.3.0 apollo-upload-client@^11.0.0 ``` Alternatively, here you have a list of the packages and versions required, so you can add them to your project's `package.json`: ```json { - "async-busboy": "^0.6.2", - "brackets2dots": "^1.1.0", "gm": "^1.23.1", - "knox": "^0.9.2", "lodash": "^4.0.0", - "object-path": "^0.11.4", "randomstring": "^1.1.5", "react-dropzone": "^3.12.2", - "recursive-iterator": "^3.3.0" + "recursive-iterator": "^3.3.0", + "apollo-upload-client": "^11.0.0" } ``` From bc1c8513f67c71e63c6774a69e1fcda36fbbd957 Mon Sep 17 00:00:00 2001 From: Eloy Date: Thu, 24 Oct 2019 09:54:48 +0200 Subject: [PATCH 15/18] update vulcan core version --- package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.js b/package.js index 5a1b324..f608bc0 100644 --- a/package.js +++ b/package.js @@ -11,7 +11,7 @@ Package.onUse(api => { api.use([ 'ecmascript', - 'vulcan:core@1.11.0', + 'vulcan:core@1.13.4', 'ostrio:files@1.10.1', 'origenstudio:files-helpers@1.0.0-alpha.1', ]); From 8ecb564a4f778352e4560d4b2ec64c4768401c6e Mon Sep 17 00:00:00 2001 From: juliensl Date: Thu, 24 Oct 2019 10:53:35 +0200 Subject: [PATCH 16/18] change package version --- package.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/package.js b/package.js index 5a1b324..e594901 100644 --- a/package.js +++ b/package.js @@ -1,21 +1,22 @@ Package.describe({ - name: 'origenstudio:vulcan-files', - version: '1.0.0-alpha.1', - summary: 'Provides Vulcan with the capability of uploading files to server using Meteor-Files', - git: 'https://github.com/OrigenStudio/vulcan-files', - documentation: 'README.md' + name: "origenstudio:vulcan-files", + version: "1.0.0-alpha.1", + summary: + "Provides Vulcan with the capability of uploading files to server using Meteor-Files", + git: "https://github.com/OrigenStudio/vulcan-files", + documentation: "README.md", }); Package.onUse(api => { - api.versionsFrom('1.6.1'); + api.versionsFrom("1.6.1"); api.use([ - 'ecmascript', - 'vulcan:core@1.11.0', - 'ostrio:files@1.10.1', - 'origenstudio:files-helpers@1.0.0-alpha.1', + "ecmascript", + "vulcan:core@1.13.4", + "ostrio:files@1.10.1", + "origenstudio:files-helpers@1.0.0-alpha.1", ]); - api.mainModule('lib/server/main.js', 'server'); - api.mainModule('lib/client/main.js', 'client'); + api.mainModule("lib/server/main.js", "server"); + api.mainModule("lib/client/main.js", "client"); }); From c385e055656608eb0470baab4ace71830546196a Mon Sep 17 00:00:00 2001 From: juliensl Date: Mon, 2 Mar 2020 18:24:32 +0100 Subject: [PATCH 17/18] bugfix client graphQL call --- lib/modules/components/UploadInput.jsx | 67 +++++++++++++------------- lib/modules/graphql/types.js | 5 +- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/lib/modules/components/UploadInput.jsx b/lib/modules/components/UploadInput.jsx index d86de75..bf20df2 100644 --- a/lib/modules/components/UploadInput.jsx +++ b/lib/modules/components/UploadInput.jsx @@ -20,7 +20,7 @@ import { injectIntl } from "react-intl"; import { compose } from "recompose"; // Visual components -const UploadInputLayout = ({ children }) => (
{children}
) +const UploadInputLayout = ({ children }) =>
{children}
; const UploadInputLabel = ({ label }) => (
{upperFirst(label)}
); @@ -35,7 +35,7 @@ const UploadInputErrorMessage = ({ errrorMessage }) => ( style={{ display: "flex", justifyContent: "center", - color: "red" + color: "red", }} > {errorMessage} @@ -58,14 +58,14 @@ const UploadInput = props => { preview, clearFile, dropZoneProps, - Components + Components, } = props; const { UploadInputLabel, UploadInputDropZoneContent, UploadInputErrorMessage, - UploadInputLayout + UploadInputLayout, } = Components; return ( @@ -80,6 +80,7 @@ const UploadInput = props => { onDrop={onDrop} // accept="image/*" // TODO also add this filtering className="dropzone-base" + data-cy="dropzone" activeClassName="dropzone-active" rejectClassName="dropzone-reject" style={{ @@ -92,7 +93,7 @@ const UploadInput = props => { justifyContent: "center", alignItems: "center", color: "darkslategrey", - cursor: "pointer" + cursor: "pointer", }} {...dropZoneProps} > @@ -118,13 +119,13 @@ const UploadInput = props => { /> )) ) : ( - - )} + + )} ) : null} @@ -160,7 +161,7 @@ Remove the nth item from an array */ const removeNthItem = (array, n) => [ ..._.first(array, n), - ..._.rest(array, n + 1) + ..._.rest(array, n + 1), ]; // Container with logic @@ -175,7 +176,7 @@ class UploadInputContainer extends PureComponent { fileCheck: PropTypes.func, FileRender: PropTypes.func.isRequired, previewFromValue: PropTypes.func, - previewFromFile: PropTypes.func + previewFromFile: PropTypes.func, }; static defaultProps = { @@ -183,8 +184,8 @@ class UploadInputContainer extends PureComponent { previewFromValue: () => "", previewFromFile: value => ({ name: get(value, "name", ""), - url: get(value, "preview", "") - }) + url: get(value, "preview", ""), + }), }; constructor(props, context) { @@ -192,7 +193,7 @@ class UploadInputContainer extends PureComponent { this.state = { uploading: false, - errorMessage: null + errorMessage: null, }; } @@ -202,7 +203,7 @@ class UploadInputContainer extends PureComponent { onDrop = files => { // Reset error state this.setState({ - errorMessage: null + errorMessage: null, }); // Check that files are valid @@ -216,14 +217,14 @@ class UploadInputContainer extends PureComponent { this.props.updateCurrentValues({ [this.props.name]: this.enableMultiple() ? [...this.getValue(), ...files] - : files[0] + : files[0], }); } else { // TODO better error handling // Set error message const { errorFilesTooBig = "your file is too big", - errorFilesNotAllowedType = "your file type is invalid" + errorFilesNotAllowedType = "your file type is invalid", } = this.props; this.setState({ @@ -242,7 +243,7 @@ class UploadInputContainer extends PureComponent { }, "" ) - ) // TODO translate + ), // TODO translate }); } }; @@ -276,7 +277,7 @@ class UploadInputContainer extends PureComponent { this.props.updateCurrentValues({ [this.props.name]: this.enableMultiple() ? removeNthItem(this.props.value, index) - : null + : null, }); }; @@ -326,29 +327,29 @@ const WrappedUploadInputContainer = compose( withProps(({ intl }) => ({ selectOrDropFilesMessage: intl.formatMessage({ id: "fileUpload.selectOrDropFilesMessage", - defaultMessage: "Drop a file here, or click to select an file to upload." + defaultMessage: "Drop a file here, or click to select an file to upload.", }), uploadingMessage: intl.formatMessage({ id: "fileUpload.uploadingMessage", - defaultMessage: "Uploading..." + defaultMessage: "Uploading...", }), errorFilesNotAllowedType: intl.formatMessage({ id: "fileUpload.errorFilesNotAllowedType", - defaultMessage: "your file type is invalid" + defaultMessage: "your file type is invalid", }), errorFilesTooBig: intl.formatMessage({ id: "fileUpload.errorFilesTooBig", - defaultMessage: "your file is too big" + defaultMessage: "your file is too big", }), removeMessage: intl.formatMessage({ id: "fileUpload.remove", - defaultMessage: "remove" - }) + defaultMessage: "remove", + }), })), getContext({ updateCurrentValues: PropTypes.func, - getDocument: PropTypes.func + getDocument: PropTypes.func, }) )(UploadInputContainer); @@ -357,19 +358,19 @@ registerComponent({ name: "UploadInputLayout", component: UploadInputLayout }); registerComponent({ name: "UploadInputLabel", component: UploadInputLabel }); registerComponent({ name: "UploadInputDropZoneContent", - component: UploadInputDropZoneContent + component: UploadInputDropZoneContent, }); registerComponent({ name: "UploadInputErrorMessage", - component: UploadInputErrorMessage + component: UploadInputErrorMessage, }); registerComponent({ name: "UploadInputInner", component: UploadInput, - hocs: [withComponents] + hocs: [withComponents], }); export default registerComponent({ name: "UploadInput", component: WrappedUploadInputContainer, - hocs: [withComponents] + hocs: [withComponents], }); diff --git a/lib/modules/graphql/types.js b/lib/modules/graphql/types.js index 81cfa47..bd5611b 100644 --- a/lib/modules/graphql/types.js +++ b/lib/modules/graphql/types.js @@ -1,7 +1,8 @@ import { addGraphQLSchema } from 'meteor/vulcan:core'; export const FILE = 'FSFile'; -addGraphQLSchema(` +if(Meteor.isServer) { + addGraphQLSchema(` type ${FILE} { _id: String name: String! @@ -22,3 +23,5 @@ addGraphQLSchema(` isPDF: Boolean } `); +} + From 9e56594ad3b7a55f67e8bc53cef1ded5c680276e Mon Sep 17 00:00:00 2001 From: eric-burel Date: Wed, 26 Aug 2020 14:44:26 +0200 Subject: [PATCH 18/18] simplify field schema generator --- lib/modules/generateFieldSchemaBase.js | 90 ++++++++++++++------------ lib/server/fieldResolver.js | 28 +++----- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/lib/modules/generateFieldSchemaBase.js b/lib/modules/generateFieldSchemaBase.js index 741b6dd..86229db 100644 --- a/lib/modules/generateFieldSchemaBase.js +++ b/lib/modules/generateFieldSchemaBase.js @@ -1,13 +1,13 @@ -import SimpleSchema from 'simpl-schema'; -import merge from 'lodash/merge'; -import pickBy from 'lodash/pickBy'; -import once from 'lodash/once'; -import get from 'lodash/get'; -import isString from 'lodash/isString'; -import isFinite from 'lodash/isFinite'; +import SimpleSchema from "simpl-schema"; +import merge from "lodash/merge"; +import pickBy from "lodash/pickBy"; +import once from "lodash/once"; +import get from "lodash/get"; +import isString from "lodash/isString"; +import isFinite from "lodash/isFinite"; -import { FILE } from './graphql/types'; -import defaultResolveId from './defaultResolveId'; +import { FILE } from "./graphql/types"; +import defaultResolveId from "./defaultResolveId"; /** * Generates schema for a file field. @@ -42,12 +42,12 @@ import defaultResolveId from './defaultResolveId'; * @return {Object} * @function generateFieldSchemaBase */ -export default (options = {}) => { +const generateFieldSchemaBase = (options = {}) => { const { fieldName, fieldSchema = {}, fieldType = fieldSchema.type || String, - resolverName = get(fieldSchema, 'resolveAs.fieldName'), + resolverName = get(fieldSchema, "resolveAs.fieldName"), multiple = false, resolveId = defaultResolveId, } = options; @@ -56,36 +56,44 @@ export default (options = {}) => { // accept generic object so SimpleSchema does not coerce the file object // to the specified `fieldType` { type: Object, blackbox: true }, - fieldType, + fieldType ); - return pickBy({ - [fieldName]: merge( - { - control: 'Upload', - form: { - previewFromValue: once(() => (value, index, props) => { - if (isString(resolveId(value))) { - const resolvedValuePath = [resolverName]; - if (isFinite(index)) { - resolvedValuePath.push(index); - } - return get(props.document, resolvedValuePath); - } - return undefined; - }), - }, - }, - fieldSchema, - { - type: multiple ? Array : FileOrType, - resolveAs: { - fieldName: resolverName, - type: multiple ? `[${FILE}]` : FILE, - addOriginalField: true, - }, - } - ), - [`${fieldName}.$`]: multiple ? { type: FileOrType } : null, - }); + const formInputFieldSchema = { + control: "Upload", + form: { + previewFromValue: once(() => (value, index, props) => { + if (isString(resolveId(value))) { + const resolvedValuePath = [resolverName]; + if (isFinite(index)) { + resolvedValuePath.push(index); + } + return get(props.document, resolvedValuePath); + } + return undefined; + }), + }, + }; + + const graphqlFieldSchema = { + type: multiple ? Array : FileOrType, + resolveAs: { + fieldName: resolverName, + type: multiple ? `[${FILE}]` : FILE, + addOriginalField: true, + }, + }; + + const enhancedFieldSchema = merge( + formInputFieldSchema, + fieldSchema, + graphqlFieldSchema + ); + const schema = { + [fieldName]: enhancedFieldSchema, + }; + if (multiple) schema[`${fieldName}.$`] = { type: FileOrType }; + return schema; }; + +export default generateFieldSchemaBase; diff --git a/lib/server/fieldResolver.js b/lib/server/fieldResolver.js index 781ce53..cfae58b 100644 --- a/lib/server/fieldResolver.js +++ b/lib/server/fieldResolver.js @@ -1,6 +1,6 @@ -import defaultResolveId from '../modules/defaultResolveId'; -import castArray from 'lodash/castArray'; -import compact from 'lodash/compact'; +import defaultResolveId from "../modules/defaultResolveId"; +import castArray from "lodash/castArray"; +import compact from "lodash/compact"; /** * Creates field's resolver to retrieve the file. @@ -30,12 +30,8 @@ export default function createResolver(options) { : createResolverSingle(fieldName, FSCollection, resolveId); } -function createResolverMultiple( - fieldName, - FSCollection, - resolveId, -) { - return async document => { +function createResolverMultiple(fieldName, FSCollection, resolveId) { + return async (document) => { const fileIds = compact(castArray(document[fieldName])).map(resolveId); if (fileIds.length === 0) return []; @@ -44,18 +40,14 @@ function createResolverMultiple( }; } -function createResolverSingle( - fieldName, - FSCollection, - resolveId, -) { +function createResolverSingle(fieldName, FSCollection, resolveId) { const resolverMultiple = createResolverMultiple( fieldName, FSCollection, - resolveId, + resolveId ); - return async document => { + return async (document) => { const files = await resolverMultiple(document); return files[0] || null; - } -} \ No newline at end of file + }; +}