objection
Version:
An SQL-friendly ORM for Node.js
504 lines (414 loc) • 14.3 kB
JavaScript
'use strict';
const { getModel } = require('../../model/getModel');
const { Relation } = require('../Relation');
const { RelationProperty } = require('../RelationProperty');
const { isSqlite } = require('../../utils/knexUtils');
const { inheritModel } = require('../../model/inheritModel');
const { resolveModel } = require('../../utils/resolveModel');
const { mapAfterAllReturn } = require('../../utils/promiseUtils');
const { isFunction, isObject, isString } = require('../../utils/objectUtils');
const { createModifier } = require('../../utils/createModifier');
const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
const { ManyToManyInsertOperation } = require('./insert/ManyToManyInsertOperation');
const { ManyToManyRelateOperation } = require('./relate/ManyToManyRelateOperation');
const { ManyToManyUnrelateOperation } = require('./unrelate/ManyToManyUnrelateOperation');
const {
ManyToManyUnrelateSqliteOperation,
} = require('./unrelate/ManyToManyUnrelateSqliteOperation');
const { ManyToManyUpdateOperation } = require('./update/ManyToManyUpdateOperation');
const { ManyToManyUpdateSqliteOperation } = require('./update/ManyToManyUpdateSqliteOperation');
const { ManyToManyDeleteOperation } = require('./delete/ManyToManyDeleteOperation');
const { ManyToManyDeleteSqliteOperation } = require('./delete/ManyToManyDeleteSqliteOperation');
class ManyToManyRelation extends Relation {
setMapping(mapping) {
const retVal = super.setMapping(mapping);
let ctx = {
mapping,
ownerModelClass: this.ownerModelClass,
relatedModelClass: this.relatedModelClass,
ownerProp: this.ownerProp,
relatedProp: this.relatedProp,
joinTableModelClass: null,
joinTableOwnerProp: null,
joinTableRelatedProp: null,
joinTableBeforeInsert: null,
joinTableExtras: [],
createError: (msg) => this.createError(msg),
};
ctx = checkThroughObject(ctx);
ctx = resolveJoinModelClassIfDefined(ctx);
ctx = createJoinProperties(ctx);
ctx = parseExtras(ctx);
ctx = parseModify(ctx);
ctx = parseBeforeInsert(ctx);
ctx = finalizeJoinModelClass(ctx);
this.joinTableExtras = ctx.joinTableExtras;
this.joinTableModify = ctx.joinTableModify;
this.joinTableModelClass = ctx.joinTableModelClass;
this.joinTableOwnerProp = ctx.joinTableOwnerProp;
this.joinTableRelatedProp = ctx.joinTableRelatedProp;
this.joinTableBeforeInsert = ctx.joinTableBeforeInsert;
return retVal;
}
get forbiddenMappingProperties() {
return [];
}
findQuery(builder, owner) {
const joinTableOwnerRefs = this.joinTableOwnerProp.refs(builder);
const joinTable = builder.tableNameFor(this.joinTable);
const joinTableAlias = builder.tableRefFor(this.joinTable);
let joinTableSelect = this.joinTableModelClass
.query()
.childQueryOf(builder)
.modify(this.joinTableModify)
.as(joinTableAlias);
if (joinTableSelect.isSelectAll()) {
joinTableSelect = aliasedTableName(joinTable, joinTableAlias);
}
builder.join(joinTableSelect, (join) => {
this.relatedProp.forEach((i) => {
const relatedRef = this.relatedProp.ref(builder, i);
const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i);
join.on(relatedRef, joinTableRelatedRef);
});
});
owner.buildFindQuery(builder, this, joinTableOwnerRefs);
return this.applyModify(builder);
}
join(
builder,
{
joinOperation = defaultJoinOperation(this, builder),
relatedTableAlias = defaultRelatedTableAlias(this, builder),
relatedJoinSelectQuery = defaultRelatedJoinSelectQuery(this, builder),
relatedTable = defaultRelatedTable(this, builder),
ownerTable = defaultOwnerTable(this, builder),
joinTableAlias = defaultJoinTableAlias(this, relatedTableAlias, builder),
} = {},
) {
let relatedJoinSelect = this.applyModify(relatedJoinSelectQuery).as(relatedTableAlias);
if (relatedJoinSelect.isSelectAll()) {
// No need to join a subquery if the query is `select * from "RelatedTable"`.
relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
}
let joinTableSelect = this.joinTableModelClass
.query()
.childQueryOf(builder)
.modify(this.joinTableModify)
.as(joinTableAlias);
if (joinTableSelect.isSelectAll()) {
joinTableSelect = aliasedTableName(this.joinTable, joinTableAlias);
}
return builder[joinOperation](joinTableSelect, (join) => {
const ownerProp = this.ownerProp;
const joinTableOwnerProp = this.joinTableOwnerProp;
ownerProp.forEach((i) => {
const joinTableOwnerRef = joinTableOwnerProp.ref(builder, i).table(joinTableAlias);
const ownerRef = ownerProp.ref(builder, i).table(ownerTable);
join.on(joinTableOwnerRef, ownerRef);
});
})[joinOperation](relatedJoinSelect, (join) => {
const relatedProp = this.relatedProp;
const joinTableRelatedProp = this.joinTableRelatedProp;
relatedProp.forEach((i) => {
const joinTableRelatedRef = joinTableRelatedProp.ref(builder, i).table(joinTableAlias);
const relatedRef = relatedProp.ref(builder, i).table(relatedTableAlias);
join.on(joinTableRelatedRef, relatedRef);
});
});
}
find(_, owner) {
return new ManyToManyFindOperation('find', {
relation: this,
owner,
});
}
insert(_, owner) {
return new ManyToManyInsertOperation('insert', {
relation: this,
owner,
});
}
update(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUpdateSqliteOperation('update', {
relation: this,
owner,
});
} else {
return new ManyToManyUpdateOperation('update', {
relation: this,
owner,
});
}
}
patch(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUpdateSqliteOperation('patch', {
modelOptions: { patch: true },
relation: this,
owner,
});
} else {
return new ManyToManyUpdateOperation('patch', {
modelOptions: { patch: true },
relation: this,
owner,
});
}
}
delete(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyDeleteSqliteOperation('delete', {
relation: this,
owner,
});
} else {
return new ManyToManyDeleteOperation('delete', {
relation: this,
owner,
});
}
}
relate(builder, owner) {
return new ManyToManyRelateOperation('relate', {
relation: this,
owner,
});
}
unrelate(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUnrelateSqliteOperation('unrelate', {
relation: this,
owner,
});
} else {
return new ManyToManyUnrelateOperation('unrelate', {
relation: this,
owner,
});
}
}
createJoinModels(ownerId, related) {
return related.map((related) => this.createJoinModel(ownerId, related));
}
createJoinModel(ownerId, rel) {
let joinModel = {};
for (let j = 0, lp = this.joinTableOwnerProp.size; j < lp; ++j) {
this.joinTableOwnerProp.setProp(joinModel, j, ownerId[j]);
}
for (let j = 0, lp = this.joinTableRelatedProp.size; j < lp; ++j) {
const value = this.relatedProp.getProp(rel, j);
if (value !== undefined) {
this.joinTableRelatedProp.setProp(joinModel, j, value);
}
}
for (const extra of this.joinTableExtras) {
let extraValue = rel[extra.aliasProp];
if (extraValue === undefined && rel.$$queryProps) {
extraValue = rel.$$queryProps[extra.aliasProp];
}
if (extraValue !== undefined) {
joinModel[extra.joinTableProp] = extraValue;
}
}
return joinModel;
}
omitExtraProps(models) {
if (this.joinTableExtras && this.joinTableExtras.length) {
const props = this.joinTableExtras.map((extra) => extra.aliasProp);
for (const model of models) {
// Omit extra properties instead of deleting them from the models so that they can
// be used in the `$before` and `$after` hooks.
model.$omitFromDatabaseJson(props);
}
}
}
executeJoinTableBeforeInsert(models, queryContext, result) {
return mapAfterAllReturn(
models,
(model) => this.joinTableBeforeInsert(model, queryContext),
result,
);
}
}
Object.defineProperties(ManyToManyRelation.prototype, {
isObjectionManyToManyRelation: {
enumerable: false,
writable: false,
value: true,
},
});
function defaultJoinOperation() {
return 'join';
}
function defaultRelatedTableAlias(relation, builder) {
return builder.tableRefFor(relation.relatedModelClass);
}
function defaultRelatedJoinSelectQuery(relation, builder) {
return relation.relatedModelClass.query().childQueryOf(builder);
}
function defaultRelatedTable(relation, builder) {
return builder.tableNameFor(relation.relatedModelClass);
}
function defaultOwnerTable(relation, builder) {
return builder.tableRefFor(relation.ownerModelClass);
}
function defaultJoinTableAlias(relation, relatedTableAlias, builder) {
const alias = builder.tableRefFor(relation.joinTable);
if (alias === relation.joinTable) {
return relation.ownerModelClass.joinTableAlias(relatedTableAlias);
} else {
return alias;
}
}
function aliasedTableName(tableName, alias) {
if (tableName === alias) {
return tableName;
} else {
return `${tableName} as ${alias}`;
}
}
function checkThroughObject(ctx) {
const mapping = ctx.mapping;
if (!isObject(mapping.join.through)) {
throw ctx.createError('join must have a `through` object that describes the join table.');
}
if (!mapping.join.through.from || !mapping.join.through.to) {
throw ctx.createError(
'join.through must be an object that describes the join table. For example: {from: "JoinTable.someId", to: "JoinTable.someOtherId"}',
);
}
return ctx;
}
function resolveJoinModelClassIfDefined(ctx) {
let joinTableModelClass = null;
if (ctx.mapping.join.through.modelClass) {
try {
joinTableModelClass = resolveModel(
ctx.mapping.join.through.modelClass,
ctx.ownerModelClass.modelPaths,
'join.through.modelClass',
);
} catch (err) {
throw ctx.createError(err.message);
}
}
return Object.assign(ctx, { joinTableModelClass });
}
function createJoinProperties(ctx) {
let ret;
let fromProp;
let toProp;
let relatedProp;
let ownerProp;
ret = createRelationProperty(ctx, ctx.mapping.join.through.from, 'join.through.from');
fromProp = ret.prop;
ctx = ret.ctx;
ret = createRelationProperty(ctx, ctx.mapping.join.through.to, 'join.through.to');
toProp = ret.prop;
ctx = ret.ctx;
if (fromProp.modelClass.getTableName() !== toProp.modelClass.getTableName()) {
throw ctx.createError('join.through `from` and `to` must point to the same join table.');
}
if (ctx.relatedProp.modelClass.getTableName() === fromProp.modelClass.getTableName()) {
relatedProp = fromProp;
ownerProp = toProp;
} else {
relatedProp = toProp;
ownerProp = fromProp;
}
return Object.assign(ctx, {
joinTableOwnerProp: ownerProp,
joinTableRelatedProp: relatedProp,
});
}
function createRelationProperty(ctx, refString, messagePrefix) {
let prop = null;
let joinTableModelClass = ctx.joinTableModelClass;
const resolveModelClass = (table) => {
if (joinTableModelClass === null) {
joinTableModelClass = inheritModel(getModel());
joinTableModelClass.tableName = table;
joinTableModelClass.idColumn = null;
joinTableModelClass.concurrency = ctx.ownerModelClass.concurrency;
}
if (joinTableModelClass.getTableName() === table) {
return joinTableModelClass;
} else {
return null;
}
};
try {
prop = new RelationProperty(refString, resolveModelClass);
} catch (err) {
if (err instanceof RelationProperty.ModelNotFoundError) {
throw ctx.createError('join.through `from` and `to` must point to the same join table.');
} else {
throw ctx.createError(
`${messagePrefix} must have format JoinTable.columnName. For example "JoinTable.someId" or in case of composite key ["JoinTable.a", "JoinTable.b"].`,
);
}
}
return {
ctx: Object.assign(ctx, { joinTableModelClass }),
prop,
};
}
function parseExtras(ctx) {
let extraDef = ctx.mapping.join.through.extra;
if (!extraDef) {
return ctx;
}
if (isString(extraDef)) {
extraDef = {
[extraDef]: extraDef,
};
} else if (Array.isArray(extraDef)) {
extraDef = extraDef.reduce((extraDef, col) => {
extraDef[col] = col;
return extraDef;
}, {});
}
const joinTableExtras = Object.keys(extraDef).map((key) => {
const val = extraDef[key];
return {
joinTableCol: val,
joinTableProp: ctx.joinTableModelClass.columnNameToPropertyName(val),
aliasCol: key,
aliasProp: ctx.joinTableModelClass.columnNameToPropertyName(key),
};
});
return Object.assign(ctx, { joinTableExtras });
}
function parseModify(ctx) {
const mapping = ctx.mapping.join.through;
const modifier = mapping.modify || mapping.filter;
const joinTableModify =
modifier &&
createModifier({
modifier,
modelClass: ctx.relatedModelClass,
});
return Object.assign(ctx, { joinTableModify });
}
function parseBeforeInsert(ctx) {
let joinTableBeforeInsert;
if (isFunction(ctx.mapping.join.through.beforeInsert)) {
joinTableBeforeInsert = ctx.mapping.join.through.beforeInsert;
} else {
joinTableBeforeInsert = (model) => model;
}
return Object.assign(ctx, { joinTableBeforeInsert });
}
function finalizeJoinModelClass(ctx) {
if (ctx.joinTableModelClass.getIdColumn() === null) {
// We cannot know if the join table has a primary key. Therefore we set some
// known column as the idColumn so that inserts will work.
ctx.joinTableModelClass.idColumn = ctx.joinTableRelatedProp.cols;
}
return ctx;
}
module.exports = {
ManyToManyRelation,
};