634 lines
18 KiB
JavaScript
Raw Normal View History

'use strict';
/**
* Module dependencies
*/
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-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();
},
/**
* Retrieve the value based on the primary key
*/
getValuePrimaryKey: (value, defaultKey) => {
return value[defaultKey] || value.id || value._id;
},
/**
* 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)) {
2019-07-05 17:17:39 +02:00
models = association.plugin
? strapi.plugins[association.plugin].models
: strapi.models;
}
2016-04-19 17:29:19 +02:00
if (
2019-07-05 17:17:39 +02:00
(association.hasOwnProperty('collection') &&
association.collection === '*') ||
(association.hasOwnProperty('model') && association.model === '*')
) {
2018-02-22 15:34:33 +01:00
if (association.model) {
types.current = 'morphToD';
} else {
types.current = 'morphTo';
}
2019-07-05 17:17:39 +02:00
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];
});
2018-02-22 15:34:33 +01:00
2019-07-05 17:17:39 +02:00
return acc;
},
{}
);
2018-02-22 15:34:33 +01:00
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 => {
2019-07-05 17:17:39 +02:00
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;
}
}
});
});
2019-07-05 17:17:39 +02:00
} else if (
association.hasOwnProperty('via') &&
association.hasOwnProperty('collection')
) {
const relatedAttribute =
models[association.collection].attributes[association.via];
if (!relatedAttribute) {
throw new Error(
2019-07-05 17:17:39 +02:00
`The attribute \`${
association.via
}\` is missing in the model ${_.upperFirst(
association.collection
)} ${
association.plugin ? '(plugin - ' + association.plugin + ')' : ''
2019-07-05 17:17:39 +02:00
}`
);
}
types.current = 'collection';
if (
relatedAttribute.hasOwnProperty('collection') &&
relatedAttribute.collection !== '*' &&
relatedAttribute.hasOwnProperty('via')
) {
types.other = 'collection';
} else if (
relatedAttribute.hasOwnProperty('collection') &&
relatedAttribute.collection !== '*' &&
!relatedAttribute.hasOwnProperty('via')
) {
types.other = 'collectionD';
2019-07-05 17:17:39 +02:00
} else if (
relatedAttribute.hasOwnProperty('model') &&
relatedAttribute.model !== '*'
) {
types.other = 'model';
2019-07-05 17:17:39 +02:00
} else if (
relatedAttribute.hasOwnProperty('collection') ||
relatedAttribute.hasOwnProperty('model')
) {
2018-02-09 10:43:09 +01:00
types.other = 'morphTo';
}
2019-07-05 17:17:39 +02:00
} 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
if (
attribute.hasOwnProperty('via') &&
attribute.via === key &&
attribute.hasOwnProperty('collection') &&
attribute.collection !== '*'
) {
2018-04-30 17:14:13 +02:00
types.other = 'collection';
2019-07-05 17:17:39 +02:00
} else if (
attribute.hasOwnProperty('model') &&
attribute.model !== '*'
) {
2018-04-30 17:14:13 +02:00
types.other = 'model';
2019-07-05 17:17:39 +02:00
} else if (
attribute.hasOwnProperty('collection') ||
attribute.hasOwnProperty('model')
) {
2018-04-30 17:14:13 +02:00
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) {
2019-07-08 17:34:56 +02:00
if (
attribute.hasOwnProperty('collection') &&
attribute.collection === currentModelName
) {
types.other = 'collection';
// Break loop
return false;
2019-07-08 17:34:56 +02:00
} else if (
attribute.hasOwnProperty('model') &&
attribute.model === currentModelName
) {
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) {
2019-07-08 17:34:56 +02:00
if (
attribute.hasOwnProperty('collection') &&
attribute.collection === currentModelName
) {
types.other = 'collection';
// Break loop
return false;
2019-07-08 17:34:56 +02:00
} else if (
attribute.hasOwnProperty('model') &&
attribute.model === currentModelName
) {
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',
verbose: 'belongsToMorph',
2018-02-12 18:54:34 +01:00
};
} 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',
};
2019-07-05 17:17:39 +02:00
} else if (
types.current === 'morphTo' &&
(types.other === 'model' || association.hasOwnProperty('model'))
) {
return {
nature: 'manyMorphToOne',
verbose: 'belongsToManyMorph',
};
} else if (
types.current === 'morphTo' &&
2019-07-05 17:17:39 +02:00
(types.other === 'collection' ||
association.hasOwnProperty('collection'))
) {
return {
nature: 'manyMorphToMany',
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',
};
2019-07-05 17:17:39 +02:00
} 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',
};
2019-07-05 17:17:39 +02:00
} 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(
2019-07-05 17:17:39 +02:00
`Something went wrong in the model \`${_.upperFirst(
currentModelName
)}\` with the attribute \`${key}\``
);
strapi.log.error(e);
strapi.stop();
}
},
/**
* Return table name for a collection many-to-many
*/
getCollectionName: (associationA, associationB) => {
return [associationA, associationB]
2019-04-19 17:24:56 +02:00
.sort((a, b) => {
if (a.collection === b.collection) {
if (a.dominant) return 1;
else return -1;
}
return a.collection < b.collection ? -1 : 1;
})
.map(table =>
_.snakeCase(
2019-07-05 17:17:39 +02:00
`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`
2019-04-19 17:24:56 +02:00
)
)
.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
2019-07-05 17:17:39 +02:00
if (!_.has(association, 'collection') && !_.has(association, 'model')) {
return;
}
// Get relation nature
2018-02-22 15:34:33 +01:00
let details;
2019-07-05 17:17:39 +02:00
const targetName = association.model || association.collection || '';
const infos = this.getNature(
association,
key,
undefined,
model.toLowerCase()
);
2018-02-22 15:34:33 +01:00
2019-07-05 17:17:39 +02:00
if (targetName !== '*') {
if (association.plugin) {
details = _.get(
strapi.plugins,
[
association.plugin,
'models',
targetName,
'attributes',
association.via,
],
{}
);
} else {
details = _.get(
strapi.models,
[targetName, 'attributes', association.via],
{}
);
}
2018-02-22 15:34:33 +01:00
}
// Build associations object
2019-07-05 17:17:39 +02:00
if (_.has(association, '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' && definition.orm === 'bookshelf') {
2019-07-05 17:17:39 +02:00
ast.tableCollectionName =
_.get(association, 'collectionName') ||
this.getCollectionName(details, association);
}
if (infos.nature === 'manyWay' && definition.orm === 'bookshelf') {
ast.tableCollectionName = `${
definition.collectionName
}__${_.snakeCase(key)}`;
}
definition.associations.push(ast);
2019-07-05 17:17:39 +02:00
return;
}
if (_.has(association, '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
});
2019-07-05 17:17:39 +02:00
return;
}
const pluginsModels = Object.keys(strapi.plugins).reduce(
(acc, current) => {
Object.keys(strapi.plugins[current].models).forEach(entity => {
2019-07-05 17:17:39 +02:00
Object.keys(
strapi.plugins[current].models[entity].attributes
).forEach(attribute => {
const attr =
strapi.plugins[current].models[entity].attributes[attribute];
if (
2019-07-05 17:17:39 +02:00
(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;
2019-07-05 17:17:39 +02:00
},
[]
);
2019-07-05 17:17:39 +02:00
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);
}
});
2019-07-05 17:17:39 +02:00
return acc;
}, []);
const models = _.uniq(appModels.concat(pluginsModels));
definition.associations.push({
alias: key,
type: association.model ? 'model' : 'collection',
related: models,
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
filter: association.filter,
});
} catch (e) {
2019-07-05 17:17:39 +02:00
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
},
convertParams: (entity, params) => {
2017-09-13 10:30:37 +02:00
if (!entity) {
2019-07-05 17:17:39 +02:00
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 model = entity.toLowerCase();
2017-11-20 14:35:24 +01:00
const models = _.assign(
_.clone(strapi.models),
_.clone(strapi.admin.models),
Object.keys(strapi.plugins).reduce((acc, current) => {
_.assign(acc, _.get(strapi.plugins[current], ['models'], {}));
return acc;
2019-07-05 17:17:39 +02:00
}, {})
);
2017-09-13 10:30:37 +02:00
if (!models.hasOwnProperty(model)) {
return this.log.error(`The model ${model} can't be found.`);
2017-09-13 10:30:37 +02:00
}
const client = models[model].client;
const connector = models[model].orm;
2017-09-13 10:30:37 +02:00
if (!connector) {
2019-07-05 17:17:39 +02:00
throw new Error(
`Impossible to determine the ORM used for the model ${model}.`
);
}
2017-09-13 10:30:37 +02:00
const convertor = strapi.hook[connector].load().getQueryParams;
const convertParams = {
where: {},
sort: '',
start: 0,
limit: 100,
2017-09-12 17:58:31 +02:00
};
_.forEach(params, (value, key) => {
let result;
let formattedValue;
let modelAttributes = models[model]['attributes'];
let fieldType;
// Get the field type to later check if it's a string before number conversion
if (modelAttributes[key]) {
fieldType = modelAttributes[key]['type'];
} else {
// Remove the filter keyword at the end
let splitKey = key.split('_').slice(0, -1);
splitKey = splitKey.join('_');
if (modelAttributes[splitKey]) {
fieldType = modelAttributes[splitKey]['type'];
}
}
// Check if the value is a valid candidate to be converted to a number value
if (fieldType !== 'string') {
formattedValue = isNumeric(value) ? _.toNumber(value) : value;
} else {
formattedValue = value;
2017-09-12 17:58:31 +02:00
}
2018-12-06 19:12:31 +01:00
if (_.includes(['_start', '_limit', '_populate'], key)) {
result = convertor(formattedValue, key);
} else if (key === '_sort') {
const [attr, order = 'ASC'] = formattedValue.split(':');
result = convertor(order, key, attr);
} else {
const suffix = key.split('_');
// Mysql stores boolean as 1 or 0
2019-07-05 17:17:39 +02:00
if (
client === 'mysql' &&
_.get(models, [model, 'attributes', suffix, 'type']) === 'boolean'
) {
2019-01-24 11:56:21 +01:00
formattedValue = value.toString() === 'true' ? '1' : '0';
2017-09-12 17:58:31 +02:00
}
let type;
2017-09-12 17:58:31 +02:00
2019-07-05 17:17:39 +02:00
if (
_.includes(
[
'ne',
'lt',
'gt',
'lte',
'gte',
'contains',
'containss',
'in',
'nin',
],
_.last(suffix)
)
) {
type = `_${_.last(suffix)}`;
key = _.dropRight(suffix).join('_');
} else {
type = '=';
}
result = convertor(formattedValue, type, key);
}
2017-09-12 17:58:31 +02:00
_.set(convertParams, result.key, result.value);
});
return convertParams;
},
};