diff --git a/README.md b/README.md
index d89f7f8..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 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 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"
}
```
diff --git a/lib/client/interface.js b/lib/client/interface.js
index 40b90f9..5c340f5 100644
--- a/lib/client/interface.js
+++ b/lib/client/interface.js
@@ -68,58 +68,59 @@ function serializeFormData(obj, formDataObj = {}, namespace) {
return formDataObj;
}
-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();
- }
-};
+// 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 function createUploadNetworkInterface(opts) {
const batchedInterface = createBatchingNetworkInterface(opts);
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]);
- });
-});
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..bf20df2
--- /dev/null
+++ b/lib/modules/components/UploadInput.jsx
@@ -0,0 +1,376 @@
+/**
+ * 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 UploadInputLayout = ({ children }) => {children}
;
+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,
+ dropZoneProps,
+ Components,
+ } = props;
+
+ const {
+ UploadInputLabel,
+ UploadInputDropZoneContent,
+ UploadInputErrorMessage,
+ UploadInputLayout,
+ } = 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,
+
+ dropZoneProps: PropTypes.object, // additionnal props passed to react-dropzone
+};
+
+/*
+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,
+ uploadingMessage,
+ removeMessage,
+ dropZoneProps = {},
+ label,
+ 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: "fileUpload.remove",
+ defaultMessage: "remove",
+ }),
+ })),
+ getContext({
+ updateCurrentValues: PropTypes.func,
+ getDocument: PropTypes.func,
+ })
+)(UploadInputContainer);
+
+// registeration
+registerComponent({ name: "UploadInputLayout", component: UploadInputLayout });
+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/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/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
}
`);
+}
+
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);
diff --git a/lib/server/createEditHandler.js b/lib/server/createEditHandler.js
deleted file mode 100644
index e558bee..0000000
--- a/lib/server/createEditHandler.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Readable } from 'stream';
-
-import { addFileFromReadable } from './helpers';
-
-const createEditHandler = (fieldName, FSCollection, getValue) =>
- async function editHandler(modifier, document, currentUser) {
- if (modifier.$set && modifier.$set[fieldName]) {
- const fieldValue = modifier.$set[fieldName];
- return fieldValue instanceof Readable
- ? getValue(await addFileFromReadable(FSCollection, fieldValue))
- : 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 51%
rename from lib/server/createResolver.js
rename to lib/server/fieldResolver.js
index a230ddd..cfae58b 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 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,25 @@ 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;
+ };
+}
diff --git a/lib/server/fieldUploadHandlers.js b/lib/server/fieldUploadHandlers.js
new file mode 100644
index 0000000..de7f02e
--- /dev/null
+++ b/lib/server/fieldUploadHandlers.js
@@ -0,0 +1,134 @@
+/**
+ * Create Vulcan field callbacks for file upload and edition
+ */
+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.
+ *
+ * @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) {
+ return uploadFromField(getValue, FSCollection, fieldValue)
+ }
+ return undefined
+ };
+
+export const createHandlerMultiple = (
+ fieldName,
+ FSCollection,
+ getValue,
+) =>
+ async function createHandlerMultiple({ newDocument, currentUser }) {
+ const fieldValues = newDocument[fieldName];
+ if (Array.isArray(fieldValue)) {
+ return uploadFromFields(getValue, FSCollection, fieldValues)
+ }
+ 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 }) {
+ 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]
+ await FSCollection.remove(fileId)
+ }
+ // update new file (if there is a new file)
+ 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 }) {
+ const fieldValues = data[fieldName]
+ if (!(isEmpty(fieldValues) || _any(fieldValues, fieldValue => hasUploadedFile(fieldValue)))) return undefined // no change or deletion
+ // remove old files
+ if (oldDocument[fieldName]) {
+ const oldFileIds = oldDocument[fieldName]
+ await Promise.all(oldFileIds.map(FSCollection.remove))
+ }
+ // create files
+ if (fieldValues) {
+ if (Array.isArray(fieldValues)) {
+ return uploadFromFields(getValue, FSCollection, fieldValues)
+ }
+ }
+ 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 bfee311..0000000
--- a/lib/server/uploadMiddleware.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { webAppConnectHandlersUse } from 'meteor/vulcan:lib';
-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 {
- if (!isUpload(req)) {
- return next();
- }
- const { files, fields } = await asyncBusboy(req);
-
- // console.log('');
- // console.log('## graphqlUploadMiddleware ##########################################');
-
- files.forEach(file => {
- objectPath.set(fields, brackets2Dots(file.fieldname), file);
- });
- req.body = fields.data;
-
- // console.log('files:', files);
- // console.log('req:', req);
- } catch (e) {
- // console.error('error:', e);
- return next(e);
- }
- return next();
-}
-
-Meteor.startup(() => {
- webAppConnectHandlersUse('graphql-upload-middleware', '/graphql', graphqlUploadMiddleware, {
- order: 1
- });
-});
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..48e341a
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3 @@
+{
+ "lockfileVersion": 1
+}
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");
});