From 78380aa220dfedf4f44a85a894ef54c58d0fe35c Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Fri, 12 Jul 2019 17:54:42 +0200 Subject: [PATCH 1/3] Implement create and updates for groups in a CT --- .../strapi-hook-mongoose/lib/mount-models.js | 1 + .../strapi/lib/core-api/queries/bookshelf.js | 85 +++---- .../strapi/lib/core-api/queries/mongoose.js | 210 +++++++++++++++++- 3 files changed, 238 insertions(+), 58 deletions(-) diff --git a/packages/strapi-hook-mongoose/lib/mount-models.js b/packages/strapi-hook-mongoose/lib/mount-models.js index 50285215a75..e011f5cd61a 100644 --- a/packages/strapi-hook-mongoose/lib/mount-models.js +++ b/packages/strapi-hook-mongoose/lib/mount-models.js @@ -17,6 +17,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { definition.associations = []; definition.globalName = _.upperFirst(_.camelCase(definition.globalId)); definition.loadedModel = {}; + definition.ObjectId = mongoose.Types.ObjectId; // Set the default values to model settings. _.defaults(definition, { primaryKey: '_id', diff --git a/packages/strapi/lib/core-api/queries/bookshelf.js b/packages/strapi/lib/core-api/queries/bookshelf.js index b4c6311b08b..70ad4ada493 100644 --- a/packages/strapi/lib/core-api/queries/bookshelf.js +++ b/packages/strapi/lib/core-api/queries/bookshelf.js @@ -8,9 +8,16 @@ module.exports = ({ model, modelKey }) => { }); const excludedKeys = assocKeys.concat(groupKeys); - function excludeExernalValues(values) { + const defaultPopulate = model.associations + .filter(ast => ast.autoPopulate !== false) + .map(ast => ast.alias); + + const pickRelations = values => { + return _.pick(values, assocKeys); + }; + const omitExernalValues = values => { return _.omit(values, excludedKeys); - } + }; /** * @@ -91,7 +98,7 @@ module.exports = ({ model, modelKey }) => { const groupValue = values[key]; - const updateOreateGroupAndLink = async ({ value, order }) => { + const updateOrCreateGroupAndLink = async ({ value, order }) => { // check if value has an id then update else create if (_.has(value, groupModel.primaryKey)) { return groupModel @@ -115,23 +122,23 @@ module.exports = ({ model, modelKey }) => { { transacting, patch: true, require: false } ); }); - } else { - return groupModel - .forge() - .save(value, { transacting }) - .then(group => { - return joinModel.forge().save( - { - [foreignKey]: entry.id, - slice_type: groupModel.collectionName, - slice_id: group.id, - field: key, - order, - }, - { transacting } - ); - }); } + // create + return groupModel + .forge() + .save(value, { transacting }) + .then(group => { + return joinModel.forge().save( + { + [foreignKey]: entry.id, + slice_type: groupModel.collectionName, + slice_id: group.id, + field: key, + order, + }, + { transacting } + ); + }); }; if (repeatable === true) { @@ -146,7 +153,7 @@ module.exports = ({ model, modelKey }) => { await Promise.all( groupValue.map((value, idx) => { - return updateOreateGroupAndLink({ value, order: idx + 1 }); + return updateOrCreateGroupAndLink({ value, order: idx + 1 }); }) ); } else { @@ -159,7 +166,7 @@ module.exports = ({ model, modelKey }) => { transacting, }); - await updateOreateGroupAndLink({ value: groupValue, order: 1 }); + await updateOrCreateGroupAndLink({ value: groupValue, order: 1 }); } } return; @@ -196,7 +203,7 @@ module.exports = ({ model, modelKey }) => { }); const idsToDelete = _.difference(allIds, idsToKeep); - if (idsToDelete > 0) { + if (idsToDelete.length > 0) { await joinModel .forge() .query(qb => qb.whereIn('slice_id', idsToDelete)) @@ -252,11 +259,7 @@ module.exports = ({ model, modelKey }) => { return { find(params, populate) { - const withRelated = - populate || - model.associations - .filter(ast => ast.autoPopulate !== false) - .map(ast => ast.alias); + const withRelated = populate || defaultPopulate; const filters = convertRestQueryParams(params); @@ -266,11 +269,7 @@ module.exports = ({ model, modelKey }) => { }, findOne(params, populate) { - const withRelated = - populate || - model.associations - .filter(ast => ast.autoPopulate !== false) - .map(ast => ast.alias); + const withRelated = populate || defaultPopulate; return model .forge({ @@ -288,12 +287,8 @@ module.exports = ({ model, modelKey }) => { }, async create(values) { - const relations = _.pick( - values, - model.associations.map(ast => ast.alias) - ); - - const data = excludeExernalValues(values); + const relations = pickRelations(values); + const data = omitExernalValues(values); const runCreate = async trx => { // Create entry with no-relational data. @@ -318,12 +313,8 @@ module.exports = ({ model, modelKey }) => { } // Extract values related to relational data. - const relations = _.pick( - values, - model.associations.map(ast => ast.alias) - ); - - const data = excludeExernalValues(values); + const relations = pickRelations(values); + const data = omitExernalValues(values); const runUpdate = async trx => { const entry = await model @@ -386,11 +377,7 @@ module.exports = ({ model, modelKey }) => { const filters = strapi.utils.models.convertParams(modelKey, params); // Select field to populate. - const withRelated = - populate || - model.associations - .filter(ast => ast.autoPopulate !== false) - .map(ast => ast.alias); + const withRelated = populate || defaultPopulate; return model .query(qb => { diff --git a/packages/strapi/lib/core-api/queries/mongoose.js b/packages/strapi/lib/core-api/queries/mongoose.js index b6e6b049e9e..ae343bf39b6 100644 --- a/packages/strapi/lib/core-api/queries/mongoose.js +++ b/packages/strapi/lib/core-api/queries/mongoose.js @@ -2,12 +2,162 @@ const _ = require('lodash'); const { convertRestQueryParams, buildQuery } = require('strapi-utils'); module.exports = ({ model, modelKey, strapi }) => { - const assocs = model.associations.map(ast => ast.alias); + const hasPK = obj => _.has(obj, model.primaryKey) || _.has(obj, 'id'); + const getPK = obj => + _.has(obj, model.primaryKey) ? obj[model.primaryKey] : obj.id; + + const assocKeys = model.associations.map(ast => ast.alias); + const groupKeys = Object.keys(model.attributes).filter(key => { + return model.attributes[key].type === 'group'; + }); + const excludedKeys = assocKeys.concat(groupKeys); const defaultPopulate = model.associations .filter(ast => ast.autoPopulate !== false) .map(ast => ast.alias); + const pickRelations = values => { + return _.pick(values, assocKeys); + }; + + const omitExernalValues = values => { + return _.omit(values, excludedKeys); + }; + + async function createGroups(entry, values) { + if (groupKeys.length === 0) return; + + for (let key of groupKeys) { + const attr = model.attributes[key]; + const { group, required = true, repeatable = true } = attr; + + const groupModel = strapi.groups[group]; + + if (required === true && !_.has(values, key)) { + const err = new Error(`Group ${key} is required`); + err.status = 400; + throw err; + } + + if (!_.has(values, key)) continue; + + const groupValue = values[key]; + + if (repeatable === true) { + validateRepeatableInput(groupValue, { key, ...attr }); + const groups = await Promise.all( + groupValue.map(value => groupModel.create(value)) + ); + + const groupsArr = groups.map(group => ({ + kind: groupModel.globalId, + ref: group, + })); + + entry[key] = groupsArr; + await entry.save(); + } else { + validateNonRepeatableInput(groupValue, { key, ...attr }); + const group = await groupModel.create(groupValue); + entry[key] = [ + { + kind: groupModel.globalId, + ref: group, + }, + ]; + await entry.save(); + } + } + } + + async function updateGroups(entry, values) { + if (groupKeys.length === 0) return; + + for (let key of groupKeys) { + // if key isn't present then don't change the current group data + if (!_.has(values, key)) continue; + + const attr = model.attributes[key]; + const { group, repeatable = true } = attr; + + const groupModel = strapi.groups[group]; + const groupValue = values[key]; + + const updateOCreateGroup = async value => { + // check if value has an id then update else create + if (hasPK(value)) { + return groupModel.findOneAndUpdate( + { + [model.primaryKey]: getPK(value), + }, + value, + { new: true } + ); + } + return groupModel.create(value); + }; + + if (repeatable === true) { + validateRepeatableInput(groupValue, { key, ...attr }); + + await deleteOldGroups(entry, groupValue, { key, groupModel }); + + const groups = await Promise.all(groupValue.map(updateOCreateGroup)); + const groupsArr = groups.map(group => ({ + kind: groupModel.globalId, + ref: group, + })); + + entry[key] = groupsArr; + await entry.save(); + } else { + validateNonRepeatableInput(groupValue, { key, ...attr }); + + await deleteOldGroups(entry, groupValue, { key, groupModel }); + + const group = await updateOCreateGroup(groupValue); + entry[key] = [ + { + kind: groupModel.globalId, + ref: group, + }, + ]; + await entry.save(); + } + } + return; + } + + async function deleteOldGroups(entry, groupValue, { key, groupModel }) { + const groupArr = Array.isArray(groupValue) ? groupValue : [groupValue]; + + const idsToKeep = groupArr.filter(hasPK).map(getPK); + const allIds = await (entry[key] || []) + .filter(el => el.ref) + .map(el => el.ref._id); + + // verify the provided ids are realted to this entity. + idsToKeep.forEach(id => { + if (allIds.findIndex(currentId => currentId.toString() === id) === -1) { + const err = new Error( + `Some of the provided groups in ${key} are not related to the entity` + ); + err.status = 400; + throw err; + } + }); + + const idsToDelete = allIds.reduce((acc, id) => { + if (idsToKeep.includes(id.toString())) return acc; + return acc.concat(id); + }, []); + + if (idsToDelete.length > 0) { + await groupModel.deleteMany({ [model.primaryKey]: { $in: idsToDelete } }); + } + } + + // public api return { find(params, populate) { const populateOpt = populate || defaultPopulate; @@ -25,9 +175,7 @@ module.exports = ({ model, modelKey, strapi }) => { const populateOpt = populate || defaultPopulate; return model - .findOne({ - [model.primaryKey]: params[model.primaryKey] || params.id, - }) + .findOne({ [model.primaryKey]: getPK(params) }) .populate(populateOpt); }, @@ -42,23 +190,34 @@ module.exports = ({ model, modelKey, strapi }) => { async create(values) { // Extract values related to relational data. - const relations = _.pick(values, assocs); - const data = _.omit(values, assocs); + const relations = pickRelations(values); + const data = omitExernalValues(values); // Create entry with no-relational data. const entry = await model.create(data); + await createGroups(entry, values); + // Create relational data and return the entry. return model.updateRelations({ _id: entry.id, values: relations }); }, async update(params, values) { + const entry = await model.findOne({ _id: params.id }); + + if (!entry) { + const err = new Error('entry.notFound'); + err.status = 404; + throw err; + } + // Extract values related to relational data. - const relations = _.pick(values, assocs); - const data = _.omit(values, assocs); + const relations = pickRelations(values); + const data = omitExernalValues(values); // Update entry with no-relational data. - await model.updateOne(params, data, { multi: true }); + await entry.updateOne(params, data); + await updateGroups(entry, values); // Update relational data and return the entry. return model.updateRelations( @@ -153,3 +312,36 @@ const buildSearchOr = (model, query) => { } }, []); }; + +function validateRepeatableInput(value, { key, min, max }) { + if (!Array.isArray(value)) { + const err = new Error(`Group ${key} is repetable. Expected an array`); + err.status = 400; + throw err; + } + + if (min && value.length < min) { + const err = new Error(`Group ${key} must contain at least ${min} items`); + err.status = 400; + throw err; + } + if (max && value.length > max) { + const err = new Error(`Group ${key} must contain at most ${max} items`); + err.status = 400; + throw err; + } +} + +function validateNonRepeatableInput(value, { key, required }) { + if (typeof value !== 'object') { + const err = new Error(`Group ${key} should be an object`); + err.status = 400; + throw err; + } + + if (required === true && value === null) { + const err = new Error(`Group ${key} is required`); + err.status = 400; + throw err; + } +} From 4056dc017f3e38139affdd52754b7fa84f8d91b7 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Fri, 12 Jul 2019 18:07:34 +0200 Subject: [PATCH 2/3] Delete groups --- .../strapi/lib/core-api/queries/bookshelf.js | 8 --- .../strapi/lib/core-api/queries/mongoose.js | 53 ++++++++++++++----- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/strapi/lib/core-api/queries/bookshelf.js b/packages/strapi/lib/core-api/queries/bookshelf.js index 70ad4ada493..a8ee0eac4f8 100644 --- a/packages/strapi/lib/core-api/queries/bookshelf.js +++ b/packages/strapi/lib/core-api/queries/bookshelf.js @@ -19,14 +19,6 @@ module.exports = ({ model, modelKey }) => { return _.omit(values, excludedKeys); }; - /** - * - * @param {Object} entry - the entity the groups are linked ot - * @param {Object} values - Input values - * @param {Object} options - * @param {Object} options.model - the ref model - * @param {Object} options.transacting - transaction option - */ async function createGroups(entry, values, { transacting }) { if (groupKeys.length === 0) return; diff --git a/packages/strapi/lib/core-api/queries/mongoose.js b/packages/strapi/lib/core-api/queries/mongoose.js index ae343bf39b6..4c82362c960 100644 --- a/packages/strapi/lib/core-api/queries/mongoose.js +++ b/packages/strapi/lib/core-api/queries/mongoose.js @@ -157,6 +157,22 @@ module.exports = ({ model, modelKey, strapi }) => { } } + async function deleteGroups(entry) { + if (groupKeys.length === 0) return; + + for (let key of groupKeys) { + const attr = model.attributes[key]; + const { group } = attr; + const groupModel = strapi.groups[group]; + + if (Array.isArray(entry[key]) && entry[key].length > 0) { + await groupModel.deleteMany({ + [model.primaryKey]: { $in: entry[key].map(el => el.ref) }, + }); + } + } + } + // public api return { find(params, populate) { @@ -199,11 +215,14 @@ module.exports = ({ model, modelKey, strapi }) => { await createGroups(entry, values); // Create relational data and return the entry. - return model.updateRelations({ _id: entry.id, values: relations }); + return model.updateRelations({ + [model.primaryKey]: getPK(entry), + values: relations, + }); }, async update(params, values) { - const entry = await model.findOne({ _id: params.id }); + const entry = await model.findOne({ [model.primaryKey]: getPK(params) }); if (!entry) { const err = new Error('entry.notFound'); @@ -226,43 +245,51 @@ module.exports = ({ model, modelKey, strapi }) => { }, async delete(params) { - const data = await model - .findOneAndRemove(params, {}) + const entry = await model + .findOneAndRemove({ [model.primaryKey]: getPK(params) }) .populate(defaultPopulate); - if (!data) { - return data; + if (!entry) { + const err = new Error('entry.notFound'); + err.status = 404; + throw err; } + await deleteGroups(entry); + await Promise.all( model.associations.map(async association => { - if (!association.via || !data._id || association.dominant) { + if ( + !association.via || + !entry[model.primaryKey] || + association.dominant + ) { return true; } const search = _.endsWith(association.nature, 'One') || association.nature === 'oneToMany' - ? { [association.via]: data._id } - : { [association.via]: { $in: [data._id] } }; + ? { [association.via]: entry[model.primaryKey] } + : { [association.via]: { $in: [entry[model.primaryKey]] } }; const update = _.endsWith(association.nature, 'One') || association.nature === 'oneToMany' ? { [association.via]: null } - : { $pull: { [association.via]: data._id } }; + : { $pull: { [association.via]: entry[model.primaryKey] } }; // Retrieve model. - const model = association.plugin + const assocModel = association.plugin ? strapi.plugins[association.plugin].models[ association.model || association.collection ] : strapi.models[association.model || association.collection]; - return model.update(search, update, { multi: true }); + return assocModel.update(search, update, { multi: true }); }) ); - return data; + return entry; }, search(params, populate) { From 8d872daf77bf03a6f75f2e9c5500b363dc0ac085 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 15 Jul 2019 09:26:03 +0200 Subject: [PATCH 3/3] call entry updateOne with correct params --- packages/strapi/lib/core-api/queries/mongoose.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/strapi/lib/core-api/queries/mongoose.js b/packages/strapi/lib/core-api/queries/mongoose.js index 4c82362c960..917d360d3e0 100644 --- a/packages/strapi/lib/core-api/queries/mongoose.js +++ b/packages/strapi/lib/core-api/queries/mongoose.js @@ -235,7 +235,7 @@ module.exports = ({ model, modelKey, strapi }) => { const data = omitExernalValues(values); // Update entry with no-relational data. - await entry.updateOne(params, data); + await entry.updateOne(data); await updateGroups(entry, values); // Update relational data and return the entry.