diff --git a/examples/getstarted/api/restaurant/models/Restaurant.settings.json b/examples/getstarted/api/restaurant/models/Restaurant.settings.json index 175f0746c7..1d93982c45 100755 --- a/examples/getstarted/api/restaurant/models/Restaurant.settings.json +++ b/examples/getstarted/api/restaurant/models/Restaurant.settings.json @@ -7,10 +7,7 @@ }, "options": { "increments": true, - "timestamps": [ - "created_at", - "updated_at" - ], + "timestamps": ["created_at", "updated_at"], "comment": "" }, "attributes": { @@ -38,21 +35,12 @@ "collection": "category" }, "price_range": { - "enum": [ - "very_cheap", - "cheap", - "average", - "expensive", - "very_expensive" - ], + "enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"], "type": "enumeration" }, "body": { "type": "dynamiczone", - "components": [ - "default.closingperiod", - "default.restaurantservice" - ] + "components": ["default.closingperiod", "default.restaurantservice"] }, "description": { "type": "richtext", @@ -61,7 +49,6 @@ "opening_times": { "component": "default.openingtimes", "type": "component", - "required": true, "repeatable": true, "min": 1, "max": 10 @@ -72,7 +59,6 @@ }, "services": { "component": "default.restaurantservice", - "required": true, "repeatable": true, "type": "component" }, @@ -83,4 +69,4 @@ "type": "text" } } -} \ No newline at end of file +} diff --git a/examples/getstarted/components/default/closingperiod.json b/examples/getstarted/components/default/closingperiod.json index 2f91094900..425d83cc5f 100644 --- a/examples/getstarted/components/default/closingperiod.json +++ b/examples/getstarted/components/default/closingperiod.json @@ -17,6 +17,11 @@ "type": "date", "required": true }, + "media": { + "model": "file", + "via": "related", + "plugin": "upload" + }, "dish": { "component": "default.dish", "type": "component" diff --git a/examples/getstarted/components/default/restaurantservice.json b/examples/getstarted/components/default/restaurantservice.json index c12a906fbd..738881bbcd 100644 --- a/examples/getstarted/components/default/restaurantservice.json +++ b/examples/getstarted/components/default/restaurantservice.json @@ -10,6 +10,11 @@ "type": "string", "required": true }, + "media": { + "model": "file", + "via": "related", + "plugin": "upload" + }, "is_available": { "type": "boolean", "required": true, diff --git a/packages/strapi-connector-mongoose/lib/mount-models.js b/packages/strapi-connector-mongoose/lib/mount-models.js index d2cc3530f1..0bb63fcff6 100644 --- a/packages/strapi-connector-mongoose/lib/mount-models.js +++ b/packages/strapi-connector-mongoose/lib/mount-models.js @@ -6,6 +6,7 @@ const mongoose = require('mongoose'); const utilsModels = require('strapi-utils').models; const utils = require('./utils'); const relations = require('./relations'); +const { findComponentByGlobalId } = require('./utils/helpers'); module.exports = ({ models, target, plugin = false }, ctx) => { const { instance } = ctx; @@ -27,13 +28,18 @@ module.exports = ({ models, target, plugin = false }, ctx) => { global[definition.globalName] = {}; } - const componentAttributes = Object.keys(definition.attributes).filter( - key => definition.attributes[key].type === 'component' + const componentAttributes = Object.keys(definition.attributes).filter(key => + ['component', 'dynamiczone'].includes(definition.attributes[key].type) ); const scalarAttributes = Object.keys(definition.attributes).filter(key => { const { type } = definition.attributes[key]; - return type !== undefined && type !== null && type !== 'component'; + return ( + type !== undefined && + type !== null && + type !== 'component' && + type !== 'dynamiczone' + ); }); const relationalAttributes = Object.keys(definition.attributes).filter( @@ -43,7 +49,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { } ); - // handle gorup attrs + // handle component and dynamic zone attrs if (componentAttributes.length > 0) { // create join morph collection thingy componentAttributes.forEach(name => { @@ -129,8 +135,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { if (_.isFunction(target[model.toLowerCase()][fn])) { schema.pre(key, function() { - return target[model.toLowerCase()] - [fn](this); + return target[model.toLowerCase()][fn](this); }); } }); @@ -252,14 +257,28 @@ module.exports = ({ models, target, plugin = false }, ctx) => { componentAttributes.forEach(name => { const attribute = definition.attributes[name]; + const { type } = attribute; - if (Array.isArray(returned[name])) { - const components = returned[name].map(el => el.ref); - // Reformat data by bypassing the many-to-many relationship. - returned[name] = - attribute.repeatable === true - ? components - : _.first(components) || null; + if (type === 'component') { + if (Array.isArray(returned[name])) { + const components = returned[name].map(el => el.ref); + // Reformat data by bypassing the many-to-many relationship. + returned[name] = + attribute.repeatable === true + ? components + : _.first(components) || null; + } + } + + if (type === 'dynamiczone') { + const components = returned[name].map(el => { + return { + __component: findComponentByGlobalId(el.kind).uid, + ...el.ref, + }; + }); + + returned[name] = components; } }); }, @@ -304,121 +323,38 @@ const createOnFetchPopulateFn = ({ componentAttributes, definition, }) => { - return function(next) { + return function() { + const populatedPaths = this.getPopulatedPaths(); + morphAssociations.forEach(association => { - if ( - this._mongooseOptions.populate && - this._mongooseOptions.populate[association.alias] - ) { - if ( - association.nature === 'oneToManyMorph' || - association.nature === 'manyToManyMorph' - ) { - this._mongooseOptions.populate[association.alias].match = { + const { alias, nature } = association; + + if (['oneToManyMorph', 'manyToManyMorph'].includes(nature)) { + this.populate({ + path: alias, + match: { [`${association.via}.${association.filter}`]: association.alias, [`${association.via}.kind`]: definition.globalId, - }; - - // Select last related to an entity. - this._mongooseOptions.populate[association.alias].options = { + }, + options: { sort: '-createdAt', - }; - } else { - this._mongooseOptions.populate[ - association.alias - ].path = `${association.alias}.ref`; - } - } else { - if (!this._mongooseOptions.populate) { - this._mongooseOptions.populate = {}; - } - // Images are not displayed in populated data. - // We automatically populate morph relations. - if ( - association.nature === 'oneToManyMorph' || - association.nature === 'manyToManyMorph' - ) { - this._mongooseOptions.populate[association.alias] = { - path: association.alias, - match: { - [`${association.via}.${association.filter}`]: association.alias, - [`${association.via}.kind`]: definition.globalId, - }, - options: { - sort: '-createdAt', - }, - select: undefined, - model: undefined, - _docs: {}, - }; - } - } - }); - - componentAttributes.forEach(name => { - const attr = definition.attributes[name]; - - const component = strapi.components[attr.component]; - - const assocs = (component.associations || []).filter( - assoc => assoc.autoPopulate === true - ); - - let subpopulates = []; - - assocs.forEach(assoc => { - if (isPolymorphic({ assoc })) { - if ( - assoc.nature === 'oneToManyMorph' || - assoc.nature === 'manyToManyMorph' - ) { - subpopulates.push({ - path: assoc.alias, - match: { - [`${assoc.via}.${assoc.filter}`]: assoc.alias, - [`${assoc.via}.kind`]: definition.globalId, - }, - options: { - sort: '-createdAt', - }, - select: undefined, - model: undefined, - _docs: {}, - }); - } else { - subpopulates.push({ path: `${assoc.alias}.ref`, _docs: {} }); - } - } else { - subpopulates.push({ - path: assoc.alias, - _docs: {}, - }); - } - }); - - if ( - this._mongooseOptions.populate && - this._mongooseOptions.populate[name] - ) { - this._mongooseOptions.populate[name].path = `${name}.ref`; - this._mongooseOptions.populate[name].populate = subpopulates; - } else { - _.set(this._mongooseOptions, ['populate', name], { - path: `${name}.ref`, - populate: subpopulates, - _docs: {}, + }, }); + + return; + } + + if (populatedPaths.includes(alias)) { + _.set(this._mongooseOptions.populate, [alias, 'path'], `${alias}.ref`); } }); - next(); + componentAttributes.forEach(key => { + this.populate({ path: `${key}.ref` }); + }); }; }; -const isPolymorphic = ({ assoc }) => { - return assoc.nature.toLowerCase().indexOf('morph') !== -1; -}; - const buildRelation = ({ definition, model, instance, attribute, name }) => { const { nature, verbose } = utilsModels.getNature(attribute, name, undefined, model.toLowerCase()) || diff --git a/packages/strapi-connector-mongoose/lib/queries.js b/packages/strapi-connector-mongoose/lib/queries.js index 9a78277130..401adb3efb 100644 --- a/packages/strapi-connector-mongoose/lib/queries.js +++ b/packages/strapi-connector-mongoose/lib/queries.js @@ -10,15 +10,18 @@ const { models: modelUtils, } = require('strapi-utils'); -module.exports = ({ model, modelKey, strapi }) => { - const hasPK = obj => _.has(obj, model.primaryKey) || _.has(obj, 'id'); - const getPK = obj => - _.has(obj, model.primaryKey) ? obj[model.primaryKey] : obj.id; +const { findComponentByGlobalId } = require('./utils/helpers'); +const hasPK = (obj, model) => _.has(obj, model.primaryKey) || _.has(obj, 'id'); +const getPK = (obj, model) => + _.has(obj, model.primaryKey) ? obj[model.primaryKey] : obj.id; + +module.exports = ({ model, modelKey, strapi }) => { const assocKeys = model.associations.map(ast => ast.alias); - const componentKeys = Object.keys(model.attributes).filter(key => { - return model.attributes[key].type === 'component'; - }); + const componentKeys = Object.keys(model.attributes).filter(key => + ['component', 'dynamiczone'].includes(model.attributes[key].type) + ); + const excludedKeys = assocKeys.concat(componentKeys); const defaultPopulate = model.associations @@ -38,49 +41,96 @@ module.exports = ({ model, modelKey, strapi }) => { for (let key of componentKeys) { const attr = model.attributes[key]; - const { component, required = false, repeatable = false } = attr; + const { type } = attr; - const componentModel = strapi.components[component]; + if (type === 'component') { + const { component, required = false, repeatable = false } = attr; - if (required === true && !_.has(values, key)) { - const err = new Error(`Component ${key} is required`); - err.status = 400; - throw err; + const componentModel = strapi.components[component]; + + if (required === true && !_.has(values, key)) { + const err = new Error(`Component ${key} is required`); + err.status = 400; + throw err; + } + + if (!_.has(values, key)) continue; + + const componentValue = values[key]; + + if (repeatable === true) { + validateRepeatableInput(componentValue, { key, ...attr }); + const components = await Promise.all( + componentValue.map(value => { + return strapi.query(component).create(value); + }) + ); + + const componentsArr = components.map(componentEntry => ({ + kind: componentModel.globalId, + ref: componentEntry, + })); + + entry[key] = componentsArr; + await entry.save(); + } else { + validateNonRepeatableInput(componentValue, { key, ...attr }); + if (componentValue === null) continue; + + const componentEntry = await strapi + .query(component) + .create(componentValue); + entry[key] = [ + { + kind: componentModel.globalId, + ref: componentEntry, + }, + ]; + await entry.save(); + } } - if (!_.has(values, key)) continue; + if (type === 'dynamiczone') { + const { required = false } = attr; - const componentValue = values[key]; + if (required === true && !_.has(values, key)) { + const err = new Error(`Dynamiczone ${key} is required`); + err.status = 400; + throw err; + } - if (repeatable === true) { - validateRepeatableInput(componentValue, { key, ...attr }); - const components = await Promise.all( - componentValue.map(value => { - return strapi.query(component).create(value); + if (!_.has(values, key)) continue; + + const dynamiczoneValues = values[key]; + + validateDynamiczoneInput(dynamiczoneValues, { key, ...attr }); + + const dynamiczones = await Promise.all( + dynamiczoneValues.map(value => { + const component = value.__component; + return strapi + .query(component) + .create(value) + .then(entity => { + return { + __component: value.__component, + entity, + }; + }); }) ); - const componentsArr = components.map(componentEntry => ({ - kind: componentModel.globalId, - ref: componentEntry, - })); + const componentsArr = dynamiczones.map(({ __component, entity }) => { + const componentModel = strapi.components[__component]; + + return { + kind: componentModel.globalId, + ref: entity, + }; + }); entry[key] = componentsArr; await entry.save(); - } else { - validateNonRepeatableInput(componentValue, { key, ...attr }); - if (componentValue === null) continue; - - const componentEntry = await strapi - .query(component) - .create(componentValue); - entry[key] = [ - { - kind: componentModel.globalId, - ref: componentEntry, - }, - ]; - await entry.save(); } } } @@ -88,70 +138,185 @@ module.exports = ({ model, modelKey, strapi }) => { async function updateComponents(entry, values) { if (componentKeys.length === 0) return; + const updateOrCreateComponent = async ({ componentUID, value }) => { + // check if value has an id then update else create + const query = strapi.query(componentUID); + if (hasPK(value, query.model)) { + return query.update( + { + [query.model.primaryKey]: getPK(value, query.model), + }, + value + ); + } + return query.create(value); + }; + for (let key of componentKeys) { // if key isn't present then don't change the current component data if (!_.has(values, key)) continue; const attr = model.attributes[key]; - const { component, repeatable = false } = attr; + const { type } = attr; - const componentModel = strapi.components[component]; - const componentValue = values[key]; + if (type === 'component') { + const { component: componentUID, repeatable = false } = attr; - const updateOrCreateComponent = async value => { - // check if value has an id then update else create - if (hasPK(value)) { - return strapi.query(component).update( - { - [model.primaryKey]: getPK(value), - }, - value + const componentModel = strapi.components[componentUID]; + const componentValue = values[key]; + + if (repeatable === true) { + validateRepeatableInput(componentValue, { key, ...attr }); + + await deleteOldComponents(entry, componentValue, { + key, + componentModel, + }); + + const components = await Promise.all( + componentValue.map(value => + updateOrCreateComponent({ componentUID, value }) + ) ); - } - return strapi.query(component).create(value); - }; - - if (repeatable === true) { - validateRepeatableInput(componentValue, { key, ...attr }); - - await deleteOldComponents(entry, componentValue, { - key, - componentModel, - }); - - const components = await Promise.all( - componentValue.map(updateOrCreateComponent) - ); - const componentsArr = components.map(component => ({ - kind: componentModel.globalId, - ref: component, - })); - - entry[key] = componentsArr; - await entry.save(); - } else { - validateNonRepeatableInput(componentValue, { key, ...attr }); - - await deleteOldComponents(entry, componentValue, { - key, - componentModel, - }); - - if (componentValue === null) continue; - - const component = await updateOrCreateComponent(componentValue); - entry[key] = [ - { + const componentsArr = components.map(component => ({ kind: componentModel.globalId, ref: component, - }, - ]; + })); + + entry[key] = componentsArr; + await entry.save(); + } else { + validateNonRepeatableInput(componentValue, { key, ...attr }); + + await deleteOldComponents(entry, componentValue, { + key, + componentModel, + }); + + if (componentValue === null) continue; + + const component = await updateOrCreateComponent({ + componentUID, + value: componentValue, + }); + + entry[key] = [ + { + kind: componentModel.globalId, + ref: component, + }, + ]; + await entry.save(); + } + } + + if (type === 'dynamiczone') { + const dynamiczoneValues = values[key]; + + validateDynamiczoneInput(dynamiczoneValues, { key, ...attr }); + + await deleteDynamicZoneOldComponents(entry, dynamiczoneValues, { + key, + }); + + const dynamiczones = await Promise.all( + dynamiczoneValues.map(value => { + const componentUID = value.__component; + return updateOrCreateComponent({ componentUID, value }).then( + entity => { + return { + componentUID, + entity, + }; + } + ); + }) + ); + + const componentsArr = dynamiczones.map(({ componentUID, entity }) => { + const componentModel = strapi.components[componentUID]; + + return { + kind: componentModel.globalId, + ref: entity, + }; + }); + + entry[key] = componentsArr; await entry.save(); } } return; } + async function deleteDynamicZoneOldComponents(entry, values, { key }) { + const idsToKeep = values.reduce((acc, value) => { + const component = value.__component; + const componentModel = strapi.components[component]; + if (hasPK(value, componentModel)) { + acc.push({ + id: getPK(value, componentModel).toString(), + componentUID: componentModel.uid, + }); + } + + return acc; + }, []); + + const allIds = [] + .concat(entry[key] || []) + .filter(el => el.ref) + .map(el => ({ + id: el.ref._id.toString(), + componentUID: findComponentByGlobalId(el.kind).uid, + })); + + // verify the provided ids are realted to this entity. + idsToKeep.forEach(({ id, componentUID }) => { + if ( + !allIds.find(el => el.id === id && el.componentUID === componentUID) + ) { + const err = new Error( + `Some of the provided components in ${key} are not related to the entity` + ); + err.status = 400; + throw err; + } + }); + + const idsToDelete = allIds.reduce((acc, { id, componentUID }) => { + if ( + !idsToKeep.find(el => el.id === id && el.componentUID === componentUID) + ) { + acc.push({ + id, + componentUID, + }); + } + return acc; + }, []); + + if (idsToDelete.length > 0) { + const deleteMap = idsToDelete.reduce((map, { id, componentUID }) => { + if (!_.has(map, componentUID)) { + map[componentUID] = [id]; + return map; + } + + map[componentUID].push(id); + return map; + }, {}); + + await Promise.all( + Object.keys(deleteMap).map(componentUID => { + return strapi + .query(componentUID) + .delete({ [`${model.primaryKey}_in`]: deleteMap[componentUID] }); + }) + ); + } + } + async function deleteOldComponents( entry, componentValue, @@ -161,8 +326,12 @@ module.exports = ({ model, modelKey, strapi }) => { ? componentValue : [componentValue]; - const idsToKeep = componentArr.filter(hasPK).map(getPK); - const allIds = await (entry[key] || []) + const idsToKeep = componentArr + .filter(val => hasPK(val, componentModel)) + .map(val => getPK(val, componentModel)); + + const allIds = [] + .concat(entry[key] || []) .filter(el => el.ref) .map(el => el.ref._id); @@ -194,14 +363,45 @@ module.exports = ({ model, modelKey, strapi }) => { for (let key of componentKeys) { const attr = model.attributes[key]; - const { component } = attr; - const componentModel = strapi.components[component]; + const { type } = attr; - if (Array.isArray(entry[key]) && entry[key].length > 0) { - const idsToDelete = entry[key].map(el => el.ref); - await strapi - .query(componentModel.uid) - .delete({ [`${model.primaryKey}_in`]: idsToDelete }); + if (type === 'component') { + const { component } = attr; + const componentModel = strapi.components[component]; + + if (Array.isArray(entry[key]) && entry[key].length > 0) { + const idsToDelete = entry[key].map(el => el.ref); + await strapi + .query(componentModel.uid) + .delete({ [`${model.primaryKey}_in`]: idsToDelete }); + } + } + + if (type === 'dynamiczone') { + if (Array.isArray(entry[key]) && entry[key].length > 0) { + const idsToDelete = entry[key].map(el => ({ + componentUID: findComponentByGlobalId(el.kind).uid, + id: el.ref, + })); + + const deleteMap = idsToDelete.reduce((map, { id, componentUID }) => { + if (!_.has(map, componentUID)) { + map[componentUID] = [id]; + return map; + } + + map[componentUID].push(id); + return map; + }, {}); + + await Promise.all( + Object.keys(deleteMap).map(componentUID => { + return strapi.query(componentUID).delete({ + [`${model.primaryKey}_in`]: idsToDelete[componentUID], + }); + }) + ); + } } } } @@ -221,7 +421,7 @@ module.exports = ({ model, modelKey, strapi }) => { } async function findOne(params, populate) { - const primaryKey = getPK(params); + const primaryKey = getPK(params, model); if (primaryKey) { params = { @@ -257,13 +457,13 @@ module.exports = ({ model, modelKey, strapi }) => { // Create relational data and return the entry. return model.updateRelations({ - [model.primaryKey]: getPK(entry), + [model.primaryKey]: getPK(entry, model), values: relations, }); } async function update(params, values) { - const primaryKey = getPK(params); + const primaryKey = getPK(params, model); if (primaryKey) { params = { @@ -293,7 +493,7 @@ module.exports = ({ model, modelKey, strapi }) => { } async function deleteMany(params) { - const primaryKey = getPK(params); + const primaryKey = getPK(params, model); if (primaryKey) return deleteOne(params); @@ -303,7 +503,7 @@ module.exports = ({ model, modelKey, strapi }) => { async function deleteOne(params) { const entry = await model - .findOneAndRemove({ [model.primaryKey]: getPK(params) }) + .findOneAndRemove({ [model.primaryKey]: getPK(params, model) }) .populate(defaultPopulate); if (!entry) { @@ -454,3 +654,56 @@ function validateNonRepeatableInput(value, { key, required }) { throw err; } } + +function validateDynamiczoneInput( + value, + { key, min, max, components, required } +) { + if (!Array.isArray(value)) { + const err = new Error(`Dynamiczone ${key} is invalid. Expected an array`); + err.status = 400; + throw err; + } + + value.forEach(val => { + if (typeof val !== 'object' || Array.isArray(val) || val === null) { + const err = new Error( + `Dynamiczone ${key} has invalid items. Expected each items to be objects` + ); + err.status = 400; + throw err; + } + + if (!_.has(val, '__component')) { + const err = new Error( + `Dynamiczone ${key} has invalid items. Expected each items to have a valid __component key` + ); + err.status = 400; + throw err; + } else if (!components.includes(val.__component)) { + const err = new Error( + `Dynamiczone ${key} has invalid items. Each item must have a __component key that is present in the attribute definition` + ); + err.status = 400; + throw err; + } + }); + + if ( + (required === true || (required !== true && value.length > 0)) && + (min && value.length < min) + ) { + const err = new Error( + `Dynamiczone ${key} must contain at least ${min} items` + ); + err.status = 400; + throw err; + } + if (max && value.length > max) { + const err = new Error( + `Dynamiczone ${key} must contain at most ${max} items` + ); + err.status = 400; + throw err; + } +} diff --git a/packages/strapi-connector-mongoose/lib/utils/helpers.js b/packages/strapi-connector-mongoose/lib/utils/helpers.js new file mode 100644 index 0000000000..1313c770cd --- /dev/null +++ b/packages/strapi-connector-mongoose/lib/utils/helpers.js @@ -0,0 +1,11 @@ +'use strict'; + +const findComponentByGlobalId = globalId => { + return Object.values(strapi.components).find( + compo => compo.globalId === globalId + ); +}; + +module.exports = { + findComponentByGlobalId, +}; diff --git a/packages/strapi-plugin-content-manager/config/policies/routing.js b/packages/strapi-plugin-content-manager/config/policies/routing.js index 249ff2bc77..b3558efd30 100644 --- a/packages/strapi-plugin-content-manager/config/policies/routing.js +++ b/packages/strapi-plugin-content-manager/config/policies/routing.js @@ -8,6 +8,10 @@ module.exports = async (ctx, next) => { const ct = strapi.contentTypes[model]; + if (!ct) { + return ctx.send({ error: 'contentType.notFound' }, 404); + } + const target = ct.plugin === 'admin' ? strapi.admin : strapi.plugins[ct.plugin]; diff --git a/packages/strapi-plugin-content-manager/test/components/repeatable-not-required-min-max.test.e2e.js b/packages/strapi-plugin-content-manager/test/components/repeatable-not-required-min-max.test.e2e.js index de981ef402..2c36738767 100644 --- a/packages/strapi-plugin-content-manager/test/components/repeatable-not-required-min-max.test.e2e.js +++ b/packages/strapi-plugin-content-manager/test/components/repeatable-not-required-min-max.test.e2e.js @@ -576,6 +576,7 @@ describe.each([ }; expect(updateRes.statusCode).toBe(200); + expect(updateRes.body).toMatchObject(expectedResult); const getRes = await rq.get(`/${res.body.id}`); diff --git a/packages/strapi-plugin-content-manager/utils/upload-files.js b/packages/strapi-plugin-content-manager/utils/upload-files.js index 1ad149493f..70eeaa54f0 100644 --- a/packages/strapi-plugin-content-manager/utils/upload-files.js +++ b/packages/strapi-plugin-content-manager/utils/upload-files.js @@ -16,6 +16,7 @@ module.exports = async (entry, files, { model, source }) => { let tmpModel = entity; let modelName = model; let sourceName; + for (let i = 0; i < path.length; i++) { if (!tmpModel) return {}; const part = path[i]; @@ -35,7 +36,11 @@ module.exports = async (entry, files, { model, source }) => { tmpModel = strapi.components[attr.component]; } else if (attr.type === 'dynamiczone') { const entryIdx = path[i + 1]; // get component index - modelName = _.get(entry, [...currentPath, entryIdx]).__component; // get component type + const value = _.get(entry, [...currentPath, entryIdx]); + + if (!value) return {}; + + modelName = value.__component; // get component type tmpModel = strapi.components[modelName]; } else if (_.has(attr, 'model') || _.has(attr, 'collection')) { sourceName = attr.plugin; @@ -57,6 +62,7 @@ module.exports = async (entry, files, { model, source }) => { if (model) { const id = _.get(entry, path.concat('id')); + return uploadService.uploadToEntity( { id, model }, { [field]: files }, diff --git a/packages/strapi-plugin-upload/services/Upload.js b/packages/strapi-plugin-upload/services/Upload.js index e96ee88597..9ba7cf9636 100644 --- a/packages/strapi-plugin-upload/services/Upload.js +++ b/packages/strapi-plugin-upload/services/Upload.js @@ -80,7 +80,7 @@ module.exports = { delete file.buffer; file.provider = provider.provider; - const res = await strapi.plugins['upload'].services.upload.add(file); + const res = await this.add(file); // Remove temp file if (file.tmpPath) { diff --git a/packages/strapi-utils/lib/buildQuery.js b/packages/strapi-utils/lib/buildQuery.js index d3994c0fa0..fe03df1696 100644 --- a/packages/strapi-utils/lib/buildQuery.js +++ b/packages/strapi-utils/lib/buildQuery.js @@ -8,7 +8,9 @@ const findModelByAssoc = assoc => { }; const isAttribute = (model, field) => - _.has(model.allAttributes, field) || model.primaryKey === field; + _.has(model.allAttributes, field) || + model.primaryKey === field || + field === 'id'; /** * Returns the model, attribute name and association from a path of relation @@ -131,7 +133,11 @@ const buildQuery = ({ model, filters = {}, ...rest }) => { ? value.map(val => castValue({ type, operator, value: val })) : castValue({ type, operator, value: value }); - return { field, operator, value: castedValue }; + return { + field: field === 'id' ? model.primaryKey : field, + operator, + value: castedValue, + }; }); } diff --git a/packages/strapi/lib/core-api/utils/upload-files.js b/packages/strapi/lib/core-api/utils/upload-files.js index 1ad149493f..65810db2ad 100644 --- a/packages/strapi/lib/core-api/utils/upload-files.js +++ b/packages/strapi/lib/core-api/utils/upload-files.js @@ -16,6 +16,7 @@ module.exports = async (entry, files, { model, source }) => { let tmpModel = entity; let modelName = model; let sourceName; + for (let i = 0; i < path.length; i++) { if (!tmpModel) return {}; const part = path[i]; @@ -35,7 +36,11 @@ module.exports = async (entry, files, { model, source }) => { tmpModel = strapi.components[attr.component]; } else if (attr.type === 'dynamiczone') { const entryIdx = path[i + 1]; // get component index - modelName = _.get(entry, [...currentPath, entryIdx]).__component; // get component type + const value = _.get(entry, [...currentPath, entryIdx]); + + if (!value) return {}; + + modelName = value.__component; // get component type tmpModel = strapi.components[modelName]; } else if (_.has(attr, 'model') || _.has(attr, 'collection')) { sourceName = attr.plugin;