diff --git a/packages/core/content-manager/tests/dynamiczones/simple.test.e2e.js b/packages/core/content-manager/tests/dynamiczones/simple.test.e2e.js index dfb9f0c781..50963b0904 100644 --- a/packages/core/content-manager/tests/dynamiczones/simple.test.e2e.js +++ b/packages/core/content-manager/tests/dynamiczones/simple.test.e2e.js @@ -118,6 +118,9 @@ describe.each([ }, ], }, + qs: { + populate: ['field'], + }, }); expect(res.statusCode).toBe(200); @@ -148,6 +151,9 @@ describe.each([ body: { field: [], }, + qs: { + populate: ['field'], + }, }); expect(res.statusCode).toBe(200); @@ -193,7 +199,13 @@ describe.each([ const createRes = await createEntry(); const entryId = createRes.body.id; - const res = await rq({ method: 'GET', url: `/${entryId}` }); + const res = await rq({ + method: 'GET', + url: `/${entryId}`, + qs: { + populate: ['field'], + }, + }); expect(res.statusCode).toBe(200); expect(Array.isArray(res.body.field)).toBe(true); @@ -219,7 +231,13 @@ describe.each([ describe('Listing entries', () => { test('The entries have their dynamic zones populated', async () => { - const res = await rq({ method: 'GET', url: '/' }); + const res = await rq({ + method: 'GET', + url: '/', + qs: { + populate: ['field'], + }, + }); expect(res.statusCode).toBe(200); @@ -270,6 +288,9 @@ describe.each([ body: { field: [], }, + qs: { + populate: ['field'], + }, }); expect(res.statusCode).toBe(200); @@ -287,6 +308,9 @@ describe.each([ method: 'PUT', url: `/${entryId}`, body: defaultBody, + qs: { + populate: ['field'], + }, }); expect(res.statusCode).toBe(200); @@ -331,6 +355,9 @@ describe.each([ }, ], }, + qs: { + populate: ['field'], + }, }); expect(res.statusCode).toBe(200); @@ -401,7 +428,13 @@ describe.each([ expect(createRes.statusCode).toBe(200); const entryId = createRes.body.id; - const res = await rq({ method: 'DELETE', url: `/${entryId}` }); + const res = await rq({ + method: 'DELETE', + url: `/${entryId}`, + qs: { + populate: ['field'], + }, + }); expect(res.statusCode).toBe(200); expect(Array.isArray(res.body.field)).toBe(true); diff --git a/packages/core/database/examples/index.js b/packages/core/database/examples/index.js index 0ed29a4e62..8635823171 100644 --- a/packages/core/database/examples/index.js +++ b/packages/core/database/examples/index.js @@ -1,5 +1,6 @@ 'use strict'; +const util = require('util'); const _ = require('lodash'); const { Database } = require('../lib/index'); @@ -16,33 +17,165 @@ async function main(connection) { // await orm.schema.drop(); // await orm.schema.create(); - console.log(orm.connection.client.config.client); + await orm.schema.reset(); - await orm.schema.sync(); - // await orm.schema.reset(); + let res, articleA, articleB, c1, c2, f1, f2; - const compoA = await orm.query('compo-test').create({ + f1 = await orm.query('folder').create({ data: {} }); + f2 = await orm.query('folder').create({ data: {} }); + + articleA = await orm.query('article').create({ data: { - key: 'A', - value: 1, + reportables: [ + { + __type: 'folder', + id: f1.id, + }, + { + __type: 'folder', + id: f2.id, + }, + ], }, }); - orm.query('article').findMany({ - populate: { - comments: { - where: {}, - populate: {}, + articleB = await orm.query('article').create({ + data: { + reportables: { + __type: 'folder', + id: f2.id, }, }, }); + res = await orm.query('folder').findMany({ + populate: { + articles: { + populate: { + reportables: true, + }, + }, + }, + }); + + log(res); + + // morph one + + await orm.query('comment').create({ + data: { + article: articleA.id, + }, + }); + + res = await orm.query('comment').findMany({ + populate: { + article: true, + }, + }); + + log(res); + + res = await orm.query('article').findMany({ + populate: { + commentable: true, + }, + }); + + log(res); + // morph many + + await orm.query('video-comment').create({ + data: { + articles: [articleA.id, articleB.id], + }, + }); + + res = await orm.query('video-comment').findMany({ + populate: { + articles: true, + }, + }); + + log(res); + + res = await orm.query('article').findMany({ + populate: { + commentable: true, + }, + }); + + log(res); + + //---------- + + c1 = await orm.query('comment').create({ + data: { + title: 'test', + }, + }); + + c2 = await orm.query('video-comment').create({ + data: { + title: 'coucou', + articles: [articleA.id, articleB.id], + }, + }); + + // morph to one + + await orm.query('article').create({ + data: { + commentable: { + __type: 'comment', + id: c1.id, + }, + }, + }); + + res = await orm.query('article').findMany({ + populate: { + commentable: true, + }, + }); + + log(res); + + // morph to many + + await orm.query('article').create({ + data: { + dz: [ + { + __type: 'comment', + id: c1.id, + }, + { + __type: 'video-comment', + id: c2.id, + }, + ], + }, + }); + + res = await orm.query('article').findMany({ + populate: { + dz: true, + }, + }); + + log(res); + // await tests(orm); } finally { orm.destroy(); } } +function log(res) { + console.log(util.inspect(res, null, null, true)); +} + // (async function() { // for (const key in connections) { // await main(connections[key]); diff --git a/packages/core/database/examples/models.js b/packages/core/database/examples/models.js index c9e968791d..2a12144bb8 100644 --- a/packages/core/database/examples/models.js +++ b/packages/core/database/examples/models.js @@ -1,4 +1,6 @@ 'use strict'; + +/* const category = { modelName: 'category', uid: 'category', @@ -25,7 +27,7 @@ const category = { }, compo: { type: 'component', - component: 'compo-test', + component: 'compo', }, }, }; @@ -53,7 +55,7 @@ const article = { // }, // compo: { // type: 'component', - // component: 'compo-test', + // component: 'compo', // // repeatable: true, // }, // cover: { @@ -85,9 +87,9 @@ const tags = { }; const compo = { - modelName: 'compoTest', - uid: 'compo-test', - collectionName: 'compo_tests', + modelName: 'compo', + uid: 'compo', + collectionName: 'compos', attributes: { key: { type: 'string', @@ -265,31 +267,75 @@ const blogPost = { }, }; -// module.exports = [category, article, tags, compo, user, address, file, fileMorph, blogPost]; +module.exports = [category, article, tags, compo, user, address, file, fileMorph, blogPost]; +*/ -const file = { - uid: 'file', - modelName: 'file', - collectionName: 'files', +const article = { + modelName: 'article', + uid: 'article', + collectionName: 'articles', attributes: { - related: { - - } + commentable: { + type: 'relation', + relation: 'morphToOne', + }, + reportables: { + type: 'relation', + relation: 'morphToMany', + }, + dz: { + type: 'dynamiczone', + components: ['comment', 'video-comment'], + }, }, }; -const post = { - uid: 'post', - modelName: 'post', - collectionName: 'posts', +const comment = { + modelName: 'comment', + uid: 'comment', + collectionName: 'comments', attributes: { - cover: { + article: { type: 'relation', - relation: 'manyToOne', - target: 'file' - // inversedBy: 'related' - } - } -} + relation: 'morphOne', + target: 'article', + morphBy: 'commentable', + }, + title: { + type: 'string', + }, + }, +}; -module.exports = [file, post]; +const videoComment = { + modelName: 'video-comment', + uid: 'video-comment', + collectionName: 'video_comments', + attributes: { + articles: { + type: 'relation', + relation: 'morphMany', + target: 'article', + morphBy: 'commentable', + }, + title: { + type: 'string', + }, + }, +}; + +const folder = { + modelName: 'folder', + uid: 'folder', + collectionName: 'folders', + attributes: { + articles: { + type: 'relation', + relation: 'morphMany', + target: 'article', + morphBy: 'reportables', + }, + }, +}; + +module.exports = [article, comment, videoComment, folder]; diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager.js index 2cb8cff131..f119fa8449 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager.js @@ -44,6 +44,23 @@ const toRow = (metadata, data = {}) => { if (!_.isUndefined(attrValue)) { obj[joinColumnName] = attrValue; } + + continue; + } + + if (attribute.morphColumn && attribute.owner) { + const { idColumn, typeColumn } = attribute.morphColumn; + + const value = data[attributeName]; + + if (!_.isUndefined(value)) { + if (!_.has('id', value) || !_.has('__type', value)) { + throw new Error('Expects properties `__type` an `id` to make a morph association'); + } + + obj[idColumn.name] = value.id; + obj[typeColumn.name] = value.__type; + } } } } @@ -241,15 +258,84 @@ const createEntityManager = db => { async attachRelations(uid, id, data) { const { attributes } = db.metadata.get(uid); - /* - TODO: - if data[attributeName] is a single value (ID) => assign - if data[attributeName] is an object with an id => assign & use the other props as join column values - */ - for (const attributeName in attributes) { const attribute = attributes[attributeName]; + if (!_.has(attributeName, data)) { + continue; + } + + // TODO: handle cleaning before creating the assocaitions + switch (attribute.relation) { + case 'morphOne': + case 'morphMany': { + const { target, morphBy } = attribute; + + const targetAttribute = db.metadata.get(target).attributes[morphBy]; + + if (targetAttribute.relation === 'morphToOne') { + // set columns + const { idColumn, typeColumn } = targetAttribute.morphColumn; + + await this.createQueryBuilder(target) + .update({ [idColumn.name]: id, [typeColumn.name]: uid }) + .where({ id: data[attributeName] }) + .execute(); + } else if (targetAttribute.type === 'morphToMany') { + const { joinTable } = targetAttribute; + const { name, joinColumn, morphColumn } = joinTable; + + const { idColumn, typeColumn } = morphColumn; + + const rows = _.castArray(data[attributeName]).map((dataID, idx) => ({ + [joinColumn.name]: dataID, + [idColumn.name]: id, + [typeColumn.name]: uid, + ...(joinTable.on || {}), + order: idx, + })); + + if (_.isEmpty(rows)) { + continue; + } + + await this.createQueryBuilder(name) + .insert(rows) + .execute(); + } + + continue; + } + case 'morphToOne': { + // handled on the entry itself + continue; + } + case 'morphToMany': { + const { joinTable } = attribute; + const { name, joinColumn, morphColumn } = joinTable; + + const { idColumn, typeColumn } = morphColumn; + + const rows = _.castArray(data[attributeName]).map((data, idx) => ({ + [joinColumn.name]: id, + [idColumn.name]: data.id, + [typeColumn.name]: data.__type, + ...(joinTable.on || {}), + order: idx, + })); + + if (_.isEmpty(rows)) { + continue; + } + + await this.createQueryBuilder(name) + .insert(rows) + .execute(); + + continue; + } + } + if (attribute.joinColumn && attribute.owner) { if ( attribute.relation === 'oneToOne' && @@ -342,8 +428,10 @@ const createEntityManager = db => { for (const attributeName in attributes) { const attribute = attributes[attributeName]; + // TODO: implement polymorphic + if (attribute.joinColumn && attribute.owner) { - // TODO: redefine + // TODO: check edgecase if (attribute.relation === 'oneToOne' && _.has(attributeName, data)) { await this.createQueryBuilder(uid) .where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } }) @@ -440,6 +528,8 @@ const createEntityManager = db => { for (const attributeName in attributes) { const attribute = attributes[attributeName]; + // TODO: implement polymorphic + // NOTE: we do not remove existing associations with the target as it should handled by unique FKs instead if (attribute.joinColumn && attribute.owner) { // nothing to do => relation already added on the table diff --git a/packages/core/database/lib/metadata/index.js b/packages/core/database/lib/metadata/index.js index 8ba462d8d2..894fff8872 100644 --- a/packages/core/database/lib/metadata/index.js +++ b/packages/core/database/lib/metadata/index.js @@ -112,19 +112,23 @@ const createMetadata = (models = []) => { Object.assign(attribute, { type: 'relation', - relation: 'oneToMany', - // NOTE: if target is an array then th erelation is polymorphic - - target: attribute.components, + relation: 'morphToMany', + // TODO: handle restrictions at some point + // target: attribute.components, joinTable: { name: meta.componentLink.tableName, joinColumn: { name: 'entity_id', referencedColumn: 'id', }, - inverseJoinColumn: { - name: 'component_id', - referencedColumn: 'id', + morphColumn: { + idColumn: { + name: 'component_id', + referencedColumn: 'id', + }, + typeColumn: { + name: 'component_type', + }, }, on: { field: attributeName, diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index b60901eccc..4110824537 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -13,41 +13,6 @@ const isBidirectional = attribute => hasInversedBy(attribute) || hasMappedBy(att const isOwner = attribute => !isBidirectional(attribute) || hasInversedBy(attribute); const shouldUseJoinTable = attribute => attribute.useJoinTable !== false; -/** - * Creates a relation metadata - * - * @param {string} attributeName - * @param {Attribute} attribute - * @param {ModelMetadata} meta - * @param {Metadata} metadata - */ -const createRelation = (attributeName, attribute, meta, metadata) => { - if (_.has(attribute.relation, relationFactoryMap)) { - return relationFactoryMap[attribute.relation](attributeName, attribute, meta, metadata); - } - - throw new Error(`Unknown relation ${attribute.relation}`); - - /* - - polymorphic relations - - OneToOneX - ManyToOneX - OnetoManyX - ManytoManyX - XOneToOne - XManyToOne - XOnetoMany - XManytoMany - - XOneToOneX - XManyToOneX - XOnetoManyX - XManytoManyX - */ -}; - /** * Creates a oneToOne relation metadata * @@ -186,11 +151,189 @@ const createManyToMany = (attributeName, attribute, meta, metadata) => { } }; +/** + * Creates a morphToOne relation metadata + * + * if with join table then + * create join table + * else + * create join columnsa + * + * if bidirectionnal + * set info in the traget + * + * + * @param {string} attributeName + * @param {Attribute} attribute + * @param {ModelMetadata} meta + * @param {Metadata} metadata + */ +const createMorphToOne = (attributeName, attribute, meta, metadata) => { + const idColumnName = 'target_id'; + const typeColumnName = 'target_type'; + + Object.assign(attribute, { + owner: true, + morphColumn: { + // TODO: add referenced column + typeColumn: { + name: typeColumnName, + }, + idColumn: { + name: idColumnName, + referencedColumn: 'id', + }, + }, + }); + + // TODO: implement bidirectional +}; + +/** + * Creates a morphToMany relation metadata + * + * @param {string} attributeName + * @param {Attribute} attribute + * @param {ModelMetadata} meta + * @param {Metadata} metadata + */ +const createMorphToMany = (attributeName, attribute, meta, metadata) => { + const joinTableName = _.snakeCase(`${meta.tableName}_${attributeName}_morphs`); + + const joinColumnName = _.snakeCase(`${meta.singularName}_id`); + const morphColumnName = _.snakeCase(`${attributeName}`); + const idColumnName = `${morphColumnName}_id`; + const typeColumnName = `${morphColumnName}_type`; + + metadata.add({ + uid: joinTableName, + tableName: joinTableName, + attributes: { + [joinColumnName]: { + type: 'integer', + column: { + unsigned: true, + }, + }, + [idColumnName]: { + type: 'integer', + column: { + unsigned: true, + }, + }, + [typeColumnName]: { + type: 'string', + }, + order: { + type: 'integer', + column: { + unsigned: true, + }, + }, + }, + foreignKeys: [ + { + name: `${joinTableName}_fk`, + columns: [joinColumnName], + referencedColumns: ['id'], + referencedTable: meta.tableName, + onDelete: 'CASCADE', + }, + ], + }); + + const joinTable = { + name: joinTableName, + joinColumn: { + name: joinColumnName, + referencedColumn: 'id', + }, + morphColumn: { + typeColumn: { + name: typeColumnName, + }, + idColumn: { + name: idColumnName, + referencedColumn: 'id', + }, + }, + }; + + attribute.joinTable = joinTable; + + // TODO: implement bidirectional +}; + +/** + * Creates a morphOne relation metadata + * + * @param {string} attributeName + * @param {Attribute} attribute + * @param {ModelMetadata} meta + * @param {Metadata} metadata + */ +const createMorphOne = (attributeName, attribute, meta, metadata) => { + const targetMeta = metadata.get(attribute.target); + + if (!targetMeta) { + throw new Error(`Morph target not found. Looking for ${attribute.target}`); + } + + if (!_.has(attribute.morphBy, targetMeta.attributes)) { + throw new Error(`Morph target attribute not found. Looking for ${attribute.morphBy}`); + } + + // TODO: why not + // Object.assign(attribute, { + // morphReference: targetMeta.attributes[attribute.morphBy], + // }); +}; + +/** + * Creates a morphMany relation metadata + * + * @param {string} attributeName + * @param {Attribute} attribute + * @param {ModelMetadata} meta + * @param {Metadata} metadata + */ +const createMorphMany = (attributeName, attribute, meta, metadata) => { + const targetMeta = metadata.get(attribute.target); + + if (!targetMeta) { + throw new Error(`Morph target not found. Looking for ${attribute.target}`); + } + + if (!_.has(attribute.morphBy, targetMeta.attributes)) { + throw new Error(`Morph target attribute not found. Looking for ${attribute.morphBy}`); + } +}; + const relationFactoryMap = { oneToOne: createOneToOne, oneToMany: createOneToMany, manyToOne: createManyToOne, manyToMany: createManyToMany, + morphToOne: createMorphToOne, + morphToMany: createMorphToMany, + morphOne: createMorphOne, + morphMany: createMorphMany, +}; + +/** + * Creates a relation metadata + * + * @param {string} attributeName + * @param {Attribute} attribute + * @param {ModelMetadata} meta + * @param {Metadata} metadata + */ +const createRelation = (attributeName, attribute, meta, metadata) => { + if (_.has(attribute.relation, relationFactoryMap)) { + return relationFactoryMap[attribute.relation](attributeName, attribute, meta, metadata); + } + + throw new Error(`Unknown relation ${attribute.relation}`); }; const createJoinColum = (metadata, { attribute, attributeName /*meta */ }) => { diff --git a/packages/core/database/lib/query/helpers.js b/packages/core/database/lib/query/helpers.js index 8c389a5fae..4057fcbb09 100644 --- a/packages/core/database/lib/query/helpers.js +++ b/packages/core/database/lib/query/helpers.js @@ -493,6 +493,7 @@ const processPopulate = (populate, ctx) => { const attribute = meta.attributes[key]; if (!attribute) { + continue; throw new Error(`Cannot populate unknown field ${key}`); } @@ -800,6 +801,227 @@ const applyPopulate = async (results, populate, ctx) => { }); continue; + } else if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { + const { target, morphBy } = attribute; + + const targetAttribute = db.metadata.get(target).attributes[morphBy]; + + if (targetAttribute.relation === 'morphToOne') { + const { idColumn, typeColumn } = targetAttribute.morphColumn; + + const referencedValues = _.uniq( + results.map(r => r[idColumn.referencedColumn]).filter(value => !_.isNull(value)) + ); + + if (_.isEmpty(referencedValues)) { + results.forEach(result => { + result[key] = null; + }); + + continue; + } + + const rows = await db.entityManager + .createQueryBuilder(target) + .init(populateValue) + // .addSelect(`${qb.alias}.${idColumn.referencedColumn}`) + .where({ [idColumn.name]: referencedValues, [typeColumn.name]: uid }) + .execute({ mapResults: false }); + + const map = _.groupBy(idColumn.name, rows); + + results.forEach(result => { + const matchingRows = map[result[idColumn.referencedColumn]]; + + const matchingValue = + attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows; + + result[key] = fromTargetRow(matchingValue); + }); + } else if (targetAttribute.relation === 'morphToMany') { + const { joinTable } = targetAttribute; + + const { joinColumn, morphColumn } = joinTable; + + const { idColumn, typeColumn } = morphColumn; + + const referencedValues = _.uniq( + results.map(r => r[idColumn.referencedColumn]).filter(value => !_.isNull(value)) + ); + + if (_.isEmpty(referencedValues)) { + results.forEach(result => { + result[key] = []; + }); + + continue; + } + + // find with join table + const qb = db.entityManager.createQueryBuilder(target); + + const alias = qb.getAlias(); + + const rows = await qb + .init(populateValue) + .join({ + alias: alias, + referencedTable: joinTable.name, + referencedColumn: joinColumn.name, + rootColumn: joinColumn.referencedColumn, + rootTable: qb.alias, + on: joinTable.on, + }) + .addSelect([`${alias}.${idColumn.name}`, `${alias}.${typeColumn.name}`]) + .where({ + [`${alias}.${idColumn.name}`]: referencedValues, + [`${alias}.${typeColumn.name}`]: uid, + }) + .execute({ mapResults: false }); + + const map = _.groupBy(idColumn.name, rows); + + results.forEach(result => { + const matchingRows = map[result[idColumn.referencedColumn]]; + + const matchingValue = + attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows; + + result[key] = fromTargetRow(matchingValue); + }); + } + + continue; + } else if (attribute.relation === 'morphToMany') { + // find with join table + const { joinTable } = attribute; + + const { joinColumn, morphColumn } = joinTable; + const { idColumn, typeColumn } = morphColumn; + + // fetch join table to create the ids map then do the same as morphToOne without the first + + const referencedValues = _.uniq( + results.map(r => r[joinColumn.referencedColumn]).filter(value => !_.isNull(value)) + ); + + const qb = db.entityManager.createQueryBuilder(joinTable.name); + + const joinRows = await qb + .where({ + [joinColumn.name]: referencedValues, + ...(joinTable.on || {}), + }) + .orderBy([joinColumn.name, 'order']) + .execute({ mapResults: false }); + + const joinMap = _.groupBy(joinColumn.name, joinRows); + + const idsByType = joinRows.reduce((acc, result) => { + const idValue = result[morphColumn.idColumn.name]; + const typeValue = result[morphColumn.typeColumn.name]; + + if (!idValue || !typeValue) { + return acc; + } + + if (!_.has(typeValue, acc)) { + acc[typeValue] = []; + } + + acc[typeValue].push(idValue); + + return acc; + }, {}); + + const map = {}; + for (const type in idsByType) { + const ids = idsByType[type]; + + const qb = db.entityManager.createQueryBuilder(type); + + const rows = await qb + .init(populateValue) + .addSelect(`${qb.alias}.${idColumn.referencedColumn}`) + .where({ [idColumn.referencedColumn]: ids }) + .execute({ mapResults: false }); + + map[type] = _.groupBy(idColumn.referencedColumn, rows); + } + + results.forEach(result => { + const joinResults = joinMap[result[joinColumn.referencedColumn]] || []; + + const matchingRows = joinResults.flatMap(joinResult => { + const id = joinResult[idColumn.name]; + const type = joinResult[typeColumn.name]; + + const fromTargetRow = rowOrRows => fromRow(db.metadata.get(type), rowOrRows); + + return (map[type][id] || []).map(row => { + return { + __type: type, + ...fromTargetRow(row), + }; + }); + }); + + result[key] = matchingRows; + }); + } else if (attribute.relation === 'morphToOne') { + const { morphColumn } = attribute; + const { idColumn, typeColumn } = morphColumn; + + // make a map for each type what ids to return + // make a nested map per id + + const idsByType = results.reduce((acc, result) => { + const idValue = result[morphColumn.idColumn.name]; + const typeValue = result[morphColumn.typeColumn.name]; + + if (!idValue || !typeValue) { + return acc; + } + + if (!_.has(typeValue, acc)) { + acc[typeValue] = []; + } + + acc[typeValue].push(idValue); + + return acc; + }, {}); + + const map = {}; + for (const type in idsByType) { + const ids = idsByType[type]; + + const qb = db.entityManager.createQueryBuilder(type); + + const rows = await qb + .init(populateValue) + .addSelect(`${qb.alias}.${idColumn.referencedColumn}`) + .where({ [idColumn.referencedColumn]: ids }) + .execute({ mapResults: false }); + + map[type] = _.groupBy(idColumn.referencedColumn, rows); + } + + results.forEach(result => { + const id = result[idColumn.name]; + const type = result[typeColumn.name]; + + if (!type || !id) { + result[key] = null; + return; + } + + const matchingRows = map[type][id]; + + const fromTargetRow = rowOrRows => fromRow(db.metadata.get(type), rowOrRows); + + result[key] = fromTargetRow(_.first(matchingRows)); + }); } } }; diff --git a/packages/core/database/lib/schema/schema.js b/packages/core/database/lib/schema/schema.js index aec1d3fded..b213cf3a89 100644 --- a/packages/core/database/lib/schema/schema.js +++ b/packages/core/database/lib/schema/schema.js @@ -37,8 +37,24 @@ const createTable = meta => { // TODO: if relation & has a joinColumn -> create it if (types.isRelation(attribute.type)) { - if (attribute.joinColumn && attribute.owner) { - // TODO: pass uniquness for oneToOne to avoid create more than one to one + if (attribute.morphColumn && attribute.owner) { + const { idColumn, typeColumn } = attribute.morphColumn; + + table.columns.push( + createColumn(idColumn.name, { + type: 'integer', + unsigned: true, + }) + ); + + table.columns.push( + createColumn(typeColumn.name, { + type: 'string', + }) + ); + } else if (attribute.joinColumn && attribute.owner) { + // 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, { @@ -53,12 +69,10 @@ const createTable = meta => { columns: [columnName], referencedTable, referencedColumns: [referencedColumn], - onDelete: 'SET NULL', // NOTE: could allow ocnifguration + onDelete: 'SET NULL', // NOTE: could allow configuration }); } } else if (shouldCreateColumn(attribute)) { - // TODO: if column is unique then add a unique index outside so we can easily do the diff - const column = createColumn(key, meta.attributes[key]); if (column.unique) { diff --git a/packages/core/database/lib/utils/content-types.js b/packages/core/database/lib/utils/content-types.js index 2e25415bb8..18f686cefa 100644 --- a/packages/core/database/lib/utils/content-types.js +++ b/packages/core/database/lib/utils/content-types.js @@ -6,9 +6,9 @@ const transformAttribute = attribute => { // convert to relation return { type: 'relation', - relation: attribute.single === true ? 'manyToOne' : 'manyToMany', //'morphOne' : 'morphMany', + relation: attribute.single === true ? 'morphOne' : 'morphMany', target: 'plugins::upload.file', - // morphOn: 'related', + morphBy: 'related', }; } // case 'component': { @@ -29,7 +29,7 @@ const transformContentTypes = contentTypes => { singularName: contentType.modelName, tableName: contentType.collectionName, attributes: { - ...Object.keys(contentType.attributes).reduce((attrs, attrName) => { + ...Object.keys(contentType.attributes || {}).reduce((attrs, attrName) => { return Object.assign(attrs, { [attrName]: transformAttribute(contentType.attributes[attrName]), }); diff --git a/packages/core/upload/models/File.js b/packages/core/upload/models/File.js index e0e833fef7..b421757622 100644 --- a/packages/core/upload/models/File.js +++ b/packages/core/upload/models/File.js @@ -82,10 +82,10 @@ module.exports = { type: 'json', configurable: false, }, - // related: { - // collection: '*', - // filter: 'field', - // configurable: false, - // }, + related: { + type: 'relation', + relation: 'morphToMany', + configurable: false, + }, }, };