2017-04-15 12:02:16 +09:00
|
|
|
import { Mongo } from 'meteor/mongo';
|
2017-03-16 01:25:08 +08:00
|
|
|
import SimpleSchema from 'simpl-schema';
|
2018-06-28 21:35:44 +02:00
|
|
|
import { addGraphQLCollection, addToGraphQLContext } from './graphql.js';
|
2016-12-12 11:34:28 +09:00
|
|
|
import { Utils } from './utils.js';
|
2018-07-22 09:26:41 +02:00
|
|
|
import { runCallbacks, runCallbacksAsync } from './callbacks.js';
|
2017-09-22 12:24:15 +02:00
|
|
|
import { getSetting, registerSetting } from './settings.js';
|
2017-08-02 15:51:11 +09:00
|
|
|
import { registerFragment, getDefaultFragmentText } from './fragments.js';
|
|
|
|
import escapeStringRegexp from 'escape-string-regexp';
|
2018-08-21 17:47:57 +09:00
|
|
|
import { validateIntlField, getIntlString, isIntlField } from './intl';
|
2018-06-04 11:22:49 +09:00
|
|
|
|
2018-10-19 15:53:33 +02:00
|
|
|
const wrapAsync = Meteor.wrapAsync ? Meteor.wrapAsync : Meteor._wrapAsync;
|
2018-01-25 15:03:03 -06:00
|
|
|
// import { debug } from './debug.js';
|
2016-08-08 11:18:21 +09:00
|
|
|
|
2017-09-22 12:24:15 +02:00
|
|
|
registerSetting('maxDocumentsPerRequest', 1000, 'Maximum documents per request');
|
|
|
|
|
2018-06-14 08:47:58 +09:00
|
|
|
// will be set to `true` if there is one or more intl schema fields
|
|
|
|
export let hasIntlFields = false;
|
|
|
|
|
2017-04-15 12:02:16 +09:00
|
|
|
export const Collections = [];
|
|
|
|
|
2018-10-19 15:53:33 +02:00
|
|
|
export const getCollection = name =>
|
|
|
|
Collections.find(
|
|
|
|
({ options: { collectionName } }) => name === collectionName || name === collectionName.toLowerCase()
|
|
|
|
);
|
2018-01-26 16:41:30 -06:00
|
|
|
|
2018-08-10 11:24:50 +02:00
|
|
|
// TODO: find more reliable way to get collection name from type name?
|
2018-07-04 11:11:46 +02:00
|
|
|
export const getCollectionName = typeName => Utils.pluralize(typeName);
|
2018-06-15 10:18:20 +09:00
|
|
|
|
2018-08-10 11:24:50 +02:00
|
|
|
// TODO: find more reliable way to get type name from collection name?
|
2018-10-19 15:53:33 +02:00
|
|
|
export const getTypeName = collectionName => collectionName.slice(0, -1);
|
2018-06-17 07:49:33 +09:00
|
|
|
|
2015-05-10 13:37:42 +09:00
|
|
|
/**
|
2017-05-14 11:08:07 +09:00
|
|
|
* @summary replacement for Collection2's attachSchema. Pass either a schema, to
|
|
|
|
* initialize or replace the schema, or some fields, to extend the current schema
|
2015-05-10 13:37:42 +09:00
|
|
|
* @class Mongo.Collection
|
|
|
|
*/
|
2018-10-19 15:53:33 +02:00
|
|
|
Mongo.Collection.prototype.attachSchema = function(schemaOrFields) {
|
2017-04-03 16:24:19 +09:00
|
|
|
if (schemaOrFields instanceof SimpleSchema) {
|
|
|
|
this.simpleSchema = () => schemaOrFields;
|
|
|
|
} else {
|
2018-10-19 15:53:33 +02:00
|
|
|
this.simpleSchema().extend(schemaOrFields);
|
2017-04-03 16:24:19 +09:00
|
|
|
}
|
2018-09-10 06:16:00 -04:00
|
|
|
};
|
2015-05-10 13:37:42 +09:00
|
|
|
|
2015-04-24 09:28:50 +09:00
|
|
|
/**
|
2017-03-31 11:40:29 +09:00
|
|
|
* @summary Add an additional field (or an array of fields) to a schema.
|
2015-05-18 18:32:54 +09:00
|
|
|
* @param {Object|Object[]} field
|
2015-04-24 09:28:50 +09:00
|
|
|
*/
|
2018-10-19 15:53:33 +02:00
|
|
|
Mongo.Collection.prototype.addField = function(fieldOrFieldArray) {
|
2017-01-21 16:56:54 +09:00
|
|
|
const collection = this;
|
|
|
|
const schema = collection.simpleSchema()._schema;
|
|
|
|
const fieldSchema = {};
|
2015-05-01 18:22:00 +02:00
|
|
|
|
2017-01-21 16:56:54 +09:00
|
|
|
const fieldArray = Array.isArray(fieldOrFieldArray) ? fieldOrFieldArray : [fieldOrFieldArray];
|
2015-05-18 18:32:54 +09:00
|
|
|
|
2017-01-21 16:56:54 +09:00
|
|
|
// loop over fields and add them to schema (or extend existing fields)
|
2018-10-19 15:53:33 +02:00
|
|
|
fieldArray.forEach(function(field) {
|
2018-09-26 17:31:52 +02:00
|
|
|
fieldSchema[field.fieldName] = field.fieldSchema;
|
2015-05-18 18:32:54 +09:00
|
|
|
});
|
2015-04-24 09:28:50 +09:00
|
|
|
|
|
|
|
// add field schema to collection schema
|
|
|
|
collection.attachSchema(fieldSchema);
|
2015-05-01 18:22:00 +02:00
|
|
|
};
|
2015-04-27 09:55:29 +09:00
|
|
|
|
2015-04-27 10:30:47 +09:00
|
|
|
/**
|
2016-04-09 09:41:20 +09:00
|
|
|
* @summary Remove a field from a schema.
|
2015-04-27 10:30:47 +09:00
|
|
|
* @param {String} fieldName
|
|
|
|
*/
|
2018-10-19 15:53:33 +02:00
|
|
|
Mongo.Collection.prototype.removeField = function(fieldName) {
|
2015-04-27 10:30:47 +09:00
|
|
|
var collection = this;
|
|
|
|
var schema = _.omit(collection.simpleSchema()._schema, fieldName);
|
|
|
|
|
|
|
|
// add field schema to collection schema
|
2017-05-14 11:08:07 +09:00
|
|
|
collection.attachSchema(new SimpleSchema(schema));
|
2015-05-01 18:22:00 +02:00
|
|
|
};
|
2015-04-27 10:30:47 +09:00
|
|
|
|
2015-04-27 09:55:29 +09:00
|
|
|
/**
|
2017-03-12 10:25:29 +09:00
|
|
|
* @summary Add a default view function.
|
|
|
|
* @param {Function} view
|
2015-04-27 09:55:29 +09:00
|
|
|
*/
|
2018-10-19 15:53:33 +02:00
|
|
|
Mongo.Collection.prototype.addDefaultView = function(view) {
|
2017-03-05 10:33:34 +00:00
|
|
|
this.defaultView = view;
|
|
|
|
};
|
|
|
|
|
2017-03-12 10:25:29 +09:00
|
|
|
/**
|
|
|
|
* @summary Add a named view function.
|
|
|
|
* @param {String} viewName
|
|
|
|
* @param {Function} view
|
|
|
|
*/
|
2018-10-19 15:53:33 +02:00
|
|
|
Mongo.Collection.prototype.addView = function(viewName, view) {
|
2017-03-05 10:33:34 +00:00
|
|
|
this.views[viewName] = view;
|
|
|
|
};
|
|
|
|
|
2018-04-21 11:40:56 +02:00
|
|
|
/**
|
|
|
|
* @summary Allow mongodb aggregation
|
|
|
|
* @param {Array} pipelines mongodb pipeline
|
2018-10-19 15:53:33 +02:00
|
|
|
* @param {Object} options mongodb option object
|
2018-04-21 11:40:56 +02:00
|
|
|
*/
|
2018-10-19 15:53:33 +02:00
|
|
|
Mongo.Collection.prototype.aggregate = function(pipelines, options) {
|
2018-04-21 11:40:56 +02:00
|
|
|
var coll = this.rawCollection();
|
|
|
|
return wrapAsync(coll.aggregate.bind(coll))(pipelines, options);
|
|
|
|
};
|
|
|
|
|
2017-03-18 15:59:31 +09:00
|
|
|
// see https://github.com/dburles/meteor-collection-helpers/blob/master/collection-helpers.js
|
|
|
|
Mongo.Collection.prototype.helpers = function(helpers) {
|
|
|
|
var self = this;
|
|
|
|
|
2018-06-17 13:24:09 +09:00
|
|
|
if (self._transform && !self._helpers)
|
2018-10-19 15:53:33 +02:00
|
|
|
throw new Meteor.Error('Can\'t apply helpers to \'' + self._name + '\' a transform function already exists!');
|
2017-03-18 15:59:31 +09:00
|
|
|
|
2018-06-17 13:24:09 +09:00
|
|
|
if (!self._helpers) {
|
2018-10-19 15:53:33 +02:00
|
|
|
self._helpers = function Document(doc) {
|
|
|
|
return _.extend(this, doc);
|
|
|
|
};
|
2017-03-18 15:59:31 +09:00
|
|
|
self._transform = function(doc) {
|
|
|
|
return new self._helpers(doc);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_.each(helpers, function(helper, key) {
|
|
|
|
self._helpers.prototype[key] = helper;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-12-12 10:24:34 +09:00
|
|
|
export const createCollection = options => {
|
2018-10-19 15:53:33 +02:00
|
|
|
const {
|
|
|
|
typeName,
|
|
|
|
collectionName = getCollectionName(typeName),
|
|
|
|
schema,
|
|
|
|
generateGraphQLSchema = true,
|
|
|
|
dbCollectionName
|
|
|
|
} = options;
|
2017-03-12 10:25:29 +09:00
|
|
|
|
2016-11-21 16:18:08 +09:00
|
|
|
// initialize new Mongo collection
|
2018-10-19 15:53:33 +02:00
|
|
|
const collection =
|
|
|
|
collectionName === 'Users' && Meteor.users
|
|
|
|
? Meteor.users
|
|
|
|
: new Mongo.Collection(dbCollectionName ? dbCollectionName : collectionName.toLowerCase());
|
|
|
|
|
2016-11-21 16:18:08 +09:00
|
|
|
// decorate collection with options
|
|
|
|
collection.options = options;
|
|
|
|
|
2018-08-29 21:47:23 +09:00
|
|
|
// add typeName if missing
|
2017-03-12 10:25:29 +09:00
|
|
|
collection.typeName = typeName;
|
2018-08-29 21:47:23 +09:00
|
|
|
collection.options.typeName = typeName;
|
2018-09-10 06:16:00 -04:00
|
|
|
collection.options.singleResolverName = Utils.camelCaseify(typeName);
|
|
|
|
collection.options.multiResolverName = Utils.camelCaseify(Utils.pluralize(typeName));
|
2016-11-21 16:18:08 +09:00
|
|
|
|
2018-08-29 21:47:23 +09:00
|
|
|
// add collectionName if missing
|
|
|
|
collection.collectionName = collectionName;
|
|
|
|
collection.options.collectionName = collectionName;
|
2018-10-19 15:53:33 +02:00
|
|
|
|
2017-03-05 10:33:34 +00:00
|
|
|
// add views
|
|
|
|
collection.views = [];
|
|
|
|
|
2018-05-21 09:42:08 +09:00
|
|
|
// generate foo_intl fields
|
|
|
|
Object.keys(schema).forEach(fieldName => {
|
|
|
|
const fieldSchema = schema[fieldName];
|
2018-08-21 17:47:57 +09:00
|
|
|
if (isIntlField(fieldSchema)) {
|
2018-06-14 08:47:58 +09:00
|
|
|
// we have at least one intl field
|
|
|
|
hasIntlFields = true;
|
|
|
|
|
2018-08-21 17:47:57 +09:00
|
|
|
// remove `intl` to avoid treating new _intl field as a field to internationalize
|
2018-09-12 11:59:00 +09:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
2018-08-21 17:47:57 +09:00
|
|
|
const { intl, ...propertiesToCopy } = schema[fieldName];
|
|
|
|
|
2018-05-21 09:42:08 +09:00
|
|
|
schema[`${fieldName}_intl`] = {
|
2018-08-21 17:47:57 +09:00
|
|
|
...propertiesToCopy, // copy properties from regular field
|
2018-05-29 18:02:15 +09:00
|
|
|
hidden: true,
|
2018-05-21 09:42:08 +09:00
|
|
|
type: Array,
|
2018-10-19 15:53:33 +02:00
|
|
|
isIntlData: true
|
2018-09-10 06:16:00 -04:00
|
|
|
};
|
2018-08-21 17:47:57 +09:00
|
|
|
|
|
|
|
delete schema[`${fieldName}_intl`].intl;
|
|
|
|
|
2018-05-21 09:42:08 +09:00
|
|
|
schema[`${fieldName}_intl.$`] = {
|
2018-10-19 15:53:33 +02:00
|
|
|
type: getIntlString()
|
2018-09-10 06:16:00 -04:00
|
|
|
};
|
2018-06-28 21:35:44 +02:00
|
|
|
|
2018-08-17 19:02:44 +09:00
|
|
|
// if original field is required, enable custom validation function instead of `optional` property
|
|
|
|
if (!schema[fieldName].optional) {
|
|
|
|
schema[`${fieldName}_intl`].optional = true;
|
|
|
|
schema[`${fieldName}_intl`].custom = validateIntlField;
|
|
|
|
}
|
|
|
|
|
|
|
|
// make original non-intl field optional
|
2018-06-28 21:35:44 +02:00
|
|
|
schema[fieldName].optional = true;
|
2018-05-21 09:42:08 +09:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-03-12 10:25:29 +09:00
|
|
|
if (schema) {
|
2016-11-27 19:12:54 +09:00
|
|
|
// attach schema to collection
|
2017-03-12 10:25:29 +09:00
|
|
|
collection.attachSchema(new SimpleSchema(schema));
|
2016-11-27 19:12:54 +09:00
|
|
|
}
|
2016-11-21 16:18:08 +09:00
|
|
|
|
|
|
|
// add collection to resolver context
|
|
|
|
const context = {};
|
2017-04-25 12:56:48 +09:00
|
|
|
context[collectionName] = collection;
|
2017-07-03 12:32:20 +09:00
|
|
|
addToGraphQLContext(context);
|
2016-11-21 16:18:08 +09:00
|
|
|
|
2018-10-19 15:53:33 +02:00
|
|
|
if (generateGraphQLSchema) {
|
2017-03-12 10:25:29 +09:00
|
|
|
// add collection to list of dynamically generated GraphQL schemas
|
2017-07-03 12:32:20 +09:00
|
|
|
addGraphQLCollection(collection);
|
2016-11-21 16:18:08 +09:00
|
|
|
}
|
2017-03-16 01:25:08 +08:00
|
|
|
|
2018-09-12 11:59:00 +09:00
|
|
|
runCallbacksAsync({ name: '*.collection', properties: { options } });
|
2018-07-22 09:26:41 +02:00
|
|
|
runCallbacksAsync({ name: `${collectionName}.collection`, properties: { options } });
|
|
|
|
|
2017-08-02 15:51:11 +09:00
|
|
|
// ------------------------------------- Default Fragment -------------------------------- //
|
|
|
|
|
2017-09-25 22:08:17 +02:00
|
|
|
const defaultFragment = getDefaultFragmentText(collection);
|
|
|
|
if (defaultFragment) registerFragment(defaultFragment);
|
2017-08-02 15:51:11 +09:00
|
|
|
|
2016-12-18 19:04:11 +09:00
|
|
|
// ------------------------------------- Parameters -------------------------------- //
|
|
|
|
|
2017-09-25 22:08:17 +02:00
|
|
|
collection.getParameters = (terms = {}, apolloClient, context) => {
|
2018-03-08 10:48:35 +09:00
|
|
|
// console.log(terms);
|
2016-12-18 19:04:11 +09:00
|
|
|
|
|
|
|
let parameters = {
|
|
|
|
selector: {},
|
|
|
|
options: {}
|
|
|
|
};
|
|
|
|
|
2017-03-05 10:33:34 +00:00
|
|
|
if (collection.defaultView) {
|
2018-10-25 15:01:31 +02:00
|
|
|
parameters = Utils.deepExtend(true, parameters, collection.defaultView(terms, apolloClient, context));
|
2017-03-05 10:33:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// handle view option
|
|
|
|
if (terms.view && collection.views[terms.view]) {
|
|
|
|
const view = collection.views[terms.view];
|
2017-10-05 10:17:14 +09:00
|
|
|
parameters = Utils.deepExtend(true, parameters, view(terms, apolloClient, context));
|
2017-03-05 10:33:34 +00:00
|
|
|
}
|
|
|
|
|
2016-12-18 19:04:11 +09:00
|
|
|
// iterate over posts.parameters callbacks
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters = runCallbacks(
|
|
|
|
`${typeName.toLowerCase()}.parameters`,
|
|
|
|
parameters,
|
|
|
|
_.clone(terms),
|
|
|
|
apolloClient,
|
|
|
|
context
|
|
|
|
);
|
2018-06-15 10:18:20 +09:00
|
|
|
// OpenCRUD backwards compatibility
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters = runCallbacks(
|
|
|
|
`${collectionName.toLowerCase()}.parameters`,
|
|
|
|
parameters,
|
|
|
|
_.clone(terms),
|
|
|
|
apolloClient,
|
|
|
|
context
|
|
|
|
);
|
2016-12-18 19:04:11 +09:00
|
|
|
|
2017-10-05 10:17:14 +09:00
|
|
|
if (Meteor.isClient) {
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters = runCallbacks(
|
|
|
|
`${typeName.toLowerCase()}.parameters.client`,
|
|
|
|
parameters,
|
|
|
|
_.clone(terms),
|
|
|
|
apolloClient
|
|
|
|
);
|
2018-06-15 10:18:20 +09:00
|
|
|
// OpenCRUD backwards compatibility
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters = runCallbacks(
|
|
|
|
`${collectionName.toLowerCase()}.parameters.client`,
|
|
|
|
parameters,
|
|
|
|
_.clone(terms),
|
|
|
|
apolloClient
|
|
|
|
);
|
2017-10-05 10:17:14 +09:00
|
|
|
}
|
|
|
|
|
2018-02-05 10:45:51 +09:00
|
|
|
// note: check that context exists to avoid calling this from withList during SSR
|
|
|
|
if (Meteor.isServer && context) {
|
2018-06-15 10:18:20 +09:00
|
|
|
parameters = runCallbacks(`${typeName.toLowerCase()}.parameters.server`, parameters, _.clone(terms), context);
|
|
|
|
// OpenCRUD backwards compatibility
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters = runCallbacks(
|
|
|
|
`${collectionName.toLowerCase()}.parameters.server`,
|
|
|
|
parameters,
|
|
|
|
_.clone(terms),
|
|
|
|
context
|
|
|
|
);
|
2017-10-05 10:17:14 +09:00
|
|
|
}
|
|
|
|
|
2018-06-12 15:24:17 +09:00
|
|
|
// sort using terms.orderBy (overwrite defaultView's sort)
|
|
|
|
if (terms.orderBy && !_.isEmpty(terms.orderBy)) {
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters.options.sort = terms.orderBy;
|
2018-06-12 15:24:17 +09:00
|
|
|
}
|
|
|
|
|
2018-01-25 18:12:26 +09:00
|
|
|
// if there is no sort, default to sorting by createdAt descending
|
|
|
|
if (!parameters.options.sort) {
|
|
|
|
parameters.options.sort = { createdAt: -1 };
|
|
|
|
}
|
|
|
|
|
2017-06-20 10:25:34 +09:00
|
|
|
// extend sort to sort posts by _id to break ties, unless there's already an id sort
|
2017-03-18 15:59:31 +09:00
|
|
|
// NOTE: always do this last to avoid overriding another sort
|
2017-06-20 10:25:34 +09:00
|
|
|
if (!(parameters.options.sort && parameters.options.sort._id)) {
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters = Utils.deepExtend(true, parameters, { options: { sort: { _id: -1 } } });
|
2017-06-20 10:25:34 +09:00
|
|
|
}
|
2017-03-18 15:59:31 +09:00
|
|
|
|
2017-04-30 20:32:06 +09:00
|
|
|
// remove any null fields (setting a field to null means it should be deleted)
|
|
|
|
_.keys(parameters.selector).forEach(key => {
|
|
|
|
if (parameters.selector[key] === null) delete parameters.selector[key];
|
|
|
|
});
|
2017-06-20 10:25:34 +09:00
|
|
|
if (parameters.options.sort) {
|
|
|
|
_.keys(parameters.options.sort).forEach(key => {
|
|
|
|
if (parameters.options.sort[key] === null) delete parameters.options.sort[key];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-06-12 15:24:17 +09:00
|
|
|
if (terms.query) {
|
2017-08-02 16:19:15 +09:00
|
|
|
const query = escapeStringRegexp(terms.query);
|
2018-10-19 15:53:33 +02:00
|
|
|
const currentSchema = collection.simpleSchema()._schema;
|
collection.getParameters handles schema extension for searchable fields
Instead of using `schema` passed when creating the collection, `getParameters` should fetch the schema from `collection.simpleSchema()._schema` to include the fields added with `collection.addField`.
I originally found this when making the `groups` field searchable, with:
```
Users.addField([
{
fieldName: 'groups',
fieldSchema: {
type: Array,
searchable: true,
},
},
{
fieldName: 'groups.$',
fieldSchema: {
type: String,
},
},
]);
```
2018-07-23 11:59:22 +02:00
|
|
|
const searchableFieldNames = _.filter(_.keys(currentSchema), fieldName => currentSchema[fieldName].searchable);
|
2018-01-26 18:41:29 +09:00
|
|
|
if (searchableFieldNames.length) {
|
|
|
|
parameters = Utils.deepExtend(true, parameters, {
|
|
|
|
selector: {
|
2018-10-19 15:53:33 +02:00
|
|
|
$or: searchableFieldNames.map(fieldName => ({ [fieldName]: { $regex: query, $options: 'i' } }))
|
2018-01-26 18:41:29 +09:00
|
|
|
}
|
|
|
|
});
|
2018-10-19 15:53:33 +02:00
|
|
|
} else {
|
|
|
|
console.warn(
|
|
|
|
`Warning: terms.query is set but schema ${
|
|
|
|
collection.options.typeName
|
|
|
|
} has no searchable field. Set "searchable: true" for at least one field to enable search.`
|
|
|
|
);
|
2018-01-26 18:41:29 +09:00
|
|
|
}
|
2017-08-02 16:19:15 +09:00
|
|
|
}
|
|
|
|
|
2018-05-23 16:02:36 -04:00
|
|
|
// limit number of items to 1000 by default
|
2017-09-15 10:08:23 +02:00
|
|
|
const maxDocuments = getSetting('maxDocumentsPerRequest', 1000);
|
2018-05-23 16:02:36 -04:00
|
|
|
const limit = terms.limit || parameters.options.limit;
|
2018-10-19 15:53:33 +02:00
|
|
|
parameters.options.limit = !limit || limit < 1 || limit > maxDocuments ? maxDocuments : limit;
|
2017-03-18 15:59:31 +09:00
|
|
|
|
2018-03-08 10:48:35 +09:00
|
|
|
// console.log(parameters);
|
2016-12-18 19:04:11 +09:00
|
|
|
|
|
|
|
return parameters;
|
2018-09-10 06:16:00 -04:00
|
|
|
};
|
2016-12-18 19:04:11 +09:00
|
|
|
|
2017-04-15 12:02:16 +09:00
|
|
|
Collections.push(collection);
|
|
|
|
|
2016-11-21 16:18:08 +09:00
|
|
|
return collection;
|
2018-09-10 06:16:00 -04:00
|
|
|
};
|