objection
Version:
An SQL-friendly ORM for Node.js
372 lines (309 loc) • 11.8 kB
JavaScript
import _ from 'lodash';
import Relation from '../Relation';
import inheritModel from '../../model/inheritModel';
import {isSqlite} from '../../utils/dbUtils';
import memoize from '../../utils/decorators/memoize';
import ManyToManyFindOperation from './ManyToManyFindOperation';
import ManyToManyInsertOperation from './ManyToManyInsertOperation';
import ManyToManyRelateOperation from './ManyToManyRelateOperation';
import ManyToManyUnrelateOperation from './ManyToManyUnrelateOperation';
import ManyToManyUnrelateSqliteOperation from './ManyToManyUnrelateSqliteOperation';
import ManyToManyUpdateOperation from './ManyToManyUpdateOperation';
import ManyToManyUpdateSqliteOperation from './ManyToManyUpdateSqliteOperation';
import ManyToManyDeleteOperation from './ManyToManyDeleteOperation';
import ManyToManyDeleteSqliteOperation from './ManyToManyDeleteSqliteOperation';
const sqliteBuiltInRowId = '_rowid_';
export default class ManyToManyRelation extends Relation {
setMapping(mapping) {
let retVal = super.setMapping(mapping);
// Avoid require loop and import here.
let Model = require(__dirname + '/../../model/Model').default;
if (!_.isObject(mapping.join.through)) {
this.throwError('join must have the `through` that describes the join table.');
}
if (!mapping.join.through.from || !mapping.join.through.to) {
this.throwError('join.through must be an object that describes the join table. For example: {from: "JoinTable.someId", to: "JoinTable.someOtherId"}');
}
let joinFrom = this.parseReference(mapping.join.from);
let joinTableFrom = this.parseReference(mapping.join.through.from);
let joinTableTo = this.parseReference(mapping.join.through.to);
let joinTableExtra = mapping.join.through.extra || [];
if (!joinTableFrom.table || _.isEmpty(joinTableFrom.columns)) {
this.throwError('join.through.from must have format JoinTable.columnName. For example "JoinTable.someId" or in case of composite key ["JoinTable.a", "JoinTable.b"].');
}
if (!joinTableTo.table || _.isEmpty(joinTableTo.columns)) {
this.throwError('join.through.to must have format JoinTable.columnName. For example "JoinTable.someId" or in case of composite key ["JoinTable.a", "JoinTable.b"].');
}
if (joinTableFrom.table !== joinTableTo.table) {
this.throwError('join.through `from` and `to` must point to the same join table.');
}
this.joinTable = joinTableFrom.table;
if (joinFrom.table === this.ownerModelClass.tableName) {
this.joinTableOwnerCol = joinTableFrom.columns;
this.joinTableRelatedCol = joinTableTo.columns;
} else {
this.joinTableRelatedCol = joinTableFrom.columns;
this.joinTableOwnerCol = joinTableTo.columns;
}
if (mapping.join.through.modelClass) {
this._joinTableModelClass = this.resolveModel(Model, mapping.join.through.modelClass, 'join.through.modelClass');
} else {
this._joinTableModelClass = inheritModel(Model);
this._joinTableModelClass.tableName = this.joinTable;
// 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.
this._joinTableModelClass.idColumn = this.joinTableRelatedCol;
}
this.joinTableOwnerProp = this.propertyName(this.joinTableOwnerCol, this._joinTableModelClass);
this.joinTableRelatedProp = this.propertyName(this.joinTableRelatedCol, this._joinTableModelClass);
this.joinTableExtras = this.parseExtras(joinTableExtra);
return retVal;
}
/**
* @returns {Array.<string>}
*/
fullJoinTableOwnerCol() {
return this.joinTableOwnerCol.map(col => `${this.joinTable}.${col}`);
}
/**
* @returns {Array.<string>}
*/
fullJoinTableRelatedCol() {
return this.joinTableRelatedCol.map(col => `${this.joinTable}.${col}`);
}
/**
* @returns {string}
*/
joinTableAlias() {
return this.joinTable + '_rel_' + this.name;
}
/**
* @returns {ManyToManyRelation}
*/
bindKnex(knex) {
let bound = super.bindKnex(knex);
bound._joinTableModelClass = this._joinTableModelClass.bindKnex(knex);
return bound;
}
/**
* @returns {QueryBuilder}
*/
findQuery(builder, opt) {
builder.join(this.joinTable, join => {
const fullRelatedCol = this.fullRelatedCol();
const fullJoinTableRelatedCol = this.fullJoinTableRelatedCol();
for (let i = 0, l = fullJoinTableRelatedCol.length; i < l; ++i) {
join.on(fullJoinTableRelatedCol[i], fullRelatedCol[i]);
}
});
if (opt.isColumnRef) {
const fullJoinTableOwnerCol = this.fullJoinTableOwnerCol();
for (let i = 0, l = fullJoinTableOwnerCol.length; i < l; ++i) {
builder.whereRef(fullJoinTableOwnerCol[i], opt.ownerIds[i]);
}
} else {
let hasIds = false;
for (let i = 0, l = opt.ownerIds.length; i < l; ++i) {
const id = opt.ownerIds[i];
if (id) {
hasIds = true;
break;
}
}
if (hasIds) {
builder.whereInComposite(this.fullJoinTableOwnerCol(), opt.ownerIds);
} else {
builder.resolve([]);
}
}
return builder.modify(this.modify);
}
/**
* @returns {QueryBuilder}
*/
join(builder, opt) {
opt = opt || {};
opt.joinOperation = opt.joinOperation || 'join';
opt.relatedTableAlias = opt.relatedTableAlias || this.relatedTableAlias();
opt.relatedJoinSelectQuery = opt.relatedJoinSelectQuery || this.relatedModelClass.query().childQueryOf(builder);
opt.relatedTable = opt.relatedTable || this.relatedModelClass.tableName;
opt.ownerTable = opt.ownerTable || this.ownerModelClass.tableName;
opt.joinTableAlias = opt.joinTableAlias || `${opt.relatedTableAlias}_join`;
const joinTableAsAlias = `${this.joinTable} as ${opt.joinTableAlias}`;
const joinTableOwnerCol = this.joinTableOwnerCol.map(col => `${opt.joinTableAlias}.${col}`);
const joinTableRelatedCol = this.joinTableRelatedCol.map(col => `${opt.joinTableAlias}.${col}`);
const relatedCol = this.relatedCol.map(col => `${opt.relatedTableAlias}.${col}`);
const ownerCol = this.ownerCol.map(col => `${opt.ownerTable}.${col}`);
let relatedJoinSelect = opt.relatedJoinSelectQuery
.modify(this.modify)
.as(opt.relatedTableAlias);
if (relatedJoinSelect.isSelectAll()) {
// No need to join a subquery if the query is `select * from "RelatedTable"`.
relatedJoinSelect = `${this.relatedModelClass.tableName} as ${opt.relatedTableAlias}`
}
return builder
[opt.joinOperation](joinTableAsAlias, join => {
for (let i = 0, l = joinTableOwnerCol.length; i < l; ++i) {
join.on(joinTableOwnerCol[i], ownerCol[i]);
}
})
[opt.joinOperation](relatedJoinSelect, join => {
for (let i = 0, l = joinTableRelatedCol.length; i < l; ++i) {
join.on(joinTableRelatedCol[i], relatedCol[i]);
}
});
}
find(builder, owners) {
return new ManyToManyFindOperation('find', {
relation: this,
owners: owners
});
}
insert(builder, owner) {
return new ManyToManyInsertOperation('insert', {
relation: this,
owner: owner
});
}
update(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUpdateSqliteOperation('update', {
relation: this,
owner: owner
});
} else {
return new ManyToManyUpdateOperation('update', {
relation: this,
owner: owner
});
}
}
patch(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUpdateSqliteOperation('patch', {
relation: this,
owner: owner,
modelOptions: {patch: true}
});
} else {
return new ManyToManyUpdateOperation('patch', {
relation: this,
owner: owner,
modelOptions: {patch: true}
});
}
}
delete(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyDeleteSqliteOperation('delete', {
relation: this,
owner: owner
});
} else {
return new ManyToManyDeleteOperation('delete', {
relation: this,
owner: owner
});
}
}
relate(builder, owner) {
return new ManyToManyRelateOperation('relate', {
relation: this,
owner: owner
});
}
unrelate(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUnrelateSqliteOperation('unrelate', {
relation: this,
owner: owner
});
} else {
return new ManyToManyUnrelateOperation('unrelate', {
relation: this,
owner: owner
});
}
}
selectForModify(builder, owner) {
let ownerId = owner.$values(this.ownerProp);
let idQuery = this.joinTableModelClass(builder.knex())
.query()
.childQueryOf(builder)
.select(this.fullJoinTableRelatedCol())
.whereComposite(this.fullJoinTableOwnerCol(), ownerId);
return builder.whereInComposite(this.fullRelatedCol(), idQuery);
}
selectForModifySqlite(builder, owner) {
const relatedTable = this.relatedModelClass.tableName;
const relatedTableAlias = this.relatedTableAlias();
const relatedTableAsAlias = relatedTable + ' as ' + relatedTableAlias;
const relatedTableAliasRowId = relatedTableAlias + '.' + sqliteBuiltInRowId;
const relatedTableRowId = relatedTable + '.' + sqliteBuiltInRowId;
const selectRelatedQuery = this.joinTableModelClass(builder.knex())
.query()
.childQueryOf(builder)
.select(relatedTableAliasRowId)
.whereComposite(this.fullJoinTableOwnerCol(), owner.$values(this.ownerProp))
.join(relatedTableAsAlias, join => {
const fullJoinTableRelatedCols = this.fullJoinTableRelatedCol();
const fullRelatedCol = this.fullRelatedCol();
for (let i = 0, l = fullJoinTableRelatedCols.length; i < l; ++i) {
join.on(fullJoinTableRelatedCols[i], fullRelatedCol[i]);
}
});
return builder.whereInComposite(relatedTableRowId, selectRelatedQuery);
}
createJoinModels(ownerId, related) {
const joinModels = new Array(related.length);
for (let i = 0, lr = related.length; i < lr; ++i) {
const rel = related[i];
let joinModel = {};
for (let j = 0, lp = this.joinTableOwnerProp.length; j < lp; ++j) {
joinModel[this.joinTableOwnerProp[j]] = ownerId[j];
}
for (let j = 0, lp = this.joinTableRelatedProp.length; j < lp; ++j) {
joinModel[this.joinTableRelatedProp[j]] = rel[this.relatedProp[j]];
}
for (let j = 0, lp = this.joinTableExtras.length; j < lp; ++j) {
const extra = this.joinTableExtras[j];
const extraValue = rel[extra.aliasProp];
if (!_.isUndefined(extraValue)) {
joinModel[extra.joinTableProp] = extraValue;
}
}
joinModels[i] = joinModel;
}
return joinModels;
}
omitExtraProps(models) {
if (!_.isEmpty(this.joinTableExtras)) {
const props = this.joinTableExtras.map(extra => extra.aliasProp);
for (let i = 0, l = models.length; i < l; ++i) {
models[i].$omitFromDatabaseJson(props);
}
}
}
/**
* @protected
*/
parseExtras(extras) {
if (Array.isArray(extras)) {
extras = extras.reduce((extras, col) => {
extras[col] = col;
return extras;
}, {});
}
return Object.keys(extras).map(key => {
const val = extras[key];
return {
joinTableCol: val,
joinTableProp: this._joinTableModelClass.columnNameToPropertyName(val),
aliasCol: key,
aliasProp: this._joinTableModelClass.columnNameToPropertyName(key)
};
});
}
}