123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- const RelationProperty = require('./RelationProperty');
- const QueryBuilder = require('../queryBuilder/QueryBuilder');
- const getModel = () => require('../model/Model');
- const path = require('path');
- const RelationFindOperation = require('./RelationFindOperation');
- const RelationUpdateOperation = require('./RelationUpdateOperation');
- const RelationDeleteOperation = require('./RelationDeleteOperation');
- const RelationSubqueryOperation = require('./RelationSubqueryOperation');
- const { ref } = require('../queryBuilder/ReferenceBuilder');
- const { get } = require('../utils/objectUtils');
- const { isSubclassOf } = require('../utils/classUtils');
- const { resolveModel } = require('../utils/resolveModel');
- const { mapAfterAllReturn } = require('../utils/promiseUtils');
- class Relation {
- constructor(relationName, OwnerClass) {
- this.name = relationName;
- this.ownerModelClass = OwnerClass;
- this.relatedModelClass = null;
- this.ownerProp = null;
- this.relatedProp = null;
- this.joinTableModelClass = null;
- this.joinTableOwnerProp = null;
- this.joinTableRelatedProp = null;
- this.joinTableBeforeInsert = null;
- this.joinTableExtras = [];
- this.modify = null;
- this.beforeInsert = null;
- }
- setMapping(mapping) {
- let ctx = {
- name: this.name,
- mapping,
- ownerModelClass: this.ownerModelClass,
- relatedModelClass: null,
- relatedProp: null,
- ownerProp: null,
- modify: null,
- beforeInsert: null,
- forbiddenMappingProperties: this.forbiddenMappingProperties,
- createError: msg => this.createError(msg)
- };
- ctx = checkForbiddenProperties(ctx);
- ctx = checkOwnerModelClass(ctx);
- ctx = checkRelatedModelClass(ctx);
- ctx = resolveRelatedModelClass(ctx);
- ctx = checkRelation(ctx);
- ctx = createJoinProperties(ctx);
- ctx = parseModify(ctx);
- ctx = parseBeforeInsert(ctx);
- this.relatedModelClass = ctx.relatedModelClass;
- this.ownerProp = ctx.ownerProp;
- this.relatedProp = ctx.relatedProp;
- this.modify = ctx.modify;
- this.beforeInsert = ctx.beforeInsert;
- }
- get forbiddenMappingProperties() {
- return ['join.through'];
- }
- get joinTable() {
- return this.joinTableModelClass ? this.joinTableModelClass.getTableName() : null;
- }
- get joinModelClass() {
- return this.getJoinModelClass(this.ownerModelClass.knex());
- }
- getJoinModelClass(knex) {
- return this.joinTableModelClass && knex !== this.joinTableModelClass.knex()
- ? this.joinTableModelClass.bindKnex(knex)
- : this.joinTableModelClass;
- }
- relatedTableAlias(builder) {
- const tableRef = builder.tableRefFor(this.relatedModelClass);
- const tableRefWithoutSchema = tableRef.replace('.', '_');
- return `${tableRefWithoutSchema}_rel_${this.name}`;
- }
- isOneToOne() {
- return false;
- }
- clone() {
- const relation = new this.constructor(this.name, this.ownerModelClass);
- relation.relatedModelClass = this.relatedModelClass;
- relation.ownerProp = this.ownerProp;
- relation.relatedProp = this.relatedProp;
- relation.modify = this.modify;
- relation.beforeInsert = this.beforeInsert;
- relation.joinTableModelClass = this.joinTableModelClass;
- relation.joinTableOwnerProp = this.joinTableOwnerProp;
- relation.joinTableRelatedProp = this.joinTableRelatedProp;
- relation.joinTableBeforeInsert = this.joinTableBeforeInsert;
- relation.joinTableExtras = this.joinTableExtras;
- return relation;
- }
- bindKnex(knex) {
- const bound = this.clone();
- bound.relatedModelClass = this.relatedModelClass.bindKnex(knex);
- bound.ownerModelClass = this.ownerModelClass.bindKnex(knex);
- if (this.joinTableModelClass) {
- bound.joinTableModelClass = this.joinTableModelClass.bindKnex(knex);
- }
- return bound;
- }
- findQuery(builder, opt) {
- const relatedRefs = this.relatedProp.refs(builder);
- if (opt.isColumnRef) {
- for (let i = 0, l = relatedRefs.length; i < l; ++i) {
- builder.where(relatedRefs[i], ref(opt.ownerIds[i]));
- }
- } else if (containsNonNull(opt.ownerIds)) {
- builder.whereInComposite(relatedRefs, opt.ownerIds);
- } else {
- builder.resolve([]);
- }
- return builder.modify(this.modify);
- }
- join(
- builder,
- {
- joinOperation = 'join',
- relatedTableAlias = this.relatedTableAlias(builder),
- relatedJoinSelectQuery = this.relatedModelClass.query().childQueryOf(builder),
- relatedTable = builder.tableNameFor(this.relatedModelClass),
- ownerTable = builder.tableRefFor(this.ownerModelClass)
- } = {}
- ) {
- let relatedSelect = relatedJoinSelectQuery.modify(this.modify).as(relatedTableAlias);
- if (relatedSelect.isSelectAll()) {
- // No need to join a subquery if the query is `select * from "RelatedTable"`.
- relatedSelect = `${relatedTable} as ${relatedTableAlias}`;
- }
- return builder[joinOperation](relatedSelect, join => {
- const relatedProp = this.relatedProp;
- const ownerProp = this.ownerProp;
- for (let i = 0, l = relatedProp.size; i < l; ++i) {
- const relatedRef = relatedProp.ref(builder, i).table(relatedTableAlias);
- const ownerRef = ownerProp.ref(builder, i).table(ownerTable);
- join.on(relatedRef, ownerRef);
- }
- });
- }
- insert(builder, owner) {
- /* istanbul ignore next */
- throw this.createError('not implemented');
- }
- update(builder, owner) {
- return new RelationUpdateOperation('update', {
- relation: this,
- owner: owner
- });
- }
- patch(builder, owner) {
- return new RelationUpdateOperation('patch', {
- relation: this,
- owner: owner,
- modelOptions: { patch: true }
- });
- }
- find(builder, owners) {
- return new RelationFindOperation('find', {
- relation: this,
- owners: owners
- });
- }
- subQuery(builder) {
- return new RelationSubqueryOperation('subQuery', {
- relation: this
- });
- }
- delete(builder, owner) {
- return new RelationDeleteOperation('delete', {
- relation: this,
- owner: owner
- });
- }
- relate(builder, owner) {
- /* istanbul ignore next */
- throw this.createError('not implemented');
- }
- unrelate(builder, owner) {
- /* istanbul ignore next */
- throw this.createError('not implemented');
- }
- hasRelateProp(model) {
- return model.$hasProps(this.relatedProp.props);
- }
- executeBeforeInsert(models, queryContext, result) {
- return mapAfterAllReturn(models, model => this.beforeInsert(model, queryContext), result);
- }
- createError(message) {
- if (this.ownerModelClass && this.ownerModelClass.name && this.name) {
- return new Error(`${this.ownerModelClass.name}.relationMappings.${this.name}: ${message}`);
- } else {
- return new Error(`${this.constructor.name}: ${message}`);
- }
- }
- }
- function checkForbiddenProperties(ctx) {
- ctx.forbiddenMappingProperties.forEach(prop => {
- if (get(ctx.mapping, prop.split('.')) !== undefined) {
- throw ctx.createError(`Property ${prop} is not supported for this relation type.`);
- }
- });
- return ctx;
- }
- function checkOwnerModelClass(ctx) {
- if (!isSubclassOf(ctx.ownerModelClass, getModel())) {
- throw ctx.createError(`Relation's owner is not a subclass of Model`);
- }
- return ctx;
- }
- function checkRelatedModelClass(ctx) {
- if (!ctx.mapping.modelClass) {
- throw ctx.createError('modelClass is not defined');
- }
- return ctx;
- }
- function resolveRelatedModelClass(ctx) {
- let relatedModelClass;
- try {
- relatedModelClass = resolveModel(
- ctx.mapping.modelClass,
- ctx.ownerModelClass.modelPaths,
- 'modelClass'
- );
- } catch (err) {
- throw ctx.createError(err.message);
- }
- return Object.assign(ctx, { relatedModelClass });
- }
- function checkRelation(ctx) {
- if (!ctx.mapping.relation) {
- throw ctx.createError('relation is not defined');
- }
- if (!isSubclassOf(ctx.mapping.relation, Relation)) {
- throw ctx.createError('relation is not a subclass of Relation');
- }
- return ctx;
- }
- function createJoinProperties(ctx) {
- const mapping = ctx.mapping;
- if (!mapping.join || !mapping.join.from || !mapping.join.to) {
- throw ctx.createError(
- 'join must be an object that maps the columns of the related models together. For example: {from: "SomeTable.id", to: "SomeOtherTable.someModelId"}'
- );
- }
- const fromProp = createRelationProperty(ctx, mapping.join.from, 'join.from');
- const toProp = createRelationProperty(ctx, mapping.join.to, 'join.to');
- let ownerProp;
- let relatedProp;
- if (fromProp.modelClass.getTableName() === ctx.ownerModelClass.getTableName()) {
- ownerProp = fromProp;
- relatedProp = toProp;
- } else if (toProp.modelClass.getTableName() === ctx.ownerModelClass.getTableName()) {
- ownerProp = toProp;
- relatedProp = fromProp;
- } else {
- throw ctx.createError('join: either `from` or `to` must point to the owner model table.');
- }
- if (ownerProp.props.some(it => it === ctx.name)) {
- throw ctx.createError(
- `join: relation name and join property '${
- ctx.name
- }' cannot have the same name. If you cannot change one or the other, you can use $parseDatabaseJson and $formatDatabaseJson methods to convert the column name.`
- );
- }
- if (relatedProp.modelClass.getTableName() !== ctx.relatedModelClass.getTableName()) {
- throw ctx.createError('join: either `from` or `to` must point to the related model table.');
- }
- return Object.assign(ctx, { ownerProp, relatedProp });
- }
- function createRelationProperty(ctx, refString, propName) {
- try {
- return new RelationProperty(refString, table => {
- return [ctx.ownerModelClass, ctx.relatedModelClass].find(it => it.getTableName() === table);
- });
- } catch (err) {
- if (err instanceof RelationProperty.ModelNotFoundError) {
- throw ctx.createError(
- `join: either \`from\` or \`to\` must point to the owner model table and the other one to the related table. It might be that specified table '${
- err.tableName
- }' is not correct`
- );
- } else if (err instanceof RelationProperty.InvalidReferenceError) {
- throw ctx.createError(
- `${propName} must have format TableName.columnName. For example "SomeTable.id" or in case of composite key ["SomeTable.a", "SomeTable.b"].`
- );
- } else {
- throw err;
- }
- }
- }
- function parseModify(ctx) {
- const mapping = ctx.mapping;
- const value = mapping.modify || mapping.filter;
- let modify;
- if (value) {
- const type = typeof value;
- if (type === 'object') {
- modify = qb => qb.where(value);
- } else if (type === 'function') {
- modify = value;
- } else if (type === 'string') {
- const namedFilters = ctx.relatedModelClass.namedFilters;
- modify = namedFilters && namedFilters[value];
- if (!modify) {
- throw ctx.createError(`Could not find filter "${value}".`);
- }
- } else {
- throw ctx.createError(`Unable to determine modify function from provided value: "${value}".`);
- }
- } else {
- modify = () => {};
- }
- return Object.assign(ctx, { modify });
- }
- function parseBeforeInsert(ctx) {
- let beforeInsert;
- if (typeof ctx.mapping.beforeInsert === 'function') {
- beforeInsert = ctx.mapping.beforeInsert;
- } else {
- beforeInsert = model => model;
- }
- return Object.assign(ctx, { beforeInsert });
- }
- function containsNonNull(arr) {
- for (let i = 0, l = arr.length; i < l; ++i) {
- const val = arr[i];
- if (Array.isArray(val) && containsNonNull(val)) {
- return true;
- } else if (val !== null && val !== undefined) {
- return true;
- }
- }
- return false;
- }
- module.exports = Relation;
|