Init manWay relation

This commit is contained in:
Alexandre Bodin 2019-07-05 17:17:39 +02:00
parent 30e8a673f4
commit d7e9509a45
9 changed files with 610 additions and 647 deletions

View File

@ -17,10 +17,6 @@
"content2": { "content2": {
"type": "text" "type": "text"
}, },
"posts": {
"collection": "post",
"via": "articles"
},
"title": { "title": {
"type": "string" "type": "string"
}, },
@ -36,6 +32,12 @@
"repeatable": true, "repeatable": true,
"min": 2, "min": 2,
"max": 3 "max": 3
},
"mainTag": {
"model": "tag"
},
"linkedTags": {
"collection": "tag"
} }
} }
} }

View File

@ -1,18 +1,19 @@
{ {
"connection": "default", "connection": "default",
"collectionName": "", "collectionName": "post",
"info": { "info": {
"name": "post", "name": "post",
"description": "" "description": ""
}, },
"options": { "options": {
"timestamps": true "timestamps": [
"created_at",
"updated_at"
]
}, },
"attributes": { "attributes": {
"articles": { "title": {
"collection": "articles", "type": "string"
"dominant": true,
"via": "posts"
} }
} }
} }

View File

@ -0,0 +1,17 @@
{
"connection": "default",
"collectionName": "tags",
"info": {
"name": "tag",
"description": ""
},
"options": {
"increments": true,
"timestamps": false
},
"attributes": {
"name": {
"type": "string"
}
}
}

View File

@ -1,5 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const { models: utilsModels } = require('strapi-utils'); const { singular } = require('pluralize');
/* global StrapiConfigs */ /* global StrapiConfigs */
module.exports = async ({ module.exports = async ({
@ -309,36 +309,37 @@ module.exports = async ({
} }
// Equilize many to many releations // Equilize many to many releations
const manyRelations = definition.associations.filter(association => { const manyRelations = definition.associations.filter(({ nature }) =>
return association.nature === 'manyToMany'; ['manyToMany', 'manyWay'].includes(nature)
}); );
for (const manyRelation of manyRelations) { for (const manyRelation of manyRelations) {
if (manyRelation && manyRelation.dominant) { const { plugin, collection, via, dominant, alias } = manyRelation;
const collection = manyRelation.plugin
? strapi.plugins[manyRelation.plugin].models[manyRelation.collection] if (dominant) {
: strapi.models[manyRelation.collection]; const targetCollection = plugin
? strapi.plugins[plugin].models[collection]
: strapi.models[collection];
const targetAttr = via
? targetCollection.attributes[via]
: {
attribute: singular(definition.collectionName),
column: definition.primaryKey,
};
const defAttr = definition.attributes[alias];
const attributes = { const attributes = {
[`${collection.attributes[manyRelation.via].attribute}_${ [`${targetAttr.attribute}_${targetAttr.column}`]: {
collection.attributes[manyRelation.via].column type: targetCollection.primaryKeyType,
}`]: {
type: collection.primaryKeyType,
}, },
[`${definition.attributes[manyRelation.alias].attribute}_${ [`${defAttr.attribute}_${defAttr.column}`]: {
definition.attributes[manyRelation.alias].column
}`]: {
type: definition.primaryKeyType, type: definition.primaryKeyType,
}, },
}; };
const table = const table = manyRelation.tableCollectionName;
_.get(manyRelation, 'collectionName') ||
utilsModels.getCollectionName(
collection.attributes[manyRelation.via],
manyRelation
);
await createOrUpdateTable(table, attributes); await createOrUpdateTable(table, attributes);
} }
} }

View File

@ -1,9 +1,8 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const pluralize = require('pluralize'); const { singular } = require('pluralize');
const utilsModels = require('strapi-utils').models; const utilsModels = require('strapi-utils').models;
const utils = require('./utils/');
const relations = require('./relations'); const relations = require('./relations');
const buildDatabaseSchema = require('./buildDatabaseSchema'); const buildDatabaseSchema = require('./buildDatabaseSchema');
const genGroupRelatons = require('./generate-group-relations'); const genGroupRelatons = require('./generate-group-relations');
@ -125,11 +124,9 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
return; return;
} }
const verbose = const { nature, verbose } =
_.get( utilsModels.getNature(details, name, undefined, model.toLowerCase()) ||
utilsModels.getNature(details, name, undefined, model.toLowerCase()), {};
'verbose'
) || '';
// Build associations key // Build associations key
utilsModels.defineAssociations( utilsModels.defineAssociations(
@ -211,26 +208,40 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
break; break;
} }
case 'belongsToMany': { case 'belongsToMany': {
const collection = details.plugin const targetModel = details.plugin
? strapi.plugins[details.plugin].models[details.collection] ? strapi.plugins[details.plugin].models[details.collection]
: strapi.models[details.collection]; : strapi.models[details.collection];
const collectionName = // Force singular foreign key
details.attribute = singular(details.collection);
details.column = targetModel.primaryKey;
let options = [];
if (nature === 'manyWay') {
const joinTableName = `${definition.collectionName}__${_.snakeCase(
name
)}`;
const foreignKey = `${singular(definition.collectionName)}_${
definition.primaryKey
}`;
const otherKey = `${details.attribute}_${details.column}`;
options = [joinTableName, foreignKey, otherKey];
} else {
const joinTableName =
_.get(details, 'collectionName') || _.get(details, 'collectionName') ||
utilsModels.getCollectionName( utilsModels.getCollectionName(
collection.attributes[details.via], targetModel.attributes[details.via],
details details
); );
const relationship = collection.attributes[details.via]; const relationship = targetModel.attributes[details.via];
// Force singular foreign key
relationship.attribute = pluralize.singular(relationship.collection);
details.attribute = pluralize.singular(details.collection);
// Define PK column // Define PK column
details.column = utils.getPK(model, strapi.models); relationship.attribute = singular(relationship.collection);
relationship.column = utils.getPK(details.collection, strapi.models); relationship.column = definition.primaryKey;
// Sometimes the many-to-many relationships // Sometimes the many-to-many relationships
// is on the same keys on the same models (ex: `friends` key in model `User`) // is on the same keys on the same models (ex: `friends` key in model `User`)
@ -238,31 +249,30 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
`${details.attribute}_${details.column}` === `${details.attribute}_${details.column}` ===
`${relationship.attribute}_${relationship.column}` `${relationship.attribute}_${relationship.column}`
) { ) {
relationship.attribute = pluralize.singular(details.via); relationship.attribute = singular(details.via);
}
const foreignKey = `${relationship.attribute}_${relationship.column}`;
const otherKey = `${details.attribute}_${details.column}`;
options = [joinTableName, foreignKey, otherKey];
} }
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; details.isVirtual = true;
loadedModel[name] = function() { loadedModel[name] = function() {
if ( const targetBookshelfModel = GLOBALS[globalId];
_.isArray(_.get(details, 'withPivot')) && let collection = this.belongsToMany(
!_.isEmpty(details.withPivot) targetBookshelfModel,
) { ...options
return this.belongsToMany( );
GLOBALS[globalId],
collectionName, if (Array.isArray(details.withPivot)) {
`${relationship.attribute}_${relationship.column}`, return collection.withPivot(details.withPivot);
`${details.attribute}_${details.column}`
).withPivot(details.withPivot);
} }
return this.belongsToMany( return collection;
GLOBALS[globalId],
collectionName,
`${relationship.attribute}_${relationship.column}`,
`${details.attribute}_${details.column}`
);
}; };
break; break;
} }

