685 lines
21 KiB
JavaScript
Raw Normal View History

'use strict';
/**
* Module dependencies
*/
// Node.js core
const path = require('path');
2018-05-03 18:13:22 +02:00
// Public node modules.
const _ = require('lodash');
const pluralize = require('pluralize');
2018-05-03 18:13:22 +02:00
// Following this discussion https://stackoverflow.com/questions/18082/validate-decimal-numbers-in-javascript-isnumeric this function is the best implem to determine if a value is a valid number candidate
const isNumeric = (value) => {
return !_.isObject(value) && !isNaN(parseFloat(value)) && isFinite(value);
2018-06-22 16:28:54 +02:00
};
// Constants
const ORDERS = ['ASC', 'DESC'];
2018-05-03 18:13:22 +02:00
/* eslint-disable prefer-template */
/*
* Set of utils for models
*/
module.exports = {
2016-07-05 14:13:35 +02:00
/**
* Initialize to prevent some mistakes
*/
initialize: cb => {
cb();
},
2016-03-17 15:05:47 +01:00
/**
* Find primary key per ORM
*/
getPK: function (collectionIdentity, collection, models) {
2016-03-17 15:05:47 +01:00
if (_.isString(collectionIdentity)) {
const ORM = this.getORM(collectionIdentity);
try {
const GraphQLFunctions = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi-' + ORM, 'lib', 'utils'));
2016-07-06 15:51:52 +02:00
if (!_.isUndefined(GraphQLFunctions)) {
return GraphQLFunctions.getPK(collectionIdentity, collection, models || strapi.models);
}
} catch (err) {
return undefined;
2016-03-17 15:05:47 +01:00
}
}
return undefined;
},
/**
* Retrieve the value based on the primary key
*/
getValuePrimaryKey: (value, defaultKey) => {
return value[defaultKey] || value.id || value._id;
},
2016-03-17 15:05:47 +01:00
/**
* Find primary key per ORM
*/
getCount: function (collectionIdentity) {
2016-03-17 15:05:47 +01:00
if (_.isString(collectionIdentity)) {
const ORM = this.getORM(collectionIdentity);
2016-07-06 15:51:52 +02:00
try {
const ORMFunctions = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi-' + ORM, 'lib', 'utils'));
2016-07-06 15:51:52 +02:00
if (!_.isUndefined(ORMFunctions)) {
return ORMFunctions.getCount(collectionIdentity);
}
} catch (err) {
return undefined;
2016-03-17 15:05:47 +01:00
}
}
return undefined;
},
/**
* Find relation nature with verbose
*/
getNature: (association, key, models, currentModelName) => {
try {
const types = {
current: '',
other: ''
};
2016-04-19 17:29:19 +02:00
if (_.isUndefined(models)) {
models = association.plugin ? strapi.plugins[association.plugin].models : strapi.models;
}
2016-04-19 17:29:19 +02:00
2018-02-22 15:34:33 +01:00
if ((association.hasOwnProperty('collection') && association.collection === '*') || (association.hasOwnProperty('model') && association.model === '*')) {
if (association.model) {
types.current = 'morphToD';
} else {
types.current = 'morphTo';
}
const flattenedPluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => {
Object.keys(strapi.plugins[current].models).forEach((model) => {
acc[`${current}_${model}`] = strapi.plugins[current].models[model];
});
return acc;
}, {});
const allModels = _.merge({}, strapi.models, flattenedPluginsModels);
// We have to find if they are a model linked to this key
_.forIn(allModels, model => {
_.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.model === currentModelName) {
2018-02-22 15:34:33 +01:00
if (attribute.hasOwnProperty('collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model')) {
types.other = 'model';
// Break loop
return false;
}
}
});
});
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('collection')) {
const relatedAttribute = models[association.collection].attributes[association.via];
if (!relatedAttribute) {
throw new Error(`The attribute \`${association.via}\` is missing in the model ${_.upperFirst(association.collection)} ${association.plugin ? '(plugin - ' + association.plugin + ')' : '' }`);
}
types.current = 'collection';
2018-02-22 15:34:33 +01:00
if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && relatedAttribute.hasOwnProperty('via')) {
types.other = 'collection';
2018-02-22 15:34:33 +01:00
} else if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && !relatedAttribute.hasOwnProperty('via')) {
types.other = 'collectionD';
2018-02-22 15:34:33 +01:00
} else if (relatedAttribute.hasOwnProperty('model') && relatedAttribute.model !== '*') {
types.other = 'model';
2018-02-22 15:34:33 +01:00
} else if (relatedAttribute.hasOwnProperty('collection') || relatedAttribute.hasOwnProperty('model')) {
2018-02-09 10:43:09 +01:00
types.other = 'morphTo';
}
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('model')) {
types.current = 'modelD';
// We have to find if they are a model linked to this key
2018-05-16 12:07:02 +02:00
const model = models[association.model];
2018-04-30 17:14:13 +02:00
const attribute = model.attributes[association.via];
2018-02-26 14:39:06 +01:00
2018-04-30 17:14:13 +02:00
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection') && attribute.collection !== '*') {
types.other = 'collection';
} else if (attribute.hasOwnProperty('model') && attribute.model !== '*') {
types.other = 'model';
} else if (attribute.hasOwnProperty('collection') || attribute.hasOwnProperty('model')) {
types.other = 'morphTo';
}
} else if (association.hasOwnProperty('model')) {
types.current = 'model';
// We have to find if they are a model linked to this key
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key) {
if (attribute.hasOwnProperty('collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model')) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
} else if (association.hasOwnProperty('collection')) {
types.current = 'collectionD';
// We have to find if they are a model linked to this key
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key) {
if (attribute.hasOwnProperty('collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model')) {
types.other = 'modelD';
2018-02-12 18:54:34 +01:00
// Break loop
return false;
}
}
});
});
}
2018-02-12 18:54:34 +01:00
if (types.current === 'collection' && types.other === 'morphTo') {
return {
nature: 'manyToManyMorph',
verbose: 'morphMany'
};
} else if (types.current === 'collection' && types.other === 'morphToD') {
2018-02-09 10:43:09 +01:00
return {
2018-02-22 15:34:33 +01:00
nature: 'manyToOneMorph',
verbose: 'morphMany'
2018-02-09 10:43:09 +01:00
};
} else if (types.current === 'modelD' && types.other === 'morphTo') {
return {
nature: 'oneToManyMorph',
verbose: 'morphOne'
};
} else if (types.current === 'modelD' && types.other === 'morphToD') {
2018-02-09 10:43:09 +01:00
return {
2018-02-22 15:34:33 +01:00
nature: 'oneToOneMorph',
verbose: 'morphOne'
2018-02-09 10:43:09 +01:00
};
} else if (types.current === 'morphToD' && types.other === 'collection') {
2018-02-09 10:43:09 +01:00
return {
2018-02-22 15:34:33 +01:00
nature: 'oneMorphToMany',
2018-02-12 18:54:34 +01:00
verbose: 'belongsToMorph'
};
} else if (types.current === 'morphToD' && types.other === 'model') {
2018-02-12 18:54:34 +01:00
return {
2018-02-22 15:34:33 +01:00
nature: 'oneMorphToOne',
verbose: 'belongsToMorph'
};
} else if (types.current === 'morphTo' && (types.other === 'model' || association.hasOwnProperty('model'))) {
return {
nature: 'manyMorphToOne',
verbose: 'belongsToManyMorph'
};
} else if (types.current === 'morphTo' && (types.other === 'collection' || association.hasOwnProperty('collection'))) {
return {
nature: 'manyMorphToMany',
2018-02-12 18:54:34 +01:00
verbose: 'belongsToManyMorph'
2018-02-09 10:43:09 +01:00
};
} else if (types.current === 'modelD' && types.other === 'model') {
return {
nature: 'oneToOne',
verbose: 'belongsTo'
};
} else if (types.current === 'model' && types.other === 'modelD') {
return {
nature: 'oneToOne',
verbose: 'hasOne'
};
} else if ((types.current === 'model' || types.current === 'modelD') && types.other === 'collection') {
return {
nature: 'manyToOne',
verbose: 'belongsTo'
};
} else if (types.current === 'modelD' && types.other === 'collection') {
return {
nature: 'oneToMany',
verbose: 'hasMany'
};
} else if (types.current === 'collection' && types.other === 'model') {
return {
nature: 'oneToMany',
verbose: 'hasMany'
};
} else if (types.current === 'collection' && types.other === 'collection') {
return {
nature: 'manyToMany',
verbose: 'belongsToMany'
};
} else if (types.current === 'collectionD' && types.other === 'collection' || types.current === 'collection' && types.other === 'collectionD') {
return {
nature: 'manyToMany',
verbose: 'belongsToMany'
};
} else if (types.current === 'collectionD' && types.other === '') {
return {
nature: 'manyWay',
verbose: 'belongsToMany'
};
} else if (types.current === 'model' && types.other === '') {
return {
nature: 'oneWay',
verbose: 'belongsTo'
};
}
return undefined;
} catch (e) {
strapi.log.error(`Something went wrong in the model \`${_.upperFirst(currentModelName)}\` with the attribute \`${key}\``);
strapi.log.error(e);
strapi.stop();
}
},
/**
* Return ORM used for this collection.
*/
getORM: collectionIdentity => {
return _.get(strapi.models, collectionIdentity.toLowerCase() + '.orm');
},
/**
* Return table name for a collection many-to-many
*/
getCollectionName: (associationA, associationB) => {
return [associationA, associationB]
.sort((a, b) => a.collection < b.collection ? -1 : 1)
.map(table => _.snakeCase(`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`))
.join('__');
},
/**
* Define associations key to models
*/
defineAssociations: function (model, definition, association, key) {
try {
// Initialize associations object
if (definition.associations === undefined) {
definition.associations = [];
}
// Exclude non-relational attribute
2018-02-22 15:34:33 +01:00
if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model')) {
return undefined;
}
// Get relation nature
2018-02-22 15:34:33 +01:00
let details;
const globalName = association.model || association.collection || '';
const infos = this.getNature(association, key, undefined, model.toLowerCase());
2018-02-22 15:34:33 +01:00
if (globalName !== '*') {
details = association.plugin ?
_.get(strapi.plugins, `${association.plugin}.models.${globalName}.attributes.${association.via}`, {}):
_.get(strapi.models, `${globalName}.attributes.${association.via}`, {});
2018-02-22 15:34:33 +01:00
}
// Build associations object
2018-02-22 15:34:33 +01:00
if (association.hasOwnProperty('collection') && association.collection !== '*') {
const ast = {
alias: key,
type: 'collection',
collection: association.collection,
via: association.via || undefined,
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
dominant: details.dominant !== true,
plugin: association.plugin || undefined,
filter: details.filter,
};
if (infos.nature === 'manyToMany' && !association.plugin && definition.orm === 'bookshelf') {
ast.tableCollectionName = this.getCollectionName(association, details);
}
definition.associations.push(ast);
2018-02-22 15:34:33 +01:00
} else if (association.hasOwnProperty('model') && association.model !== '*') {
definition.associations.push({
alias: key,
type: 'model',
model: association.model,
via: association.via || undefined,
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
dominant: details.dominant !== true,
plugin: association.plugin || undefined,
filter: details.filter,
2018-02-12 18:54:34 +01:00
});
2018-02-22 15:34:33 +01:00
} else if (association.hasOwnProperty('collection') || association.hasOwnProperty('model')) {
const pluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => {
Object.keys(strapi.plugins[current].models).forEach((entity) => {
Object.keys(strapi.plugins[current].models[entity].attributes).forEach((attribute) => {
const attr = strapi.plugins[current].models[entity].attributes[attribute];
if (
(attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() &&
strapi.plugins[current].models[entity].globalId !== definition.globalId
) {
acc.push(strapi.plugins[current].models[entity].globalId);
}
});
});
return acc;
}, []);
const appModels = Object.keys(strapi.models).reduce((acc, entity) => {
Object.keys(strapi.models[entity].attributes).forEach((attribute) => {
const attr = strapi.models[entity].attributes[attribute];
if (
(attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() &&
strapi.models[entity].globalId !== definition.globalId
) {
acc.push(strapi.models[entity].globalId);
}
});
return acc;
}, []);
const models = _.uniq(appModels.concat(pluginsModels));
2018-02-12 18:54:34 +01:00
definition.associations.push({
alias: key,
2018-02-22 15:34:33 +01:00
type: association.model ? 'model' : 'collection',
related: models,
2018-02-12 18:54:34 +01:00
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
2018-02-22 15:34:33 +01:00
filter: association.filter,
});
}
} catch (e) {
strapi.log.error(`Something went wrong in the model \`${_.upperFirst(model)}\` with the attribute \`${key}\``);
strapi.log.error(e);
strapi.stop();
}
2016-08-08 11:12:09 +02:00
},
getVia: (attribute, association) => {
2016-09-28 11:42:26 +02:00
return _.findKey(strapi.models[association.model || association.collection].attributes, {via: attribute});
2017-09-12 17:58:31 +02:00
},
mergeStages: (...stages) => {
return _.unionWith(...stages, _.isEqual);
},
convertParams: function (entity, params) {
const { model, models, convertor, postProcessValue } = this.prepareStage(
entity,
params
);
const _filter = this.splitPrimitiveAndRelationValues(params);
// Execute Steps in the given order
return _.flow([
this.processValues({ model, models, convertor, postProcessValue }),
this.processPredicates({ model, models, convertor }),
this.processGeneratedResults(),
])(_filter);
},
prepareStage: function (entity, params) {
2017-09-13 10:30:37 +02:00
if (!entity) {
throw new Error(
'You can\'t call the convert params method without passing the model\'s name as a first argument.'
);
2017-09-12 17:58:31 +02:00
}
2018-06-04 17:46:08 +02:00
// Remove the source params (that can be sent from the ctm plugin) since it is not a filter
if (params.source) {
delete params.source;
}
const modelName = entity.toLowerCase();
const models = this.getStrapiModels();
const model = models[modelName];
2017-11-20 14:35:24 +01:00
if (!model) {
throw new Error(`The model ${modelName} can't be found.`);
}
2017-09-13 10:30:37 +02:00
if (!model.orm) {
throw new Error(
`Impossible to determine the ORM used for the model ${modelName}.`
);
2017-09-13 10:30:37 +02:00
}
const hook = strapi.hook[model.orm];
const convertor = hook.load().getQueryParams;
const postProcessValue = hook.load().postProcessValue || _.identity;
2017-09-13 10:30:37 +02:00
return {
models,
model,
hook,
convertor,
postProcessValue,
};
},
2017-09-13 10:30:37 +02:00
getStrapiModels: function() {
return {
...strapi.models,
...Object.keys(strapi.plugins).reduce(
(acc, pluginName) => ({
...acc,
..._.get(strapi.plugins[pluginName], 'models', {}),
}),
{}
),
2017-09-12 17:58:31 +02:00
};
},
2017-09-12 17:58:31 +02:00
splitPrimitiveAndRelationValues: function(_query) {
const result = _.reduce(
_query,
(acc, value, key) => {
if (_.startsWith(key, '_')) {
acc[key] = value;
} else if (!_.includes(key, '.')) {
acc.where[key] = value;
} else {
_.set(acc.relations, this.injectRelationInKey(key), value);
}
return acc;
},
{
where: {},
relations: {},
sort: '',
start: 0,
limit: 100,
}
);
return result;
},
2017-09-12 17:58:31 +02:00
injectRelationInKey: function (key) {
const numberOfRelations = key.match(/\./gi).length - 1;
const relationStrings = _.times(numberOfRelations, _.constant('relations'));
return _.chain(key)
.split('.')
.zip(relationStrings)
.flatten()
.compact()
.join('.')
.value();
},
2017-09-12 17:58:31 +02:00
transformFilter: function (filter, iteratee) {
if (!_.isArray(filter) && !_.isPlainObject(filter)) {
return filter;
}
2018-05-31 15:06:20 +02:00
return _.transform(filter, (updatedFilter, value, key) => {
const updatedValue = iteratee(value, key);
updatedFilter[key] = this.transformFilter(updatedValue, iteratee);
return updatedFilter;
});
},
2017-09-12 17:58:31 +02:00
processValues: function ({ model, models, convertor, postProcessValue }) {
return filter => {
let parentModel = model;
return this.transformFilter(filter, (value, key) => {
const field = this.getFieldFromKey(key, parentModel);
if (!field) {
return this.processMeta(value, key, {
field,
client: model.client,
model,
convertor,
});
}
if (field.collection || field.model) {
parentModel = models[field.collection || field.model];
}
return postProcessValue(
this.processValue(value, key, { field, client: model.client, model })
);
});
};
},
2017-09-12 17:58:31 +02:00
getFieldFromKey: function (key, model) {
let field;
// Primary key is a unique case because it doesn't belong to the model's attributes
if (key === model.primaryKey) {
field = {
type: 'ID', // Just in case
};
} else if (model.attributes[key]) {
field = model.attributes[key];
} else {
// Remove the filter keyword at the end
let splitKey = key.split('_').slice(0, -1);
splitKey = splitKey.join('_');
if (model.attributes[splitKey]) {
field = model.attributes[splitKey];
2017-09-12 17:58:31 +02:00
}
}
2017-09-12 17:58:31 +02:00
return field;
},
processValue: function (value, key, { field, client }) {
if (field.type === 'boolean' && client === 'mysql') {
return value === 'true' ? '1' : '0';
}
return value;
},
processMeta: function (value, key, { convertor, model }) {
if (_.includes(['_start', '_limit'], key)) {
return convertor(value, key);
} else if (key === '_sort') {
return this.processSortMeta(value, key, { convertor, model });
}
return value;
},
processSortMeta: function (value, key, { convertor, model }) {
const [attr, order = 'ASC'] = value.split(':');
if (!_.includes(ORDERS, order)) {
throw new Error(
`Unkown order value: "${order}", available values are: ${ORDERS.join(
', '
)}`
);
}
const field = this.getFieldFromKey(attr, model);
if (!field) {
throw new Error(`Unkown field: "${attr}"`);
}
return convertor(order, key, attr);
},
processPredicates: function ({ model, models, convertor }) {
return filter => {
let parentModel = model;
return this.transformFilter(filter, (value, key) => {
const field = this.getFieldFromKey(key, parentModel);
if (!field) {
return value;
}
if (field.collection || field.model) {
parentModel = models[field.collection || field.model];
}
return this.processCriteriaMeta(value, key, { convertor });
});
};
},
processCriteriaMeta: function (value, key, { convertor }) {
let type = '=';
if (key.match(/_{1}(?:ne|lte?|gte?|containss?|in)/)) {
type = key.match(/_{1}(?:ne|lte?|gte?|containss?|in)/)[0];
key = key.replace(type, '');
}
return convertor(value, type, key);
},
2017-09-12 17:58:31 +02:00
processGeneratedResults: function() {
return filter => {
if (!_.isArray(filter) && !_.isPlainObject(filter)) {
return filter;
}
return _.transform(filter, (updatedFilter, value, key) => {
// Only set results for object of shape { value, key }
if (_.has(value, 'value') && _.has(value, 'key')) {
const cleanKey = _.replace(value.key, 'where.', '');
_.set(updatedFilter, cleanKey, this.processGeneratedResults()(value.value));
} else {
updatedFilter[key] = this.processGeneratedResults()(value);
}
return updatedFilter;
});
};
}
};