mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 03:17:11 +00:00
Handle join table pivot associations and ordering
This commit is contained in:
parent
2009ebf129
commit
3c2040011a
@ -5,14 +5,32 @@ const types = require('./types');
|
||||
const { createField } = require('./fields');
|
||||
const { createQueryBuilder } = require('./query');
|
||||
const { createRepository } = require('./entity-repository');
|
||||
const { isBidirectional } = require('./metadata/relations');
|
||||
const { isBidirectional, isOneToAny } = require('./metadata/relations');
|
||||
|
||||
const toId = value => value.id || value;
|
||||
const toIds = value => _.castArray(value || []).map(toId);
|
||||
|
||||
// TODO: move to query layer
|
||||
const isValidId = value => _.isString(value) || _.isInteger(value);
|
||||
const toAssocs = data => {
|
||||
return _.castArray(data)
|
||||
.filter(datum => !_.isNil(datum))
|
||||
.map(datum => {
|
||||
// if it is a string or an integer return an obj with id = to datum
|
||||
if (isValidId(datum)) {
|
||||
return { id: datum, __pivot: {} };
|
||||
}
|
||||
|
||||
// if it is an object check it has at least a valid id
|
||||
if (!_.has('id', datum) || !isValidId(datum.id)) {
|
||||
throw new Error(`Invalid id, expected a string or integer, got ${datum}`);
|
||||
}
|
||||
|
||||
return datum;
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: handle programmatic defaults
|
||||
const toRow = (metadata, data = {}) => {
|
||||
const toRow = (metadata, data = {}, { withDefaults = false } = {}) => {
|
||||
const { attributes } = metadata;
|
||||
|
||||
const obj = {};
|
||||
@ -20,14 +38,24 @@ const toRow = (metadata, data = {}) => {
|
||||
for (const attributeName in attributes) {
|
||||
const attribute = attributes[attributeName];
|
||||
|
||||
if (types.isScalar(attribute.type) && _.has(attributeName, data)) {
|
||||
// TODO: we convert to column name
|
||||
// TODO: handle default value
|
||||
// TODO: convert to column name
|
||||
if (types.isScalar(attribute.type)) {
|
||||
const field = createField(attribute);
|
||||
|
||||
const field = createField(attribute.type, attribute);
|
||||
if (_.isUndefined(data[attributeName])) {
|
||||
if (!_.isUndefined(attribute.default) && withDefaults) {
|
||||
if (typeof attribute.default === 'function') {
|
||||
obj[attributeName] = attribute.default();
|
||||
} else {
|
||||
obj[attributeName] = attribute.default;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: validate data on creation
|
||||
// field.validate(data[attributeName]);
|
||||
if (typeof field.validate === 'function' && data[attributeName] !== null) {
|
||||
field.validate(data[attributeName]);
|
||||
}
|
||||
|
||||
const val = data[attributeName] === null ? null : field.toDB(data[attributeName]);
|
||||
|
||||
@ -139,7 +167,7 @@ const createEntityManager = db => {
|
||||
throw new Error('Create expects a data object');
|
||||
}
|
||||
|
||||
const dataToInsert = toRow(metadata, data);
|
||||
const dataToInsert = toRow(metadata, data, { withDefaults: true });
|
||||
|
||||
const [id] = await this.createQueryBuilder(uid)
|
||||
.insert(dataToInsert)
|
||||
@ -160,6 +188,7 @@ const createEntityManager = db => {
|
||||
return result;
|
||||
},
|
||||
|
||||
// TODO: where do we handle relation processing for many queries ?
|
||||
async createMany(uid, params = {}) {
|
||||
await db.lifecycles.run('beforeCreateMany', uid, { params });
|
||||
|
||||
@ -170,7 +199,7 @@ const createEntityManager = db => {
|
||||
throw new Error('CreateMany expects data to be an array');
|
||||
}
|
||||
|
||||
const dataToInsert = data.map(datum => toRow(metadata, datum));
|
||||
const dataToInsert = data.map(datum => toRow(metadata, datum, { withDefaults: true }));
|
||||
|
||||
if (_.isEmpty(dataToInsert)) {
|
||||
throw new Error('Nothing to insert');
|
||||
@ -236,6 +265,7 @@ const createEntityManager = db => {
|
||||
return result;
|
||||
},
|
||||
|
||||
// TODO: where do we handle relation processing for many queries ?
|
||||
async updateMany(uid, params = {}) {
|
||||
await db.lifecycles.run('beforeUpdateMany', uid, { params });
|
||||
|
||||
@ -327,7 +357,7 @@ const createEntityManager = db => {
|
||||
for (const attributeName in attributes) {
|
||||
const attribute = attributes[attributeName];
|
||||
|
||||
const isValidLink = _.has(attributeName, data) && !_.isNull(data[attributeName]);
|
||||
const isValidLink = _.has(attributeName, data) && !_.isNil(data[attributeName]);
|
||||
|
||||
if (attribute.type !== 'relation' || !isValidLink) {
|
||||
continue;
|
||||
@ -352,13 +382,15 @@ const createEntityManager = db => {
|
||||
|
||||
const { idColumn, typeColumn } = morphColumn;
|
||||
|
||||
const rows = toIds(data[attributeName]).map((dataID, idx) => ({
|
||||
[joinColumn.name]: dataID,
|
||||
[idColumn.name]: id,
|
||||
[typeColumn.name]: uid,
|
||||
...(joinTable.on || {}),
|
||||
order: idx,
|
||||
}));
|
||||
const rows = toAssocs(data[attributeName]).map(data => {
|
||||
return {
|
||||
[joinColumn.name]: data.id,
|
||||
[idColumn.name]: id,
|
||||
[typeColumn.name]: uid,
|
||||
...(joinTable.on || {}),
|
||||
...(data.__pivot || {}),
|
||||
};
|
||||
});
|
||||
|
||||
if (_.isEmpty(rows)) {
|
||||
continue;
|
||||
@ -379,12 +411,12 @@ const createEntityManager = db => {
|
||||
|
||||
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
|
||||
|
||||
const rows = _.castArray(data[attributeName] || []).map((data, idx) => ({
|
||||
const rows = toAssocs(data[attributeName]).map(data => ({
|
||||
[joinColumn.name]: id,
|
||||
[idColumn.name]: data.id,
|
||||
[typeColumn.name]: data[typeField],
|
||||
...(joinTable.on || {}),
|
||||
order: idx,
|
||||
...(data.__pivot || {}),
|
||||
}));
|
||||
|
||||
if (_.isEmpty(rows)) {
|
||||
@ -438,13 +470,8 @@ const createEntityManager = db => {
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn } = joinTable;
|
||||
|
||||
// TODO: redefine
|
||||
// TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
|
||||
|
||||
if (
|
||||
['oneToOne', 'oneToMany'].includes(attribute.relation) &&
|
||||
isBidirectional(attribute)
|
||||
) {
|
||||
// TODO: validate logic of delete
|
||||
if (isOneToAny(attribute) && isBidirectional(attribute)) {
|
||||
await this.createQueryBuilder(joinTable.name)
|
||||
.delete()
|
||||
.where({ [inverseJoinColumn.name]: _.castArray(data[attributeName]) })
|
||||
@ -452,11 +479,12 @@ const createEntityManager = db => {
|
||||
.execute();
|
||||
}
|
||||
|
||||
const insert = _.castArray(data[attributeName]).map(datum => {
|
||||
const insert = toAssocs(data[attributeName]).map(data => {
|
||||
return {
|
||||
[joinColumn.name]: id,
|
||||
[inverseJoinColumn.name]: datum,
|
||||
[inverseJoinColumn.name]: data.id,
|
||||
...(joinTable.on || {}),
|
||||
...(data.__pivot || {}),
|
||||
};
|
||||
});
|
||||
|
||||
@ -527,12 +555,12 @@ const createEntityManager = db => {
|
||||
})
|
||||
.execute();
|
||||
|
||||
const rows = toIds(data[attributeName] || []).map((dataID, idx) => ({
|
||||
[joinColumn.name]: dataID,
|
||||
const rows = toAssocs(data[attributeName]).map(data => ({
|
||||
[joinColumn.name]: data.id,
|
||||
[idColumn.name]: id,
|
||||
[typeColumn.name]: uid,
|
||||
...(joinTable.on || {}),
|
||||
order: idx,
|
||||
...(data.__pivot || {}),
|
||||
}));
|
||||
|
||||
if (_.isEmpty(rows)) {
|
||||
@ -566,12 +594,12 @@ const createEntityManager = db => {
|
||||
})
|
||||
.execute();
|
||||
|
||||
const rows = _.castArray(data[attributeName] || []).map((data, idx) => ({
|
||||
const rows = toAssocs(data[attributeName]).map(data => ({
|
||||
[joinColumn.name]: id,
|
||||
[idColumn.name]: data.id,
|
||||
[typeColumn.name]: data[typeField],
|
||||
...(joinTable.on || {}),
|
||||
order: idx,
|
||||
...(data.__pivot || {}),
|
||||
}));
|
||||
|
||||
if (_.isEmpty(rows)) {
|
||||
@ -624,17 +652,18 @@ const createEntityManager = db => {
|
||||
if (['oneToOne', 'oneToMany'].includes(attribute.relation)) {
|
||||
await this.createQueryBuilder(joinTable.name)
|
||||
.delete()
|
||||
.where({ [inverseJoinColumn.name]: _.castArray(data[attributeName] || []) })
|
||||
.where({ [inverseJoinColumn.name]: toIds(data[attributeName]) })
|
||||
.where(joinTable.on || {})
|
||||
.execute();
|
||||
}
|
||||
|
||||
if (!_.isNull(data[attributeName])) {
|
||||
const insert = _.castArray(data[attributeName] || []).map(datum => {
|
||||
const insert = toAssocs(data[attributeName]).map(data => {
|
||||
return {
|
||||
[joinColumn.name]: id,
|
||||
[inverseJoinColumn.name]: datum,
|
||||
[inverseJoinColumn.name]: data.id,
|
||||
...(joinTable.on || {}),
|
||||
...(data.__pivot || {}),
|
||||
};
|
||||
});
|
||||
|
||||
@ -652,9 +681,9 @@ const createEntityManager = db => {
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete relations of an existing entity
|
||||
* Delete relational associations of an existing entity
|
||||
* This removes associations but doesn't do cascade deletions for components for example. This will be handled on the entity service layer instead
|
||||
* NOTE: Most of the deletion should be handled by ON DELETE CASCADE for dialect that have FKs
|
||||
* NOTE: Most of the deletion should be handled by ON DELETE CASCADE for dialects that have FKs
|
||||
*
|
||||
* @param {EntityManager} em - entity manager instance
|
||||
* @param {Metadata} metadata - model metadta
|
||||
@ -816,11 +845,8 @@ const createEntityManager = db => {
|
||||
// custom queries
|
||||
|
||||
// utilities
|
||||
// -> format
|
||||
// -> parse
|
||||
// -> map result
|
||||
// -> map input
|
||||
// -> validation
|
||||
|
||||
// extra features
|
||||
// -> virtuals
|
||||
|
||||
5
packages/core/database/lib/fields.d.ts
vendored
5
packages/core/database/lib/fields.d.ts
vendored
@ -4,4 +4,7 @@ interface Field {
|
||||
fromDB(value: any): any;
|
||||
}
|
||||
|
||||
export function createField(type: string): Field;
|
||||
interface Attribute {
|
||||
type: string
|
||||
}
|
||||
export function createField(attribute: Attribute): Field;
|
||||
|
||||
@ -217,7 +217,9 @@ const typeToFieldMap = {
|
||||
boolean: BooleanField,
|
||||
};
|
||||
|
||||
const createField = (type /*attribute*/) => {
|
||||
const createField = attribute => {
|
||||
const { type } = attribute;
|
||||
|
||||
if (_.has(type, typeToFieldMap)) {
|
||||
return new typeToFieldMap[type]({});
|
||||
}
|
||||
|
||||
@ -102,6 +102,9 @@ const createMetadata = (models = []) => {
|
||||
on: {
|
||||
field: attributeName,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -135,6 +138,9 @@ const createMetadata = (models = []) => {
|
||||
on: {
|
||||
field: attributeName,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ const _ = require('lodash/fp');
|
||||
const hasInversedBy = _.has('inversedBy');
|
||||
const hasMappedBy = _.has('mappedBy');
|
||||
|
||||
const isOneToAny = attribute => ['oneToOne', 'oneToMany'].includes(attribute.relation);
|
||||
const isBidirectional = attribute => hasInversedBy(attribute) || hasMappedBy(attribute);
|
||||
const isOwner = attribute => !isBidirectional(attribute) || hasInversedBy(attribute);
|
||||
const shouldUseJoinTable = attribute => attribute.useJoinTable !== false;
|
||||
@ -442,4 +443,5 @@ module.exports = {
|
||||
createRelation,
|
||||
|
||||
isBidirectional,
|
||||
isOneToAny,
|
||||
};
|
||||
|
||||
@ -431,6 +431,7 @@ const applyJoin = (qb, join) => {
|
||||
rootColumn,
|
||||
rootTable = this.alias,
|
||||
on,
|
||||
orderBy,
|
||||
} = join;
|
||||
|
||||
qb[method]({ [alias]: referencedTable }, inner => {
|
||||
@ -442,6 +443,13 @@ const applyJoin = (qb, join) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (orderBy) {
|
||||
Object.keys(orderBy).forEach(column => {
|
||||
const direction = orderBy[column];
|
||||
qb.orderBy(`${alias}.${column}`, direction);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const applyJoins = (qb, joins) => joins.forEach(join => applyJoin(qb, join));
|
||||
@ -617,6 +625,7 @@ const applyPopulate = async (results, populate, ctx) => {
|
||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||
rootTable: qb.alias,
|
||||
on: joinTable.on,
|
||||
orderBy: joinTable.orderBy,
|
||||
})
|
||||
.addSelect(joinColAlias)
|
||||
.where({ [joinColAlias]: referencedValues })
|
||||
@ -734,6 +743,7 @@ const applyPopulate = async (results, populate, ctx) => {
|
||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||
rootTable: qb.alias,
|
||||
on: joinTable.on,
|
||||
orderBy: joinTable.orderBy,
|
||||
})
|
||||
.addSelect(joinColAlias)
|
||||
.where({ [joinColAlias]: referencedValues })
|
||||
@ -812,6 +822,7 @@ const applyPopulate = async (results, populate, ctx) => {
|
||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||
rootTable: qb.alias,
|
||||
on: joinTable.on,
|
||||
orderBy: joinTable.orderBy,
|
||||
})
|
||||
.addSelect(joinColAlias)
|
||||
.where({ [joinColAlias]: referencedValues })
|
||||
@ -894,6 +905,7 @@ const applyPopulate = async (results, populate, ctx) => {
|
||||
rootColumn: joinColumn.referencedColumn,
|
||||
rootTable: qb.alias,
|
||||
on: joinTable.on,
|
||||
orderBy: joinTable.orderBy,
|
||||
})
|
||||
.addSelect([`${alias}.${idColumn.name}`, `${alias}.${typeColumn.name}`])
|
||||
.where({
|
||||
@ -1078,7 +1090,7 @@ const fromRow = (metadata, row) => {
|
||||
// TODO: handle default value too
|
||||
// TODO: format data & use dialect to know which type they support (json particularly)
|
||||
|
||||
const field = createField(attribute.type, attribute);
|
||||
const field = createField(attribute);
|
||||
|
||||
// TODO: validate data on creation
|
||||
// field.validate(data[attributeName]);
|
||||
|
||||
@ -14,31 +14,6 @@ const omitComponentData = (contentType, data) => {
|
||||
return omit(componentAttributes, data);
|
||||
};
|
||||
|
||||
// components can have nested compos so this must be recursive
|
||||
const createComponent = async (uid, data) => {
|
||||
const model = strapi.getModel(uid);
|
||||
|
||||
const componentData = await createComponents(uid, data);
|
||||
|
||||
return await strapi.query(uid).create({
|
||||
data: Object.assign(omitComponentData(model, data), componentData),
|
||||
});
|
||||
};
|
||||
|
||||
// components can have nested compos so this must be recursive
|
||||
const updateComponent = async (uid, componentToUpdate, data) => {
|
||||
const model = strapi.getModel(uid);
|
||||
|
||||
const componentData = await updateComponents(uid, componentToUpdate, data);
|
||||
|
||||
return await strapi.query(uid).update({
|
||||
where: {
|
||||
id: componentToUpdate.id,
|
||||
},
|
||||
data: Object.assign(omitComponentData(model, data), componentData),
|
||||
});
|
||||
};
|
||||
|
||||
// NOTE: we could generalize the logic to allow CRUD of relation directly in the DB layer
|
||||
const createComponents = async (uid, data) => {
|
||||
const { attributes } = strapi.getModel(uid);
|
||||
@ -48,7 +23,7 @@ const createComponents = async (uid, data) => {
|
||||
for (const attributeName in attributes) {
|
||||
const attribute = attributes[attributeName];
|
||||
|
||||
if (!has(attributeName, data)) {
|
||||
if (!has(attributeName, data) || !contentTypesUtils.isComponentAttribute(attribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -71,10 +46,26 @@ const createComponents = async (uid, data) => {
|
||||
);
|
||||
|
||||
// TODO: add order
|
||||
componentBody[attributeName] = components.map(({ id }) => id);
|
||||
componentBody[attributeName] = components.map(({ id }, idx) => {
|
||||
return {
|
||||
id,
|
||||
__pivot: {
|
||||
order: idx + 1,
|
||||
field: attributeName,
|
||||
component_type: componentUID,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const component = await createComponent(componentUID, componentValue);
|
||||
componentBody[attributeName] = component.id;
|
||||
componentBody[attributeName] = {
|
||||
id: component.id,
|
||||
__pivot: {
|
||||
order: 1,
|
||||
field: attributeName,
|
||||
component_type: componentUID,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
@ -88,9 +79,16 @@ const createComponents = async (uid, data) => {
|
||||
}
|
||||
|
||||
componentBody[attributeName] = await Promise.all(
|
||||
dynamiczoneValues.map(async value => {
|
||||
dynamiczoneValues.map(async (value, idx) => {
|
||||
const { id } = await createComponent(value.__component, value);
|
||||
return { id, __component: value.__component };
|
||||
return {
|
||||
id,
|
||||
__component: value.__component,
|
||||
__pivot: {
|
||||
order: idx + 1,
|
||||
field: attributeName,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@ -101,21 +99,6 @@ const createComponents = async (uid, data) => {
|
||||
return componentBody;
|
||||
};
|
||||
|
||||
const updateOrCreateComponent = (componentUID, value) => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// update
|
||||
if (has('id', value)) {
|
||||
// TODO: verify the compo is associated with the entity
|
||||
return updateComponent(componentUID, { id: value.id }, value);
|
||||
}
|
||||
|
||||
// create
|
||||
return createComponent(componentUID, value);
|
||||
};
|
||||
|
||||
/*
|
||||
delete old components
|
||||
create or update
|
||||
@ -260,11 +243,6 @@ const deleteOldDZComponents = async (uid, entityToUpdate, attributeName, dynamic
|
||||
}
|
||||
};
|
||||
|
||||
const deleteComponent = async (uid, componentToDelete) => {
|
||||
await deleteComponents(uid, componentToDelete);
|
||||
await strapi.query(uid).delete({ where: { id: componentToDelete.id } });
|
||||
};
|
||||
|
||||
const deleteComponents = async (uid, entityToDelete) => {
|
||||
const { attributes } = strapi.getModel(uid);
|
||||
|
||||
@ -305,6 +283,55 @@ const deleteComponents = async (uid, entityToDelete) => {
|
||||
}
|
||||
};
|
||||
|
||||
/***************************
|
||||
Component queries
|
||||
***************************/
|
||||
|
||||
// components can have nested compos so this must be recursive
|
||||
const createComponent = async (uid, data) => {
|
||||
const model = strapi.getModel(uid);
|
||||
|
||||
const componentData = await createComponents(uid, data);
|
||||
|
||||
return await strapi.query(uid).create({
|
||||
data: Object.assign(omitComponentData(model, data), componentData),
|
||||
});
|
||||
};
|
||||
|
||||
// components can have nested compos so this must be recursive
|
||||
const updateComponent = async (uid, componentToUpdate, data) => {
|
||||
const model = strapi.getModel(uid);
|
||||
|
||||
const componentData = await updateComponents(uid, componentToUpdate, data);
|
||||
|
||||
return await strapi.query(uid).update({
|
||||
where: {
|
||||
id: componentToUpdate.id,
|
||||
},
|
||||
data: Object.assign(omitComponentData(model, data), componentData),
|
||||
});
|
||||
};
|
||||
|
||||
const updateOrCreateComponent = (componentUID, value) => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// update
|
||||
if (has('id', value)) {
|
||||
// TODO: verify the compo is associated with the entity
|
||||
return updateComponent(componentUID, { id: value.id }, value);
|
||||
}
|
||||
|
||||
// create
|
||||
return createComponent(componentUID, value);
|
||||
};
|
||||
|
||||
const deleteComponent = async (uid, componentToDelete) => {
|
||||
await deleteComponents(uid, componentToDelete);
|
||||
await strapi.query(uid).delete({ where: { id: componentToDelete.id } });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
omitComponentData,
|
||||
createComponents,
|
||||
|
||||
@ -70,10 +70,12 @@ const transformParamsToQuery = (uid, params = {}) => {
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
// TODO: handle *.* syntax
|
||||
query.select = _.castArray(fields);
|
||||
}
|
||||
|
||||
if (populate) {
|
||||
// TODO: handle *.* syntax
|
||||
const { populate } = params;
|
||||
query.populate = typeof populate === 'object' ? populate : _.castArray(populate);
|
||||
}
|
||||
|
||||
@ -154,10 +154,12 @@ const createContentType = (model, { modelName }, { apiName, pluginName } = {}) =
|
||||
Object.assign(model.attributes, {
|
||||
[CREATED_AT_ATTRIBUTE]: {
|
||||
type: 'datetime',
|
||||
// default: () => new Date(),
|
||||
default: () => new Date(),
|
||||
},
|
||||
// TODO: handle on edit set to new date
|
||||
[UPDATED_AT_ATTRIBUTE]: {
|
||||
type: 'datetime',
|
||||
default: () => new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user