mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 10:55:37 +00:00
Add populate v1 for morph relations + create associations
This commit is contained in:
parent
50d39abe29
commit
a9002be38a
@ -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);
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 */ }) => {
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]),
|
||||
});
|
||||
|
||||
@ -82,10 +82,10 @@ module.exports = {
|
||||
type: 'json',
|
||||
configurable: false,
|
||||
},
|
||||
// related: {
|
||||
// collection: '*',
|
||||
// filter: 'field',
|
||||
// configurable: false,
|
||||
// },
|
||||
related: {
|
||||
type: 'relation',
|
||||
relation: 'morphToMany',
|
||||
configurable: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user