View File

@ -8,10 +8,12 @@
const _ = require('lodash'); const _ = require('lodash');
// Utils // Utils
const { models: { getValuePrimaryKey } } = require('strapi-utils'); const {
models: { getValuePrimaryKey },
} = require('strapi-utils');
const transformToArrayID = (array, association) => { const transformToArrayID = (array, association) => {
if(_.isArray(array)) { if (_.isArray(array)) {
array = array.map(value => { array = array.map(value => {
if (_.isPlainObject(value)) { if (_.isPlainObject(value)) {
return value._id || value.id || false; return value._id || value.id || false;
@ -23,7 +25,10 @@ const transformToArrayID = (array, association) => {
return array.filter(n => n); return array.filter(n => n);
} }
if (association.type === 'model' || (association.type === 'collection' && _.isObject(array))) { if (
association.type === 'model' ||
(association.type === 'collection' && _.isObject(array))
) {
return _.isEmpty(_.toString(array)) ? [] : transformToArrayID([array]); return _.isEmpty(_.toString(array)) ? [] : transformToArrayID([array]);
} }
@ -31,19 +36,21 @@ const transformToArrayID = (array, association) => {
}; };
const getModel = (model, plugin) => { const getModel = (model, plugin) => {
return _.get(strapi.plugins, [plugin, 'models', model]) || _.get(strapi, ['models', model]) || undefined; return (
_.get(strapi.plugins, [plugin, 'models', model]) ||
_.get(strapi, ['models', model]) ||
undefined
);
}; };
const removeUndefinedKeys = obj => _.pickBy(obj, _.negate(_.isUndefined)); const removeUndefinedKeys = obj => _.pickBy(obj, _.negate(_.isUndefined));
module.exports = { module.exports = {
findOne: async function (params, populate) { findOne: async function(params, populate) {
const record = await this const record = await this.forge({
.forge({ [this.primaryKey]: getValuePrimaryKey(params, this.primaryKey),
[this.primaryKey]: getValuePrimaryKey(params, this.primaryKey) }).fetch({
}) withRelated: populate || this.associations.map(x => x.alias),
.fetch({
withRelated: populate || this.associations.map(x => x.alias)
}); });
const data = record ? record.toJSON() : record; const data = record ? record.toJSON() : record;
@ -51,11 +58,17 @@ module.exports = {
// Retrieve data manually. // Retrieve data manually.
if (_.isEmpty(populate)) { if (_.isEmpty(populate)) {
const arrayOfPromises = this.associations const arrayOfPromises = this.associations
.filter(association => ['manyMorphToOne', 'manyMorphToMany'].includes(association.nature)) .filter(association =>
['manyMorphToOne', 'manyMorphToMany'].includes(association.nature)
)
.map(() => { .map(() => {
return this.morph.forge() return this.morph
.forge()
.where({ .where({
[`${this.collectionName}_id`]: getValuePrimaryKey(params, this.primaryKey) [`${this.collectionName}_id`]: getValuePrimaryKey(
params,
this.primaryKey
),
}) })
.fetchAll(); .fetchAll();
}); });
@ -70,46 +83,73 @@ module.exports = {
return data; return data;
}, },
update: async function (params) { update: async function(params) {
const relationUpdates = []; const relationUpdates = [];
const primaryKeyValue = getValuePrimaryKey(params, this.primaryKey); const primaryKeyValue = getValuePrimaryKey(params, this.primaryKey);
const response = await module.exports.findOne.call(this, params); const response = await module.exports.findOne.call(this, params);
// Only update fields which are on this document. // Only update fields which are on this document.
const values = params.parseRelationships === false ? params.values : Object.keys(removeUndefinedKeys(params.values)).reduce((acc, current) => { const values =
params.parseRelationships === false
? params.values
: Object.keys(removeUndefinedKeys(params.values)).reduce(
(acc, current) => {
const property = params.values[current]; const property = params.values[current];
const association = this.associations.filter(x => x.alias === current)[0]; const association = this.associations.filter(
x => x.alias === current
)[0];
const details = this._attributes[current]; const details = this._attributes[current];
if (!association && _.get(details, 'isVirtual') !== true) { if (!association && _.get(details, 'isVirtual') !== true) {
return _.set(acc, current, property); return _.set(acc, current, property);
} }
const assocModel = getModel(details.model || details.collection, details.plugin); const assocModel = getModel(
details.model || details.collection,
details.plugin
);
switch (association.nature) { switch (association.nature) {
case 'oneWay': { case 'oneWay': {
return _.set(acc, current, _.get(property, assocModel.primaryKey, property)); return _.set(
acc,
current,
_.get(property, assocModel.primaryKey, property)
);
} }
case 'oneToOne': { case 'oneToOne': {
if (response[current] === property) return acc; if (response[current] === property) return acc;
if (_.isNull(property)) { if (_.isNull(property)) {
const updatePromise = assocModel.where({ const updatePromise = assocModel
[assocModel.primaryKey]: getValuePrimaryKey(response[current], assocModel.primaryKey) .where({
}).save({ [details.via]: null }, {method: 'update', patch: true, require: false}); [assocModel.primaryKey]: getValuePrimaryKey(
response[current],
assocModel.primaryKey
),
})
.save(
{ [details.via]: null },
{ method: 'update', patch: true, require: false }
);
relationUpdates.push(updatePromise); relationUpdates.push(updatePromise);
return _.set(acc, current, null); return _.set(acc, current, null);
} }
// set old relations to null // set old relations to null
const updateLink = this.where({ [current]: property }) const updateLink = this.where({ [current]: property })
.save({ [current]: null }, {method: 'update', patch: true, require: false}) .save(
{ [current]: null },
{ method: 'update', patch: true, require: false }
)
.then(() => { .then(() => {
return assocModel return assocModel
.where({ [this.primaryKey]: property }) .where({ [this.primaryKey]: property })
.save({ [details.via] : primaryKeyValue}, {method: 'update', patch: true, require: false}); .save(
{ [details.via]: primaryKeyValue },
{ method: 'update', patch: true, require: false }
);
}); });
// set new relation // set new relation
@ -121,33 +161,67 @@ module.exports = {
// set relation to null for all the ids not in the list // set relation to null for all the ids not in the list
const currentIds = response[current]; const currentIds = response[current];
const toRemove = _.differenceWith(currentIds, property, (a, b) => { const toRemove = _.differenceWith(
return `${a[assocModel.primaryKey] || a}` === `${b[assocModel.primaryKey] || b}`; currentIds,
}); property,
(a, b) => {
return (
`${a[assocModel.primaryKey] || a}` ===
`${b[assocModel.primaryKey] || b}`
);
}
);
const updatePromise = assocModel const updatePromise = assocModel
.where(assocModel.primaryKey, 'in', toRemove.map(val => val[assocModel.primaryKey]||val)) .where(
.save({ [details.via] : null }, { method: 'update', patch: true, require: false }) assocModel.primaryKey,
'in',
toRemove.map(val => val[assocModel.primaryKey] || val)
)
.save(
{ [details.via]: null },
{ method: 'update', patch: true, require: false }
)
.then(() => { .then(() => {
return assocModel return assocModel
.where(assocModel.primaryKey, 'in', property.map(val => val[assocModel.primaryKey]||val)) .where(
.save({ [details.via] : primaryKeyValue }, { method: 'update', patch: true, require: false }); assocModel.primaryKey,
'in',
property.map(val => val[assocModel.primaryKey] || val)
)
.save(
{ [details.via]: primaryKeyValue },
{ method: 'update', patch: true, require: false }
);
}); });
relationUpdates.push(updatePromise); relationUpdates.push(updatePromise);
return acc; return acc;
} }
case 'manyToOne': { case 'manyToOne': {
return _.set(acc, current, _.get(property, assocModel.primaryKey, property)); return _.set(
acc,
current,
_.get(property, assocModel.primaryKey, property)
);
} }
case 'manyWay':
case 'manyToMany': { case 'manyToMany': {
const currentValue = transformToArrayID(response[current], association).map(id => id.toString()); const currentValue = transformToArrayID(
const storedValue = transformToArrayID(params.values[current], association).map(id => id.toString()); response[current],
association
).map(id => id.toString());
const storedValue = transformToArrayID(
params.values[current],
association
).map(id => id.toString());
const toAdd = _.difference(storedValue, currentValue); const toAdd = _.difference(storedValue, currentValue);
const toRemove = _.difference(currentValue, storedValue); const toRemove = _.difference(currentValue, storedValue);
const collection = this.forge({ [this.primaryKey]: primaryKeyValue })[association.alias](); const collection = this.forge({
[this.primaryKey]: primaryKeyValue,
})[association.alias]();
const updatePromise = collection const updatePromise = collection
.detach(toRemove) .detach(toRemove)
.then(() => collection.attach(toAdd)); .then(() => collection.attach(toAdd));
@ -159,19 +233,21 @@ module.exports = {
case 'manyMorphToOne': case 'manyMorphToOne':
// Update the relational array. // Update the relational array.
params.values[current].forEach(obj => { params.values[current].forEach(obj => {
const model = obj.source && obj.source !== 'content-manager' ? const model =
strapi.plugins[obj.source].models[obj.ref]: obj.source && obj.source !== 'content-manager'
strapi.models[obj.ref]; ? strapi.plugins[obj.source].models[obj.ref]
: strapi.models[obj.ref];
// Remove existing relationship because only one file // Remove existing relationship because only one file
// can be related to this field. // can be related to this field.
if (association.nature === 'manyMorphToOne') { if (association.nature === 'manyMorphToOne') {
relationUpdates.push( relationUpdates.push(
module.exports.removeRelationMorph.call(this, { module.exports.removeRelationMorph
.call(this, {
alias: association.alias, alias: association.alias,
ref: model.collectionName, ref: model.collectionName,
refId: obj.refId, refId: obj.refId,
field: obj.field field: obj.field,
}) })
.then(() => .then(() =>
module.exports.addRelationMorph.call(this, { module.exports.addRelationMorph.call(this, {
@ -179,31 +255,42 @@ module.exports = {
alias: association.alias, alias: association.alias,
ref: model.collectionName, ref: model.collectionName,
refId: obj.refId, refId: obj.refId,
field: obj.field field: obj.field,
}) })
) )
); );
} else { } else {
relationUpdates.push(module.exports.addRelationMorph.call(this, { relationUpdates.push(
module.exports.addRelationMorph.call(this, {
id: response[this.primaryKey], id: response[this.primaryKey],
alias: association.alias, alias: association.alias,
ref: model.collectionName, ref: model.collectionName,
refId: obj.refId, refId: obj.refId,
field: obj.field field: obj.field,
})); })
);
} }
}); });
break; break;
case 'oneToManyMorph': case 'oneToManyMorph':
case 'manyToManyMorph': { case 'manyToManyMorph': {
// Compare array of ID to find deleted files. // Compare array of ID to find deleted files.
const currentValue = transformToArrayID(response[current], association).map(id => id.toString()); const currentValue = transformToArrayID(
const storedValue = transformToArrayID(params.values[current], association).map(id => id.toString()); response[current],
association
).map(id => id.toString());
const storedValue = transformToArrayID(
params.values[current],
association
).map(id => id.toString());
const toAdd = _.difference(storedValue, currentValue); const toAdd = _.difference(storedValue, currentValue);
const toRemove = _.difference(currentValue, storedValue); const toRemove = _.difference(currentValue, storedValue);
const model = getModel(details.collection || details.model, details.plugin); const model = getModel(
details.collection || details.model,
details.plugin
);
toAdd.forEach(id => { toAdd.forEach(id => {
relationUpdates.push( relationUpdates.push(
@ -212,7 +299,7 @@ module.exports = {
alias: association.via, alias: association.via,
ref: this.collectionName, ref: this.collectionName,
refId: response.id, refId: response.id,
field: association.alias field: association.alias,
}) })
); );
}); });
@ -225,7 +312,7 @@ module.exports = {
alias: association.via, alias: association.via,
ref: this.collectionName, ref: this.collectionName,
refId: response.id, refId: response.id,
field: association.alias field: association.alias,
}) })
); );
}); });
@ -238,16 +325,17 @@ module.exports = {
} }
return acc; return acc;
}, {}); },
{}
);
delete values[this.primaryKey];
if (!_.isEmpty(values)) { if (!_.isEmpty(values)) {
relationUpdates.push( relationUpdates.push(
this this.forge({
.forge({ [this.primaryKey]: getValuePrimaryKey(params, this.primaryKey),
[this.primaryKey]: getValuePrimaryKey(params, this.primaryKey) }).save(values, {
}) patch: true,
.save(values, {
patch: true
}) })
); );
} else { } else {
@ -257,17 +345,17 @@ module.exports = {
// Update virtuals fields. // Update virtuals fields.
await Promise.all(relationUpdates); await Promise.all(relationUpdates);
return await this return await this.forge({
.forge({ [this.primaryKey]: getValuePrimaryKey(params, this.primaryKey),
[this.primaryKey]: getValuePrimaryKey(params, this.primaryKey) }).fetch({
}) withRelated: this.associations.map(x => x.alias),
.fetch({
withRelated: this.associations.map(x => x.alias)
}); });
}, },
addRelation: async function (params) { addRelation: async function(params) {
const association = this.associations.find(x => x.via === params.foreignKey && _.get(params.values, x.alias, null)); const association = this.associations.find(
x => x.via === params.foreignKey && _.get(params.values, x.alias, null)
);
if (!association) { if (!association) {
// Resolve silently. // Resolve silently.
@ -281,16 +369,20 @@ module.exports = {
return module.exports.update.call(this, params); return module.exports.update.call(this, params);
case 'manyToMany': case 'manyToMany':
return this.forge({ return this.forge({
[this.primaryKey]: params[this.primaryKey] [this.primaryKey]: params[this.primaryKey],
})[association.alias]().attach(params.values[association.alias]); })
[association.alias]()
.attach(params.values[association.alias]);
default: default:
// Resolve silently. // Resolve silently.
return Promise.resolve(); return Promise.resolve();
} }
}, },
removeRelation: async function (params) { removeRelation: async function(params) {
const association = this.associations.find(x => x.via === params.foreignKey && _.get(params.values, x.alias, null)); const association = this.associations.find(
x => x.via === params.foreignKey && _.get(params.values, x.alias, null)
);
if (!association) { if (!association) {
// Resolve silently. // Resolve silently.
@ -304,24 +396,27 @@ module.exports = {
return module.exports.update.call(this, params); return module.exports.update.call(this, params);
case 'manyToMany': case 'manyToMany':
return this.forge({ return this.forge({
[this.primaryKey]: getValuePrimaryKey(params, this.primaryKey) [this.primaryKey]: getValuePrimaryKey(params, this.primaryKey),
})[association.alias]().detach(params.values[association.alias]); })
[association.alias]()
.detach(params.values[association.alias]);
default: default:
// Resolve silently. // Resolve silently.
return Promise.resolve(); return Promise.resolve();
} }
}, },
addRelationMorph: async function (params) { addRelationMorph: async function(params) {
const record = await this.morph.forge() const record = await this.morph
.forge()
.where({ .where({
[`${this.collectionName}_id`]: params.id, [`${this.collectionName}_id`]: params.id,
[`${params.alias}_id`]: params.refId, [`${params.alias}_id`]: params.refId,
[`${params.alias}_type`]: params.ref, [`${params.alias}_type`]: params.ref,
field: params.field field: params.field,
}) })
.fetch({ .fetch({
withRelated: this.associations.map(x => x.alias) withRelated: this.associations.map(x => x.alias),
}); });
const entry = record ? record.toJSON() : record; const entry = record ? record.toJSON() : record;
@ -330,25 +425,32 @@ module.exports = {
return Promise.resolve(); return Promise.resolve();
} }
return await this.morph.forge({ return await this.morph
.forge({
[`${this.collectionName}_id`]: params.id, [`${this.collectionName}_id`]: params.id,
[`${params.alias}_id`]: params.refId, [`${params.alias}_id`]: params.refId,
[`${params.alias}_type`]: params.ref, [`${params.alias}_type`]: params.ref,
field: params.field field: params.field,
}) })
.save(); .save();
}, },
removeRelationMorph: async function (params) { removeRelationMorph: async function(params) {
return await this.morph.forge() return await this.morph
.where(_.omitBy({ .forge()
.where(
_.omitBy(
{
[`${this.collectionName}_id`]: params.id, [`${this.collectionName}_id`]: params.id,
[`${params.alias}_id`]: params.refId, [`${params.alias}_id`]: params.refId,
[`${params.alias}_type`]: params.ref, [`${params.alias}_type`]: params.ref,
field: params.field field: params.field,
}, _.isUndefined)) },
_.isUndefined
)
)
.destroy({ .destroy({
require: false require: false,
}); });
} },
}; };

View File

@ -1,154 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
// Strapi helper for GraphQL.
const helpers = require('strapi/lib/configuration/hooks/graphql/helpers/'); // eslint-disable-line import/no-unresolved
const utils = require('./');
/**
* Utils functions for BookShelf
*/
module.exports = {
/**
* Get collection identity
*
* @return {String}
*/
getCollectionIdentity: collection => {
return _.capitalize(collection.forge().tableName);
},
/**
* Fetch one record
*
* @return {Object}
*/
fetch: (collectionIdentity, collection, criteria) => {
return collection.forge(criteria)
.fetch({withRelated: helpers.getAssociationsByIdentity(collectionIdentity)})
.then(data => _.isEmpty(data) ? data : data.toJSON());
},
/**
* Fetch all records
*
* @return {Array}
*/
fetchAll: (collectionIdentity, collection, criteria) => {
const filters = _.omit(helpers.handleFilters(criteria), value => {
return _.isUndefined(value) || _.isNumber(value) ? _.isNull(value) : _.isEmpty(value);
});
return collection.forge()
.query(filters)
.fetchAll({withRelated: helpers.getAssociationsByIdentity(collectionIdentity)})
.then(data => data.toJSON() || data);
},
/**
* Fetch latests records based on criteria
*
* @return {Array}
*/
fetchLatest: (collectionIdentity, collection, criteria) => {
const filters = _.omit(helpers.handleFilters(criteria), value => {
return _.isUndefined(value) || _.isNumber(value) ? _.isNull(value) : _.isEmpty(value);
});
// Handle filters
filters.orderBy = 'createdAt DESC';
filters.limit = filters.count;
delete filters.count;
return collection.forge(criteria)
.query(filters)
.fetchAll({withRelated: helpers.getAssociationsByIdentity(collectionIdentity)})
.then(data => data.toJSON() || data);
},
/**
* Fetch first records based on criteria
*
* @return {Array}
*/
fetchFirst: (collectionIdentity, collection, criteria) => {
const filters = _.omit(helpers.handleFilters(criteria), value => {
return _.isUndefined(value) || _.isNumber(value) ? _.isNull(value) : _.isEmpty(value);
});
// Handle filters
filters.orderBy = 'createdAt ASC';
filters.limit = filters.count;
delete filters.count;
return collection.forge(criteria)
.query(filters)
.fetchAll({withRelated: helpers.getAssociationsByIdentity(collectionIdentity)})
.then(data => data.toJSON() || data);
},
/**
* Create record
*
* @return {Object}
*/
create: (collectionIdentity, rootValue) => {
return strapi.services[collectionIdentity.toLowerCase()]
.add(rootValue.context.request.body)
.then(data => _.isFunction(_.get(data, 'toJSON')) ? data.toJSON() : data);
},
/**
* Update record
*
* @return {Object}
*/
update: (collectionIdentity, rootValue, args) => {
_.merge(args, rootValue.context.request.body);
const PK = utils.getPK(collectionIdentity.toLowerCase(), null, strapi.models);
return strapi.services[collectionIdentity.toLowerCase()]
.edit(_.set({}, PK, args[PK]), _.omit(args, PK))
.then(data => _.isFunction(_.get(data, 'toJSON')) ? data.toJSON() : data);
},
/**
* Delete record
*
* @return {Object}
*/
delete: (collectionIdentity, rootValue, args) => {
_.merge(args, rootValue.context.request.body);
return strapi.services[collectionIdentity.toLowerCase()]
.remove(args)
.then(data => _.isFunction(_.get(data, 'toJSON')) ? data.toJSON() : data);
},
/**
* Count records
*
* @return {Array}
*/
count: (collectionIdentity, collection) => collection.forge().count()
};

View File

@ -1,58 +0,0 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
/**
* Utils functions for BookShelf
*/
/* eslint-disable prefer-template */
module.exports = {
/**
* Find primary key
*/
getPK: (collectionIdentity, collection, models) => {
// This is not a Bookshelf collection, only the name.
if (_.isString(collectionIdentity) && !_.isUndefined(models)) {
const PK = _.findKey(_.get(models, collectionIdentity + '.attributes'), o => {
return o.hasOwnProperty('primary');
});
if (!_.isEmpty(PK)) {
return PK;
}
}
try {
if (_.isObject(collection)) {
return collection.forge().idAttribute || 'id';
}
} catch (e) {
// Collection undefined try to get the collection based on collectionIdentity
if (typeof strapi !== 'undefined') {
collection = _.get(strapi, `bookshelf.collections.${collectionIdentity}`);
}
// Impossible to match collectionIdentity before, try to use idAttribute
if (_.isObject(collection)) {
return collection.forge().idAttribute || 'id';
}
}
return 'id';
},
/**
* Find primary key
*/
getCount: type => {
return strapi.bookshelf.collections[type].forge().count().then(count => count);
}
};

View File

@ -4,9 +4,6 @@
* Module dependencies * Module dependencies
*/ */
// Node.js core
const path = require('path');
// Public node modules. // Public node modules.
const _ = require('lodash'); const _ = require('lodash');
const pluralize = require('pluralize'); const pluralize = require('pluralize');
@ -29,33 +26,6 @@ module.exports = {
cb(); cb();
}, },
/**
* Find primary key per ORM
*/
getPK: function(collectionIdentity, collection, models) {
if (_.isString(collectionIdentity)) {
const ORM = this.getORM(collectionIdentity);
try {
const GraphQLFunctions = require(path.resolve(
strapi.config.appPath,
'node_modules',
'strapi-' + ORM,
'lib',
'utils',
));
if (!_.isUndefined(GraphQLFunctions)) {
return GraphQLFunctions.getPK(collectionIdentity, collection, models || strapi.models);
}
} catch (err) {
return undefined;
}
}
return undefined;
},
/** /**
* Retrieve the value based on the primary key * Retrieve the value based on the primary key
*/ */
@ -64,34 +34,6 @@ module.exports = {
return value[defaultKey] || value.id || value._id; return value[defaultKey] || value.id || value._id;
}, },
/**
* Find primary key per ORM
*/
getCount: function(collectionIdentity) {
if (_.isString(collectionIdentity)) {
const ORM = this.getORM(collectionIdentity);
try {
const ORMFunctions = require(path.resolve(
strapi.config.appPath,
'node_modules',
'strapi-' + ORM,
'lib',
'utils',
));
if (!_.isUndefined(ORMFunctions)) {
return ORMFunctions.getCount(collectionIdentity);
}
} catch (err) {
return undefined;
}
}
return undefined;
},
/** /**
* Find relation nature with verbose * Find relation nature with verbose
*/ */
@ -104,11 +46,14 @@ module.exports = {
}; };
if (_.isUndefined(models)) { if (_.isUndefined(models)) {
models = association.plugin ? strapi.plugins[association.plugin].models : strapi.models; models = association.plugin
? strapi.plugins[association.plugin].models
: strapi.models;
} }
if ( if (
(association.hasOwnProperty('collection') && association.collection === '*') || (association.hasOwnProperty('collection') &&
association.collection === '*') ||
(association.hasOwnProperty('model') && association.model === '*') (association.hasOwnProperty('model') && association.model === '*')
) { ) {
if (association.model) { if (association.model) {
@ -117,20 +62,28 @@ module.exports = {
types.current = 'morphTo'; types.current = 'morphTo';
} }
const flattenedPluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { const flattenedPluginsModels = Object.keys(strapi.plugins).reduce(
(acc, current) => {
Object.keys(strapi.plugins[current].models).forEach(model => { Object.keys(strapi.plugins[current].models).forEach(model => {
acc[`${current}_${model}`] = strapi.plugins[current].models[model]; acc[`${current}_${model}`] =
strapi.plugins[current].models[model];
}); });
return acc; return acc;
}, {}); },
{}
);
const allModels = _.merge({}, strapi.models, flattenedPluginsModels); const allModels = _.merge({}, strapi.models, flattenedPluginsModels);
// We have to find if they are a model linked to this key // We have to find if they are a model linked to this key
_.forIn(allModels, model => { _.forIn(allModels, model => {
_.forIn(model.attributes, attribute => { _.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.model === currentModelName) { if (
attribute.hasOwnProperty('via') &&
attribute.via === key &&
attribute.model === currentModelName
) {
if (attribute.hasOwnProperty('collection')) { if (attribute.hasOwnProperty('collection')) {
types.other = 'collection'; types.other = 'collection';
@ -145,14 +98,22 @@ module.exports = {
} }
}); });
}); });
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('collection')) { } else if (
const relatedAttribute = models[association.collection].attributes[association.via]; association.hasOwnProperty('via') &&
association.hasOwnProperty('collection')
) {
const relatedAttribute =
models[association.collection].attributes[association.via];
if (!relatedAttribute) { if (!relatedAttribute) {
throw new Error( throw new Error(
`The attribute \`${association.via}\` is missing in the model ${_.upperFirst(association.collection)} ${ `The attribute \`${
association.via
}\` is missing in the model ${_.upperFirst(
association.collection
)} ${
association.plugin ? '(plugin - ' + association.plugin + ')' : '' association.plugin ? '(plugin - ' + association.plugin + ')' : ''
}`, }`
); );
} }
@ -170,12 +131,21 @@ module.exports = {
!relatedAttribute.hasOwnProperty('via') !relatedAttribute.hasOwnProperty('via')
) { ) {
types.other = 'collectionD'; types.other = 'collectionD';
} else if (relatedAttribute.hasOwnProperty('model') && relatedAttribute.model !== '*') { } else if (
relatedAttribute.hasOwnProperty('model') &&
relatedAttribute.model !== '*'
) {
types.other = 'model'; types.other = 'model';
} else if (relatedAttribute.hasOwnProperty('collection') || relatedAttribute.hasOwnProperty('model')) { } else if (
relatedAttribute.hasOwnProperty('collection') ||
relatedAttribute.hasOwnProperty('model')
) {
types.other = 'morphTo'; types.other = 'morphTo';
} }
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('model')) { } else if (
association.hasOwnProperty('via') &&
association.hasOwnProperty('model')
) {
types.current = 'modelD'; types.current = 'modelD';
// We have to find if they are a model linked to this key // We have to find if they are a model linked to this key
@ -189,9 +159,15 @@ module.exports = {
attribute.collection !== '*' attribute.collection !== '*'
) { ) {
types.other = 'collection'; types.other = 'collection';
} else if (attribute.hasOwnProperty('model') && attribute.model !== '*') { } else if (
attribute.hasOwnProperty('model') &&
attribute.model !== '*'
) {
types.other = 'model'; types.other = 'model';
} else if (attribute.hasOwnProperty('collection') || attribute.hasOwnProperty('model')) { } else if (
attribute.hasOwnProperty('collection') ||
attribute.hasOwnProperty('model')
) {
types.other = 'morphTo'; types.other = 'morphTo';
} }
} else if (association.hasOwnProperty('model')) { } else if (association.hasOwnProperty('model')) {
@ -268,14 +244,18 @@ module.exports = {
nature: 'oneMorphToOne', nature: 'oneMorphToOne',
verbose: 'belongsToMorph', verbose: 'belongsToMorph',
}; };
} else if (types.current === 'morphTo' && (types.other === 'model' || association.hasOwnProperty('model'))) { } else if (
types.current === 'morphTo' &&
(types.other === 'model' || association.hasOwnProperty('model'))
) {
return { return {
nature: 'manyMorphToOne', nature: 'manyMorphToOne',
verbose: 'belongsToManyMorph', verbose: 'belongsToManyMorph',
}; };
} else if ( } else if (
types.current === 'morphTo' && types.current === 'morphTo' &&
(types.other === 'collection' || association.hasOwnProperty('collection')) (types.other === 'collection' ||
association.hasOwnProperty('collection'))
) { ) {
return { return {
nature: 'manyMorphToMany', nature: 'manyMorphToMany',
@ -291,7 +271,10 @@ module.exports = {
nature: 'oneToOne', nature: 'oneToOne',
verbose: 'hasOne', verbose: 'hasOne',
}; };
} else if ((types.current === 'model' || types.current === 'modelD') && types.other === 'collection') { } else if (
(types.current === 'model' || types.current === 'modelD') &&
types.other === 'collection'
) {
return { return {
nature: 'manyToOne', nature: 'manyToOne',
verbose: 'belongsTo', verbose: 'belongsTo',
@ -306,7 +289,10 @@ module.exports = {
nature: 'oneToMany', nature: 'oneToMany',
verbose: 'hasMany', verbose: 'hasMany',
}; };
} else if (types.current === 'collection' && types.other === 'collection') { } else if (
types.current === 'collection' &&
types.other === 'collection'
) {
return { return {
nature: 'manyToMany', nature: 'manyToMany',
verbose: 'belongsToMany', verbose: 'belongsToMany',
@ -334,21 +320,15 @@ module.exports = {
return undefined; return undefined;
} catch (e) { } catch (e) {
strapi.log.error( strapi.log.error(
`Something went wrong in the model \`${_.upperFirst(currentModelName)}\` with the attribute \`${key}\``, `Something went wrong in the model \`${_.upperFirst(
currentModelName
)}\` with the attribute \`${key}\``
); );
strapi.log.error(e); strapi.log.error(e);
strapi.stop(); 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 * Return table name for a collection many-to-many
*/ */
@ -363,9 +343,7 @@ module.exports = {
}) })
.map(table => .map(table =>
_.snakeCase( _.snakeCase(
`${pluralize.plural(table.collection)} ${pluralize.plural( `${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`
table.via
)}`
) )
) )
.join('__'); .join('__');
@ -383,23 +361,44 @@ module.exports = {
} }
// Exclude non-relational attribute // Exclude non-relational attribute
if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model')) { if (!_.has(association, 'collection') && !_.has(association, 'model')) {
return undefined; return;
} }
// Get relation nature // Get relation nature
let details; let details;
const globalName = association.model || association.collection || ''; const targetName = association.model || association.collection || '';
const infos = this.getNature(association, key, undefined, model.toLowerCase()); const infos = this.getNature(
association,
key,
undefined,
model.toLowerCase()
);
if (globalName !== '*') { if (targetName !== '*') {
details = association.plugin if (association.plugin) {
? _.get(strapi.plugins, `${association.plugin}.models.${globalName}.attributes.${association.via}`, {}) details = _.get(
: _.get(strapi.models, `${globalName}.attributes.${association.via}`, {}); strapi.plugins,
[
association.plugin,
'models',
targetName,
'attributes',
association.via,
],
{}
);
} else {
details = _.get(
strapi.models,
[targetName, 'attributes', association.via],
{}
);
}
} }
// Build associations object // Build associations object
if (association.hasOwnProperty('collection') && association.collection !== '*') { if (_.has(association, 'collection') && association.collection !== '*') {
const ast = { const ast = {
alias: key, alias: key,
type: 'collection', type: 'collection',
@ -413,11 +412,22 @@ module.exports = {
}; };
if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') { if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') {
ast.tableCollectionName = this.getCollectionName(association, details); 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); definition.associations.push(ast);
} else if (association.hasOwnProperty('model') && association.model !== '*') { return;
}
if (_.has(association, 'model') && association.model !== '*') {
definition.associations.push({ definition.associations.push({
alias: key, alias: key,
type: 'model', type: 'model',
@ -429,15 +439,23 @@ module.exports = {
plugin: association.plugin || undefined, plugin: association.plugin || undefined,
filter: details.filter, filter: details.filter,
}); });
} else if (association.hasOwnProperty('collection') || association.hasOwnProperty('model')) { return;
const pluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { }
const pluginsModels = Object.keys(strapi.plugins).reduce(
(acc, current) => {
Object.keys(strapi.plugins[current].models).forEach(entity => { Object.keys(strapi.plugins[current].models).forEach(entity => {
Object.keys(strapi.plugins[current].models[entity].attributes).forEach(attribute => { Object.keys(
const attr = strapi.plugins[current].models[entity].attributes[attribute]; strapi.plugins[current].models[entity].attributes
).forEach(attribute => {
const attr =
strapi.plugins[current].models[entity].attributes[attribute];
if ( if (
(attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() && (attr.collection || attr.model || '').toLowerCase() ===
strapi.plugins[current].models[entity].globalId !== definition.globalId model.toLowerCase() &&
strapi.plugins[current].models[entity].globalId !==
definition.globalId
) { ) {
acc.push(strapi.plugins[current].models[entity].globalId); acc.push(strapi.plugins[current].models[entity].globalId);
} }
@ -445,14 +463,17 @@ module.exports = {
}); });
return acc; return acc;
}, []); },
[]
);
const appModels = Object.keys(strapi.models).reduce((acc, entity) => { const appModels = Object.keys(strapi.models).reduce((acc, entity) => {
Object.keys(strapi.models[entity].attributes).forEach(attribute => { Object.keys(strapi.models[entity].attributes).forEach(attribute => {
const attr = strapi.models[entity].attributes[attribute]; const attr = strapi.models[entity].attributes[attribute];
if ( if (
(attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() && (attr.collection || attr.model || '').toLowerCase() ===
model.toLowerCase() &&
strapi.models[entity].globalId !== definition.globalId strapi.models[entity].globalId !== definition.globalId
) { ) {
acc.push(strapi.models[entity].globalId); acc.push(strapi.models[entity].globalId);
@ -472,21 +493,22 @@ module.exports = {
autoPopulate: _.get(association, 'autoPopulate', true), autoPopulate: _.get(association, 'autoPopulate', true),
filter: association.filter, filter: association.filter,
}); });
}
} catch (e) { } catch (e) {
strapi.log.error(`Something went wrong in the model \`${_.upperFirst(model)}\` with the attribute \`${key}\``); strapi.log.error(
`Something went wrong in the model \`${_.upperFirst(
model
)}\` with the attribute \`${key}\``
);
strapi.log.error(e); strapi.log.error(e);
strapi.stop(); strapi.stop();
} }
}, },
getVia: (attribute, association) => {
return _.findKey(strapi.models[association.model || association.collection].attributes, { via: attribute });
},
convertParams: (entity, params) => { convertParams: (entity, params) => {
if (!entity) { if (!entity) {
throw new Error("You can't call the convert params method without passing the model's name as a first argument."); throw new Error(
"You can't call the convert params method without passing the model's name as a first argument."
);
} }
// Remove the source params (that can be sent from the ctm plugin) since it is not a filter // Remove the source params (that can be sent from the ctm plugin) since it is not a filter
@ -502,7 +524,7 @@ module.exports = {
Object.keys(strapi.plugins).reduce((acc, current) => { Object.keys(strapi.plugins).reduce((acc, current) => {
_.assign(acc, _.get(strapi.plugins[current], ['models'], {})); _.assign(acc, _.get(strapi.plugins[current], ['models'], {}));
return acc; return acc;
}, {}), }, {})
); );
if (!models.hasOwnProperty(model)) { if (!models.hasOwnProperty(model)) {
@ -513,7 +535,9 @@ module.exports = {
const connector = models[model].orm; const connector = models[model].orm;
if (!connector) { if (!connector) {
throw new Error(`Impossible to determine the ORM used for the model ${model}.`); throw new Error(
`Impossible to determine the ORM used for the model ${model}.`
);
} }
const convertor = strapi.hook[connector].load().getQueryParams; const convertor = strapi.hook[connector].load().getQueryParams;
@ -555,13 +579,31 @@ module.exports = {
} else { } else {
const suffix = key.split('_'); const suffix = key.split('_');
// Mysql stores boolean as 1 or 0 // Mysql stores boolean as 1 or 0
if (client === 'mysql' && _.get(models, [model, 'attributes', suffix, 'type']) === 'boolean') { if (
client === 'mysql' &&
_.get(models, [model, 'attributes', suffix, 'type']) === 'boolean'
) {
formattedValue = value.toString() === 'true' ? '1' : '0'; formattedValue = value.toString() === 'true' ? '1' : '0';
} }
let type; let type;
if (_.includes(['ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'containss', 'in', 'nin'], _.last(suffix))) { if (
_.includes(
[
'ne',
'lt',
'gt',
'lte',
'gte',
'contains',
'containss',
'in',
'nin',
],
_.last(suffix)
)
) {
type = `_${_.last(suffix)}`; type = `_${_.last(suffix)}`;
key = _.dropRight(suffix).join('_'); key = _.dropRight(suffix).join('_');
} else { } else {