diff --git a/examples/getstarted/api/kitchensink/models/kitchensink.settings.json b/examples/getstarted/api/kitchensink/models/kitchensink.settings.json index 838b484213..0f0cb14fcd 100644 --- a/examples/getstarted/api/kitchensink/models/kitchensink.settings.json +++ b/examples/getstarted/api/kitchensink/models/kitchensink.settings.json @@ -97,7 +97,7 @@ "basic.simple" ] }, - "one_way_tag_edit": { + "one_way_tag": { "type": "relation", "relation": "oneToOne", "target": "application::tag.tag" @@ -106,6 +106,7 @@ "type": "relation", "relation": "oneToOne", "target": "application::tag.tag", + "private": true, "inversedBy": "one_to_one_kitchensink" }, "one_to_many_tags": { diff --git a/examples/getstarted/api/tag/models/tag.settings.json b/examples/getstarted/api/tag/models/tag.settings.json index a22d7aacf6..1dbf320e9d 100644 --- a/examples/getstarted/api/tag/models/tag.settings.json +++ b/examples/getstarted/api/tag/models/tag.settings.json @@ -2,7 +2,8 @@ "kind": "collectionType", "collectionName": "tags", "info": { - "name": "tag" + "name": "tag", + "description": "" }, "options": { "draftAndPublish": true @@ -12,29 +13,29 @@ "name": { "type": "string" }, - "one_to_one_kitchensink": { - "type": "relation", - "target": "application::kitchensink.kitchensink", - "relation": "oneToOne", - "mappedBy": "one_to_one_tag" - }, "many_to_one_kitchensink": { "type": "relation", - "target": "application::kitchensink.kitchensink", "relation": "manyToOne", + "target": "application::kitchensink.kitchensink", "inversedBy": "one_to_many_tags" }, "one_to_many_kitchensinks": { "type": "relation", - "target": "application::kitchensink.kitchensink", "relation": "oneToMany", + "target": "application::kitchensink.kitchensink", "mappedBy": "many_to_one_tag" }, "many_to_many_kitchensinks": { "type": "relation", - "target": "application::kitchensink.kitchensink", "relation": "manyToMany", + "target": "application::kitchensink.kitchensink", "mappedBy": "many_to_many_tags" + }, + "one_to_one_kitchensink": { + "type": "relation", + "relation": "oneToOne", + "target": "application::kitchensink.kitchensink", + "mappedBy": "one_to_one_tag" } } } diff --git a/jsconfig.json b/jsconfig.json index 6ff4f75dac..64ef4dc2b4 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -4,5 +4,5 @@ "target": "es6" }, "include": ["packages/**/*"], - "exclude": ["node_modules", "**/node_modules/*"] + "exclude": ["**/node_modules/*"] } diff --git a/packages/core/content-manager/controllers/collection-types.js b/packages/core/content-manager/controllers/collection-types.js index 1cbf005cc0..094de2548b 100644 --- a/packages/core/content-manager/controllers/collection-types.js +++ b/packages/core/content-manager/controllers/collection-types.js @@ -1,6 +1,6 @@ 'use strict'; -const { has, pipe, prop, pick } = require('lodash/fp'); +const { pipe, prop, pick } = require('lodash/fp'); const { MANY_RELATIONS } = require('@strapi/utils').relations.constants; const { setCreatorFields } = require('@strapi/utils'); diff --git a/packages/core/content-manager/services/__tests__/entity-manager.test.js b/packages/core/content-manager/services/__tests__/entity-manager.test.js index 55954a3605..0a34f6f4d5 100644 --- a/packages/core/content-manager/services/__tests__/entity-manager.test.js +++ b/packages/core/content-manager/services/__tests__/entity-manager.test.js @@ -34,7 +34,7 @@ describe('Content-Manager', () => { expect(strapi.entityService.update).toBeCalledWith(uid, entity.id, { data: { published_at: expect.any(Date) }, params: { - populate: [], + populate: {}, }, }); }); @@ -63,7 +63,7 @@ describe('Content-Manager', () => { expect(strapi.entityService.update).toHaveBeenCalledWith(uid, entity.id, { data: { published_at: null }, params: { - populate: [], + populate: {}, }, }); }); diff --git a/packages/core/content-manager/services/entity-manager.js b/packages/core/content-manager/services/entity-manager.js index a2ff0e0fd7..f333120dbd 100644 --- a/packages/core/content-manager/services/entity-manager.js +++ b/packages/core/content-manager/services/entity-manager.js @@ -61,6 +61,10 @@ const getDeepPopulate = (uid, populate, depth = 0) => { }; } + if (attribute.type === 'media') { + populateAcc[attributeName] = true; + } + if (attribute.type === 'dynamiczone') { populateAcc[attributeName] = { populate: (attribute.components || []).reduce((acc, componentUID) => { @@ -82,7 +86,9 @@ const getBasePopulate = (uid, populate) => { const { attributes } = strapi.getModel(uid); return Object.keys(attributes).filter(attributeName => { - return ['relation', 'component', 'dynamiczone'].includes(attributes[attributeName].type); + return ['relation', 'component', 'dynamiczone', 'media'].includes( + attributes[attributeName].type + ); }); }; @@ -121,7 +127,7 @@ module.exports = ({ strapi }) => ({ }, async findOne(id, uid, populate) { - const params = { populate: getBasePopulate(uid, populate) }; + const params = { populate: getDeepPopulate(uid, populate) }; return strapi.entityService.findOne(uid, id, { params }); }, @@ -144,7 +150,7 @@ module.exports = ({ strapi }) => ({ publishData[PUBLISHED_AT_ATTRIBUTE] = null; } - const params = { populate: getBasePopulate(uid) }; + const params = { populate: getDeepPopulate(uid) }; return strapi.entityService.create(uid, { params, data: publishData }); }, @@ -152,13 +158,13 @@ module.exports = ({ strapi }) => ({ update(entity, body, uid) { const publishData = omitPublishedAtField(body); - const params = { populate: getBasePopulate(uid) }; + const params = { populate: getDeepPopulate(uid) }; return strapi.entityService.update(uid, entity.id, { params, data: publishData }); }, delete(entity, uid) { - const params = { populate: getBasePopulate(uid) }; + const params = { populate: getDeepPopulate(uid) }; return strapi.entityService.delete(uid, entity.id, { params }); }, @@ -180,7 +186,7 @@ module.exports = ({ strapi }) => ({ const data = { [PUBLISHED_AT_ATTRIBUTE]: new Date() }; - const params = { populate: getBasePopulate(uid) }; + const params = { populate: getDeepPopulate(uid) }; return strapi.entityService.update(uid, entity.id, { params, data }); }), @@ -192,7 +198,7 @@ module.exports = ({ strapi }) => ({ const data = { [PUBLISHED_AT_ATTRIBUTE]: null }; - const params = { populate: getBasePopulate(uid) }; + const params = { populate: getDeepPopulate(uid) }; return strapi.entityService.update(uid, entity.id, { params, data }); }), diff --git a/packages/core/content-manager/tests/api/basic-compo-repeatable.test.e2e.js b/packages/core/content-manager/tests/api/basic-compo-repeatable.test.e2e.js index 76557a2e7b..51c71e51ef 100644 --- a/packages/core/content-manager/tests/api/basic-compo-repeatable.test.e2e.js +++ b/packages/core/content-manager/tests/api/basic-compo-repeatable.test.e2e.js @@ -90,14 +90,12 @@ describe('CM API - Basic + compo', () => { test('Read product with compo', async () => { const res = await rq({ method: 'GET', - url: '/content-manager/collection-types/application::product-with-compo.product-with-compo', + url: `/content-manager/collection-types/application::product-with-compo.product-with-compo/${data.productsWithCompo[0].id}`, }); expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.results)).toBe(true); - expect(res.body.results).toHaveLength(1); - expect(res.body.results[0]).toMatchObject(data.productsWithCompo[0]); - res.body.results.forEach(p => expect(p.published_at).toBeUndefined()); + expect(res.body).toMatchObject(data.productsWithCompo[0]); + expect(res.body.published_at).toBeUndefined(); }); test('Update product with compo', async () => { diff --git a/packages/core/content-manager/tests/api/basic-compo.test.e2e.js b/packages/core/content-manager/tests/api/basic-compo.test.e2e.js index 36394bb5ad..be2f135b36 100644 --- a/packages/core/content-manager/tests/api/basic-compo.test.e2e.js +++ b/packages/core/content-manager/tests/api/basic-compo.test.e2e.js @@ -87,14 +87,12 @@ describe('CM API - Basic + compo', () => { test('Read product with compo', async () => { const res = await rq({ method: 'GET', - url: '/content-manager/collection-types/application::product-with-compo.product-with-compo', + url: `/content-manager/collection-types/application::product-with-compo.product-with-compo/${data.productsWithCompo[0].id}`, }); expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.results)).toBe(true); - expect(res.body.results).toHaveLength(1); - expect(res.body.results[0]).toMatchObject(data.productsWithCompo[0]); - res.body.results.forEach(p => expect(p.published_at).toBeUndefined()); + expect(res.body).toMatchObject(data.productsWithCompo[0]); + expect(res.body.published_at).toBeUndefined(); }); test('Update product with compo', async () => { diff --git a/packages/core/content-manager/tests/api/basic-dp-compo-repeatable.test.e2e.js b/packages/core/content-manager/tests/api/basic-dp-compo-repeatable.test.e2e.js index 84953236a8..6102dfbd28 100644 --- a/packages/core/content-manager/tests/api/basic-dp-compo-repeatable.test.e2e.js +++ b/packages/core/content-manager/tests/api/basic-dp-compo-repeatable.test.e2e.js @@ -94,17 +94,12 @@ describe('CM API - Basic + compo + draftAndPublish', () => { test('Read product with compo', async () => { const res = await rq({ method: 'GET', - url: - '/content-manager/collection-types/application::product-with-compo-and-dp.product-with-compo-and-dp', + url: `/content-manager/collection-types/application::product-with-compo-and-dp.product-with-compo-and-dp/${data.productsWithCompoAndDP[0].id}`, }); expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.results)).toBe(true); - expect(res.body.results).toHaveLength(1); - expect(res.body.results[0]).toMatchObject(data.productsWithCompoAndDP[0]); - res.body.results.forEach(p => { - expect(p.published_at).toBeNull(); - }); + expect(res.body).toMatchObject(data.productsWithCompoAndDP[0]); + expect(res.body.published_at).toBeNull(); }); test('Update product with compo', async () => { diff --git a/packages/core/content-manager/tests/api/basic-dp-compo.test.e2e.js b/packages/core/content-manager/tests/api/basic-dp-compo.test.e2e.js index ae4fb9efad..8a25391622 100644 --- a/packages/core/content-manager/tests/api/basic-dp-compo.test.e2e.js +++ b/packages/core/content-manager/tests/api/basic-dp-compo.test.e2e.js @@ -91,17 +91,12 @@ describe('CM API - Basic + compo + draftAndPublish', () => { test('Read product with compo', async () => { const res = await rq({ method: 'GET', - url: - '/content-manager/collection-types/application::product-with-compo-and-dp.product-with-compo-and-dp', + url: `/content-manager/collection-types/application::product-with-compo-and-dp.product-with-compo-and-dp/${data.productsWithCompoAndDP[0].id}`, }); expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.results)).toBe(true); - expect(res.body.results).toHaveLength(1); - expect(res.body.results[0]).toMatchObject(data.productsWithCompoAndDP[0]); - res.body.results.forEach(p => { - expect(p.published_at).toBeNull(); - }); + expect(res.body).toMatchObject(data.productsWithCompoAndDP[0]); + expect(res.body.published_at).toBeNull(); }); test('Update product with compo', async () => { diff --git a/packages/core/content-manager/tests/api/basic-dp-dz.test.e2e.js b/packages/core/content-manager/tests/api/basic-dp-dz.test.e2e.js index ea4ffafe86..0f7e9494f7 100644 --- a/packages/core/content-manager/tests/api/basic-dp-dz.test.e2e.js +++ b/packages/core/content-manager/tests/api/basic-dp-dz.test.e2e.js @@ -94,17 +94,12 @@ describe('CM API - Basic + dz + draftAndPublish', () => { test('Read product with compo', async () => { const res = await rq({ method: 'GET', - url: - '/content-manager/collection-types/application::product-with-dz-and-dp.product-with-dz-and-dp', + url: `/content-manager/collection-types/application::product-with-dz-and-dp.product-with-dz-and-dp/${data.productsWithDzAndDP[0].id}`, }); expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.results)).toBe(true); - expect(res.body.results).toHaveLength(1); - expect(res.body.results[0]).toMatchObject(data.productsWithDzAndDP[0]); - res.body.results.forEach(p => { - expect(p.published_at).toBeNull(); - }); + expect(res.body).toMatchObject(data.productsWithDzAndDP[0]); + expect(res.body.published_at).toBeNull(); }); test('Update product with compo', async () => { diff --git a/packages/core/content-manager/tests/api/basic-dz.test.e2e.js b/packages/core/content-manager/tests/api/basic-dz.test.e2e.js index 1c041dd7d8..71a8d9a17b 100644 --- a/packages/core/content-manager/tests/api/basic-dz.test.e2e.js +++ b/packages/core/content-manager/tests/api/basic-dz.test.e2e.js @@ -90,14 +90,12 @@ describe('Core API - Basic + dz', () => { test('Read product with compo', async () => { const res = await rq({ method: 'GET', - url: '/content-manager/collection-types/application::product-with-dz.product-with-dz', + url: `/content-manager/collection-types/application::product-with-dz.product-with-dz/${data.productsWithDz[0].id}`, }); expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.results)).toBe(true); - expect(res.body.results).toHaveLength(1); - expect(res.body.results[0]).toMatchObject(data.productsWithDz[0]); - res.body.results.forEach(p => expect(p.published_at).toBeUndefined()); + expect(res.body).toMatchObject(data.productsWithDz[0]); + expect(res.body.published_at).toBeUndefined(); }); test('Update product with compo', async () => { diff --git a/packages/core/content-type-builder/services/schema-builder/content-type-builder.js b/packages/core/content-type-builder/services/schema-builder/content-type-builder.js index e2f61e44a1..9794b7f548 100644 --- a/packages/core/content-type-builder/services/schema-builder/content-type-builder.js +++ b/packages/core/content-type-builder/services/schema-builder/content-type-builder.js @@ -9,9 +9,28 @@ const { isRelation, isConfigurable } = require('../../utils/attributes'); const { typeKinds } = require('../constants'); const createSchemaHandler = require('./schema-handler'); +const reuseUnsetPreviousProperties = (newAttribute, oldAttribute) => { + _.defaults( + newAttribute, + _.omit(oldAttribute, [ + 'configurable', + 'required', + 'private', + 'unique', + 'pluginOptions', + 'inversedBy', + 'mappedBy', + ]) + ); +}; + module.exports = function createComponentBuilder() { return { setRelation({ key, uid, attribute }) { + if (!_.has(attribute, 'target')) { + return; + } + const targetCT = this.contentTypes.get(attribute.target); const targetAttribute = targetCT.getAttribute(attribute.targetAttribute); @@ -31,6 +50,10 @@ module.exports = function createComponentBuilder() { }, unsetRelation(attribute) { + if (!_.has(attribute, 'target')) { + return; + } + const targetCT = this.contentTypes.get(attribute.target); const targetAttributeName = attribute.inversedBy || attribute.mappedBy; @@ -38,11 +61,6 @@ module.exports = function createComponentBuilder() { if (!targetAttribute) return; - // TODO: do not delete polymorphic relations - // if (false) { - // return; - // } - return targetCT.deleteAttribute(targetAttributeName); }, @@ -156,17 +174,21 @@ module.exports = function createComponentBuilder() { if (isRelation(oldAttribute) && isRelation(newAttribute)) { const oldTargetAttributeName = oldAttribute.inversedBy || oldAttribute.mappedBy; - if ( - !_.isNil(oldTargetAttributeName) && - oldTargetAttributeName !== newAttribute.targetAttribute - ) { + const sameRelation = oldAttribute.relation === newAttribute.relation; + const targetAttributeHasChanged = oldTargetAttributeName !== newAttribute.targetAttribute; + + if (!sameRelation || targetAttributeHasChanged) { this.unsetRelation(oldAttribute); } - // TODO: handle edition to keep the direction - // keep extra options that were set manually on oldAttribute - _.defaults(newAttribute, oldAttribute); + reuseUnsetPreviousProperties(newAttribute, oldAttribute); + + if (oldAttribute.inversedBy) { + newAttribute.dominant = true; + } else if (oldAttribute.mappedBy) { + newAttribute.dominant = false; + } return this.setRelation({ key, @@ -239,7 +261,12 @@ const generateRelation = ({ key, attribute, uid, targetAttribute = {} }) => { switch (attribute.relation) { case 'oneToOne': { opts.relation = 'oneToOne'; - opts.mappedBy = key; + + if (attribute.dominant) { + opts.mappedBy = key; + } else { + opts.inversedBy = key; + } break; } case 'oneToMany': { @@ -254,12 +281,25 @@ const generateRelation = ({ key, attribute, uid, targetAttribute = {} }) => { } case 'manyToMany': { opts.relation = 'manyToMany'; - opts.mappedBy = key; + + if (attribute.dominant) { + opts.mappedBy = key; + } else { + opts.inversedBy = key; + } break; } default: } - return opts; + // we do this just to make sure we have the same key order when writing to files + const { type, relation, target, ...restOptions } = opts; + + return { + type, + relation, + target, + ...restOptions, + }; }; diff --git a/packages/core/content-type-builder/services/schema-builder/index.js b/packages/core/content-type-builder/services/schema-builder/index.js index 1c3b2648fc..608943d165 100644 --- a/packages/core/content-type-builder/services/schema-builder/index.js +++ b/packages/core/content-type-builder/services/schema-builder/index.js @@ -93,45 +93,51 @@ function createSchemaBuilder({ components, contentTypes }) { return Object.keys(attributes).reduce((acc, key) => { const attribute = attributes[key]; - const { configurable } = attribute; + const { configurable, private: isPrivate } = attribute; + + const baseProperties = { + private: isPrivate === true ? true : undefined, + configurable: configurable === false ? false : undefined, + }; if (attribute.type === 'relation') { - const { target, relation, targetAttribute, private: isPrivate } = attribute; + const { target, relation, targetAttribute, dominant, ...restOfProperties } = attribute; const attr = { type: 'relation', relation, target, - configurable: configurable === false ? false : undefined, - private: isPrivate === true ? true : undefined, + ...restOfProperties, + ...baseProperties, }; + acc[key] = attr; + if (target && !this.contentTypes.has(target)) { throw new Error(`target: ${target} does not exist`); } - // FIXME: this will create inversion of inversedBy & mappedBy fields - if ( - ['oneToOne', 'manyToOne', 'manyToMany'].includes(relation) && - !_.isNil(targetAttribute) - ) { + if (_.isNil(targetAttribute)) { + return acc; + } + + if (['oneToOne', 'manyToMany'].includes(relation) && dominant === true) { attr.inversedBy = targetAttribute; - } else if (['oneToMany'].includes(relation) && !_.isNil(targetAttribute)) { + } else if (['oneToOne', 'manyToMany'].includes(relation) && dominant === false) { + attr.mappedBy = targetAttribute; + } else if (['oneToOne', 'manyToOne', 'manyToMany'].includes(relation)) { + attr.inversedBy = targetAttribute; + } else if (['oneToMany'].includes(relation)) { attr.mappedBy = targetAttribute; } - acc[key] = attr; return acc; } - if (_.has(attribute, 'type')) { - acc[key] = { - ...attribute, - configurable: configurable === false ? false : undefined, - }; - - return acc; - } + acc[key] = { + ...attribute, + ...baseProperties, + }; return acc; }, {}); diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager.js index e3cf4d91dd..f05c03ee31 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager.js @@ -7,6 +7,9 @@ const { createQueryBuilder } = require('./query'); const { createRepository } = require('./entity-repository'); const { isBidirectional } = require('./metadata/relations'); +const toId = value => value.id || value; +const toIds = value => _.castArray(value || []).map(toId); + // TODO: move to query layer const toRow = (metadata, data = {}) => { const { attributes } = metadata; @@ -53,13 +56,24 @@ const toRow = (metadata, data = {}) => { const value = data[attributeName]; + if (value === null) { + Object.assign(obj, { + [idColumn.name]: null, + [typeColumn.name]: null, + }); + + continue; + } + if (!_.isUndefined(value)) { if (!_.has('id', value) || !_.has(typeField, value)) { throw new Error(`Expects properties ${typeField} an id to make a morph association`); } - obj[idColumn.name] = value.id; - obj[typeColumn.name] = value[typeField]; + Object.assign(obj, { + [idColumn.name]: value.id, + [typeColumn.name]: value[typeField], + }); } } } @@ -278,15 +292,15 @@ const createEntityManager = db => { await this.createQueryBuilder(target) .update({ [idColumn.name]: id, [typeColumn.name]: uid }) - .where({ id: data[attributeName] }) + .where({ id: toId(data[attributeName]) }) .execute(); - } else if (targetAttribute.type === 'morphToMany') { + } else if (targetAttribute.relation === 'morphToMany') { const { joinTable } = targetAttribute; const { joinColumn, morphColumn } = joinTable; const { idColumn, typeColumn } = morphColumn; - const rows = _.castArray(data[attributeName]).map((dataID, idx) => ({ + const rows = toIds(data[attributeName]).map((dataID, idx) => ({ [joinColumn.name]: dataID, [idColumn.name]: id, [typeColumn.name]: uid, @@ -313,7 +327,7 @@ const createEntityManager = db => { const { idColumn, typeColumn, typeField = '__type' } = morphColumn; - const rows = _.castArray(data[attributeName]).map((data, idx) => ({ + const rows = _.castArray(data[attributeName] || []).map((data, idx) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], @@ -426,17 +440,6 @@ const createEntityManager = db => { continue; } - /* - if morphOne | morphMany - clear previous: - if morphBy is morphToOne - set null - set new - - if morphBy is morphToMany - delete links - add links - */ if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { const { target, morphBy } = attribute; @@ -451,11 +454,13 @@ const createEntityManager = db => { .where({ [idColumn.name]: id, [typeColumn.name]: uid }) .execute(); - await this.createQueryBuilder(target) - .update({ [idColumn.name]: id, [typeColumn.name]: uid }) - .where({ id: data[attributeName] }) - .execute(); - } else if (targetAttribute.type === 'morphToMany') { + if (!_.isNull(data[attributeName])) { + await this.createQueryBuilder(target) + .update({ [idColumn.name]: id, [typeColumn.name]: uid }) + .where({ id: toId(data[attributeName]) }) + .execute(); + } + } else if (targetAttribute.relation === 'morphToMany') { const { joinTable } = targetAttribute; const { joinColumn, morphColumn } = joinTable; @@ -470,7 +475,7 @@ const createEntityManager = db => { }) .execute(); - const rows = _.castArray(data[attributeName]).map((dataID, idx) => ({ + const rows = toIds(data[attributeName] || []).map((dataID, idx) => ({ [joinColumn.name]: dataID, [idColumn.name]: id, [typeColumn.name]: uid, @@ -490,21 +495,11 @@ const createEntityManager = db => { continue; } - /* - if morphToOne - set new values in morph columns - */ if (attribute.relation === 'morphToOne') { - // do nothing + // handled on the entry itself + continue; } - /* - - if morphToMany - delete old links - create new links - - */ if (attribute.relation === 'morphToMany') { const { joinTable } = attribute; const { joinColumn, morphColumn } = joinTable; @@ -519,7 +514,7 @@ const createEntityManager = db => { }) .execute(); - const rows = _.castArray(data[attributeName]).map((data, idx) => ({ + const rows = _.castArray(data[attributeName] || []).map((data, idx) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], @@ -577,13 +572,13 @@ const createEntityManager = db => { if (['oneToOne', 'oneToMany'].includes(attribute.relation)) { await this.createQueryBuilder(joinTable.name) .delete() - .where({ [inverseJoinColumn.name]: _.castArray(data[attributeName]) }) + .where({ [inverseJoinColumn.name]: _.castArray(data[attributeName] || []) }) .where(joinTable.on || {}) .execute(); } if (!_.isNull(data[attributeName])) { - const insert = _.castArray(data[attributeName]).map(datum => { + const insert = _.castArray(data[attributeName] || []).map(datum => { return { [joinColumn.name]: id, [inverseJoinColumn.name]: datum, @@ -644,7 +639,7 @@ const createEntityManager = db => { .update({ [idColumn.name]: null, [typeColumn.name]: null }) .where({ [idColumn.name]: id, [typeColumn.name]: uid }) .execute(); - } else if (targetAttribute.type === 'morphToMany') { + } else if (targetAttribute.relation === 'morphToMany') { const { joinTable } = targetAttribute; const { morphColumn } = joinTable; diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index 31ee41407e..4a653932f0 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -168,7 +168,7 @@ const createManyToMany = (attributeName, attribute, meta, metadata) => { * @param {ModelMetadata} meta * @param {Metadata} metadata */ -const createMorphToOne = (attributeName, attribute, meta, metadata) => { +const createMorphToOne = (attributeName, attribute /*meta, metadata*/) => { const idColumnName = 'target_id'; const typeColumnName = 'target_type'; @@ -344,7 +344,7 @@ const createJoinColum = (metadata, { attribute, attributeName /*meta */ }) => { if (isBidirectional(attribute)) { const inverseAttribute = targetMeta.attributes[attribute.inversedBy]; - // TODO: do not invert here but invert in the query ? => means we need to use owner info in the query layer + Object.assign(inverseAttribute, { joinColumn: { name: joinColumn.referencedColumn, diff --git a/packages/core/database/lib/query/helpers.js b/packages/core/database/lib/query/helpers.js index 5291c6b0f4..162f5b1892 100644 --- a/packages/core/database/lib/query/helpers.js +++ b/packages/core/database/lib/query/helpers.js @@ -799,7 +799,7 @@ const applyPopulate = async (results, populate, ctx) => { }); continue; - } else if (attribute.relation in ['morphOne', 'morphMany']) { + } else if (['morphOne', 'morphMany'].includes(attribute.relation)) { const { target, morphBy } = attribute; const targetAttribute = db.metadata.get(target).attributes[morphBy]; @@ -849,7 +849,7 @@ const applyPopulate = async (results, populate, ctx) => { if (_.isEmpty(referencedValues)) { results.forEach(result => { - result[key] = []; + result[key] = attribute.relation === 'morphOne' ? null : []; }); continue; diff --git a/packages/core/database/lib/schema/schema.js b/packages/core/database/lib/schema/schema.js index b213cf3a89..921fb54dbf 100644 --- a/packages/core/database/lib/schema/schema.js +++ b/packages/core/database/lib/schema/schema.js @@ -56,6 +56,7 @@ const createTable = meta => { // NOTE: we could pass uniquness for oneToOne to avoid creating more than one to one const { name: columnName, referencedColumn, referencedTable } = attribute.joinColumn; + table.columns.push( createColumn(columnName, { type: 'integer', diff --git a/packages/core/database/lib/utils/content-types.js b/packages/core/database/lib/utils/content-types.js index eab6e04d50..4f5ebf5d69 100644 --- a/packages/core/database/lib/utils/content-types.js +++ b/packages/core/database/lib/utils/content-types.js @@ -6,7 +6,7 @@ const transformAttribute = attribute => { // TODO: handle a filter on field return { type: 'relation', - relation: attribute.single === true ? 'morphOne' : 'morphMany', + relation: attribute.multiple === true ? 'morphMany' : 'morphOne', target: 'plugins::upload.file', morphBy: 'related', }; diff --git a/packages/core/strapi/lib/services/entity-service.js b/packages/core/strapi/lib/services/entity-service.js index ea8ba27946..dac066d960 100644 --- a/packages/core/strapi/lib/services/entity-service.js +++ b/packages/core/strapi/lib/services/entity-service.js @@ -57,6 +57,21 @@ module.exports = ctx => { return service; }; +// TODO: remove once the front is migrated +const convertOldQuery = params => { + const obj = {}; + + Object.keys(params).forEach(key => { + if (key.startsWith('_')) { + obj[key.slice(1)] = params[key]; + } else { + obj[key] = params[key]; + } + }); + + return obj; +}; + // TODO: move to Controller ? const transformParamsToQuery = (uid, params = {}) => { const model = strapi.getModel(uid); @@ -75,10 +90,13 @@ const transformParamsToQuery = (uid, params = {}) => { fields, populate, publicationState, + _q, + _where, + ...rest } = params; - if (params._q) { - query._q = params._q; + if (_q) { + query._q = _q; } if (page) { @@ -105,6 +123,12 @@ const transformParamsToQuery = (uid, params = {}) => { query.where = filters; } + if (_where) { + query.where = { + $and: [_where].concat(query.where || []), + }; + } + if (fields) { query.select = _.castArray(fields); } @@ -133,7 +157,12 @@ const transformParamsToQuery = (uid, params = {}) => { } } - return query; + const finalQuery = { + ...convertOldQuery(rest), + ...query, + }; + + return finalQuery; }; const pickSelectionParams = pick(['fields', 'populate']); diff --git a/packages/plugins/i18n/server/services/entity-service-decorator.js b/packages/plugins/i18n/server/services/entity-service-decorator.js index 65b04a3e7a..5c6ec7d72f 100644 --- a/packages/plugins/i18n/server/services/entity-service-decorator.js +++ b/packages/plugins/i18n/server/services/entity-service-decorator.js @@ -35,6 +35,20 @@ const wrapParams = async (params = {}, ctx = {}) => { }; } + // TODO: remove when the _locale is renamed to locale + if (has('_locale', params)) { + if (params['_locale'] === 'all') { + return omit('_locale', params); + } + + return { + ...omit('_locale', params), + filters: { + $and: [{ locale: params['_locale'] }].concat(params.filters || []), + }, + }; + } + const entityDefinedById = paramsContain('id', params) && SINGLE_ENTRY_ACTIONS.includes(action); const entitiesDefinedByIds = paramsContain('id.$in', params) && BULK_ACTIONS.includes(action); diff --git a/packages/plugins/users-permissions/config/routes.json b/packages/plugins/users-permissions/config/routes.json index d2eba6275d..1f3d2ec18d 100644 --- a/packages/plugins/users-permissions/config/routes.json +++ b/packages/plugins/users-permissions/config/routes.json @@ -174,7 +174,7 @@ { "method": "GET", "path": "/connect/*", - "handler": "Auth.connect", + "handler": "auth.connect", "config": { "policies": ["plugins::users-permissions.ratelimit"], "prefix": "", @@ -188,7 +188,7 @@ { "method": "POST", "path": "/auth/local", - "handler": "Auth.callback", + "handler": "auth.callback", "config": { "policies": ["plugins::users-permissions.ratelimit"], "prefix": "", @@ -202,7 +202,7 @@ { "method": "POST", "path": "/auth/local/register", - "handler": "Auth.register", + "handler": "auth.register", "config": { "policies": ["plugins::users-permissions.ratelimit"], "prefix": "", @@ -217,7 +217,7 @@ { "method": "GET", "path": "/auth/:provider/callback", - "handler": "Auth.callback", + "handler": "auth.callback", "config": { "policies": [], "prefix": "", @@ -231,7 +231,7 @@ { "method": "POST", "path": "/auth/forgot-password", - "handler": "Auth.forgotPassword", + "handler": "auth.forgotPassword", "config": { "policies": ["plugins::users-permissions.ratelimit"], "prefix": "", @@ -245,7 +245,7 @@ { "method": "POST", "path": "/auth/reset-password", - "handler": "Auth.resetPassword", + "handler": "auth.resetPassword", "config": { "policies": ["plugins::users-permissions.ratelimit"], "prefix": "", @@ -259,7 +259,7 @@ { "method": "GET", "path": "/auth/email-confirmation", - "handler": "Auth.emailConfirmation", + "handler": "auth.emailConfirmation", "config": { "policies": [], "prefix": "", @@ -273,7 +273,7 @@ { "method": "POST", "path": "/auth/send-email-confirmation", - "handler": "Auth.sendEmailConfirmation", + "handler": "auth.sendEmailConfirmation", "config": { "policies": [], "prefix": "", @@ -287,7 +287,7 @@ { "method": "GET", "path": "/users/count", - "handler": "User.count", + "handler": "user.count", "config": { "prefix": "", "policies": [] @@ -296,7 +296,7 @@ { "method": "GET", "path": "/users", - "handler": "User.find", + "handler": "user.find", "config": { "policies": [], "prefix": "", @@ -311,7 +311,7 @@ { "method": "GET", "path": "/users/me", - "handler": "User.me", + "handler": "user.me", "config": { "policies": [], "prefix": "", @@ -326,7 +326,7 @@ { "method": "GET", "path": "/users/:id", - "handler": "User.findOne", + "handler": "user.findOne", "config": { "policies": [], "prefix": "", @@ -341,7 +341,7 @@ { "method": "POST", "path": "/users", - "handler": "User.create", + "handler": "user.create", "config": { "policies": [], "prefix": "" @@ -350,7 +350,7 @@ { "method": "PUT", "path": "/users/:id", - "handler": "User.update", + "handler": "user.update", "config": { "policies": [], "prefix": "", @@ -365,7 +365,7 @@ { "method": "DELETE", "path": "/users/:id", - "handler": "User.destroy", + "handler": "user.destroy", "config": { "policies": [], "prefix": "", diff --git a/packages/plugins/users-permissions/services/user.js b/packages/plugins/users-permissions/services/user.js index c22d7ca356..0ddc26c63d 100644 --- a/packages/plugins/users-permissions/services/user.js +++ b/packages/plugins/users-permissions/services/user.js @@ -35,12 +35,12 @@ module.exports = ({ strapi }) => ({ */ async add(values) { if (values.password) { - values.password = await strapi.plugins['users-permissions'].services.user.hashPassword( - values - ); + values.password = await getService('user').hashPassword(values); } - return strapi.query('plugins::users-permissions.user').create({ data: values }); + return strapi + .query('plugins::users-permissions.user') + .create({ data: values, populate: ['role'] }); }, /** @@ -52,7 +52,9 @@ module.exports = ({ strapi }) => ({ values.password = await getService('user').hashPassword(values); } - return strapi.query('plugins::users-permissions.user').update({ where: params, data: values }); + return strapi + .query('plugins::users-permissions.user') + .update({ where: params, data: values, populate: ['role'] }); }, /** @@ -121,7 +123,7 @@ module.exports = ({ strapi }) => ({ }, async sendConfirmationEmail(user) { - const userPermissionService = strapi.plugins['users-permissions'].services.userspermissions; + const userPermissionService = getService('users-permissions'); const pluginStore = await strapi.store({ environment: '', type: 'plugin', diff --git a/packages/plugins/users-permissions/services/users-permissions.js b/packages/plugins/users-permissions/services/users-permissions.js index 2576e81272..a0b189e89a 100644 --- a/packages/plugins/users-permissions/services/users-permissions.js +++ b/packages/plugins/users-permissions/services/users-permissions.js @@ -10,6 +10,7 @@ const DEFAULT_PERMISSIONS = [ { action: 'callback', controller: 'auth', type: 'users-permissions', roleType: 'public' }, { action: 'connect', controller: 'auth', type: 'users-permissions', roleType: null }, { action: 'forgotpassword', controller: 'auth', type: 'users-permissions', roleType: 'public' }, + { action: 'resetpassword', controller: 'auth', type: 'users-permissions', roleType: 'public' }, { action: 'register', controller: 'auth', type: 'users-permissions', roleType: 'public' }, { action: 'emailconfirmation', @@ -17,20 +18,18 @@ const DEFAULT_PERMISSIONS = [ type: 'users-permissions', roleType: 'public', }, - { action: 'resetpassword', controller: 'auth', type: 'users-permissions', roleType: 'public' }, - { action: 'init', controller: 'userspermissions', type: null, roleType: null }, { action: 'me', controller: 'user', type: 'users-permissions', roleType: null }, - { action: 'autoreload', controller: null, type: null, roleType: null }, ]; -const isPermissionEnabled = (permission, role) => - DEFAULT_PERMISSIONS.some( +const isEnabledByDefault = (permission, role) => { + return DEFAULT_PERMISSIONS.some( defaultPerm => (defaultPerm.action === null || permission.action === defaultPerm.action) && (defaultPerm.controller === null || permission.controller === defaultPerm.controller) && (defaultPerm.type === null || permission.type === defaultPerm.type) && (defaultPerm.roleType === null || role.type === defaultPerm.roleType) ); +}; module.exports = ({ strapi }) => ({ async createRole(params) { @@ -254,7 +253,7 @@ module.exports = ({ strapi }) => ({ async updatePermissions() { const roles = await strapi.query('plugins::users-permissions.role').findMany(); - const rolesMap = _.groupBy(roles, 'id'); + const rolesMap = _.keyBy(roles, 'id'); const dbPermissions = await strapi .query('plugins::users-permissions.permission') @@ -319,18 +318,18 @@ module.exports = ({ strapi }) => ({ // Execute request to update entries in database for each role. await Promise.all( - toAdd.map(permission => - query.create({ + toAdd.map(permission => { + return query.create({ data: { type: permission.type, controller: permission.controller, action: permission.action, - enabled: isPermissionEnabled(permission, rolesMap[permission.roleId]), + enabled: isEnabledByDefault(permission, rolesMap[permission.roleId]), policy: '', role: permission.roleId, }, - }) - ) + }); + }) ); await Promise.all(