Relation.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. const RelationProperty = require('./RelationProperty');
  2. const QueryBuilder = require('../queryBuilder/QueryBuilder');
  3. const getModel = () => require('../model/Model');
  4. const path = require('path');
  5. const RelationFindOperation = require('./RelationFindOperation');
  6. const RelationUpdateOperation = require('./RelationUpdateOperation');
  7. const RelationDeleteOperation = require('./RelationDeleteOperation');
  8. const RelationSubqueryOperation = require('./RelationSubqueryOperation');
  9. const { ref } = require('../queryBuilder/ReferenceBuilder');
  10. const { get } = require('../utils/objectUtils');
  11. const { isSubclassOf } = require('../utils/classUtils');
  12. const { resolveModel } = require('../utils/resolveModel');
  13. const { mapAfterAllReturn } = require('../utils/promiseUtils');
  14. class Relation {
  15. constructor(relationName, OwnerClass) {
  16. this.name = relationName;
  17. this.ownerModelClass = OwnerClass;
  18. this.relatedModelClass = null;
  19. this.ownerProp = null;
  20. this.relatedProp = null;
  21. this.joinTableModelClass = null;
  22. this.joinTableOwnerProp = null;
  23. this.joinTableRelatedProp = null;
  24. this.joinTableBeforeInsert = null;
  25. this.joinTableExtras = [];
  26. this.modify = null;
  27. this.beforeInsert = null;
  28. }
  29. setMapping(mapping) {
  30. let ctx = {
  31. name: this.name,
  32. mapping,
  33. ownerModelClass: this.ownerModelClass,
  34. relatedModelClass: null,
  35. relatedProp: null,
  36. ownerProp: null,
  37. modify: null,
  38. beforeInsert: null,
  39. forbiddenMappingProperties: this.forbiddenMappingProperties,
  40. createError: msg => this.createError(msg)
  41. };
  42. ctx = checkForbiddenProperties(ctx);
  43. ctx = checkOwnerModelClass(ctx);
  44. ctx = checkRelatedModelClass(ctx);
  45. ctx = resolveRelatedModelClass(ctx);
  46. ctx = checkRelation(ctx);
  47. ctx = createJoinProperties(ctx);
  48. ctx = parseModify(ctx);
  49. ctx = parseBeforeInsert(ctx);
  50. this.relatedModelClass = ctx.relatedModelClass;
  51. this.ownerProp = ctx.ownerProp;
  52. this.relatedProp = ctx.relatedProp;
  53. this.modify = ctx.modify;
  54. this.beforeInsert = ctx.beforeInsert;
  55. }
  56. get forbiddenMappingProperties() {
  57. return ['join.through'];
  58. }
  59. get joinTable() {
  60. return this.joinTableModelClass ? this.joinTableModelClass.getTableName() : null;
  61. }
  62. get joinModelClass() {
  63. return this.getJoinModelClass(this.ownerModelClass.knex());
  64. }
  65. getJoinModelClass(knex) {
  66. return this.joinTableModelClass && knex !== this.joinTableModelClass.knex()
  67. ? this.joinTableModelClass.bindKnex(knex)
  68. : this.joinTableModelClass;
  69. }
  70. relatedTableAlias(builder) {
  71. const tableRef = builder.tableRefFor(this.relatedModelClass);
  72. const tableRefWithoutSchema = tableRef.replace('.', '_');
  73. return `${tableRefWithoutSchema}_rel_${this.name}`;
  74. }
  75. isOneToOne() {
  76. return false;
  77. }
  78. clone() {
  79. const relation = new this.constructor(this.name, this.ownerModelClass);
  80. relation.relatedModelClass = this.relatedModelClass;
  81. relation.ownerProp = this.ownerProp;
  82. relation.relatedProp = this.relatedProp;
  83. relation.modify = this.modify;
  84. relation.beforeInsert = this.beforeInsert;
  85. relation.joinTableModelClass = this.joinTableModelClass;
  86. relation.joinTableOwnerProp = this.joinTableOwnerProp;
  87. relation.joinTableRelatedProp = this.joinTableRelatedProp;
  88. relation.joinTableBeforeInsert = this.joinTableBeforeInsert;
  89. relation.joinTableExtras = this.joinTableExtras;
  90. return relation;
  91. }
  92. bindKnex(knex) {
  93. const bound = this.clone();
  94. bound.relatedModelClass = this.relatedModelClass.bindKnex(knex);
  95. bound.ownerModelClass = this.ownerModelClass.bindKnex(knex);
  96. if (this.joinTableModelClass) {
  97. bound.joinTableModelClass = this.joinTableModelClass.bindKnex(knex);
  98. }
  99. return bound;
  100. }
  101. findQuery(builder, opt) {
  102. const relatedRefs = this.relatedProp.refs(builder);
  103. if (opt.isColumnRef) {
  104. for (let i = 0, l = relatedRefs.length; i < l; ++i) {
  105. builder.where(relatedRefs[i], ref(opt.ownerIds[i]));
  106. }
  107. } else if (containsNonNull(opt.ownerIds)) {
  108. builder.whereInComposite(relatedRefs, opt.ownerIds);
  109. } else {
  110. builder.resolve([]);
  111. }
  112. return builder.modify(this.modify);
  113. }
  114. join(
  115. builder,
  116. {
  117. joinOperation = 'join',
  118. relatedTableAlias = this.relatedTableAlias(builder),
  119. relatedJoinSelectQuery = this.relatedModelClass.query().childQueryOf(builder),
  120. relatedTable = builder.tableNameFor(this.relatedModelClass),
  121. ownerTable = builder.tableRefFor(this.ownerModelClass)
  122. } = {}
  123. ) {
  124. let relatedSelect = relatedJoinSelectQuery.modify(this.modify).as(relatedTableAlias);
  125. if (relatedSelect.isSelectAll()) {
  126. // No need to join a subquery if the query is `select * from "RelatedTable"`.
  127. relatedSelect = `${relatedTable} as ${relatedTableAlias}`;
  128. }
  129. return builder[joinOperation](relatedSelect, join => {
  130. const relatedProp = this.relatedProp;
  131. const ownerProp = this.ownerProp;
  132. for (let i = 0, l = relatedProp.size; i < l; ++i) {
  133. const relatedRef = relatedProp.ref(builder, i).table(relatedTableAlias);
  134. const ownerRef = ownerProp.ref(builder, i).table(ownerTable);
  135. join.on(relatedRef, ownerRef);
  136. }
  137. });
  138. }
  139. insert(builder, owner) {
  140. /* istanbul ignore next */
  141. throw this.createError('not implemented');
  142. }
  143. update(builder, owner) {
  144. return new RelationUpdateOperation('update', {
  145. relation: this,
  146. owner: owner
  147. });
  148. }
  149. patch(builder, owner) {
  150. return new RelationUpdateOperation('patch', {
  151. relation: this,
  152. owner: owner,
  153. modelOptions: { patch: true }
  154. });
  155. }
  156. find(builder, owners) {
  157. return new RelationFindOperation('find', {
  158. relation: this,
  159. owners: owners
  160. });
  161. }
  162. subQuery(builder) {
  163. return new RelationSubqueryOperation('subQuery', {
  164. relation: this
  165. });
  166. }
  167. delete(builder, owner) {
  168. return new RelationDeleteOperation('delete', {
  169. relation: this,
  170. owner: owner
  171. });
  172. }
  173. relate(builder, owner) {
  174. /* istanbul ignore next */
  175. throw this.createError('not implemented');
  176. }
  177. unrelate(builder, owner) {
  178. /* istanbul ignore next */
  179. throw this.createError('not implemented');
  180. }
  181. hasRelateProp(model) {
  182. return model.$hasProps(this.relatedProp.props);
  183. }
  184. executeBeforeInsert(models, queryContext, result) {
  185. return mapAfterAllReturn(models, model => this.beforeInsert(model, queryContext), result);
  186. }
  187. createError(message) {
  188. if (this.ownerModelClass && this.ownerModelClass.name && this.name) {
  189. return new Error(`${this.ownerModelClass.name}.relationMappings.${this.name}: ${message}`);
  190. } else {
  191. return new Error(`${this.constructor.name}: ${message}`);
  192. }
  193. }
  194. }
  195. function checkForbiddenProperties(ctx) {
  196. ctx.forbiddenMappingProperties.forEach(prop => {
  197. if (get(ctx.mapping, prop.split('.')) !== undefined) {
  198. throw ctx.createError(`Property ${prop} is not supported for this relation type.`);
  199. }
  200. });
  201. return ctx;
  202. }
  203. function checkOwnerModelClass(ctx) {
  204. if (!isSubclassOf(ctx.ownerModelClass, getModel())) {
  205. throw ctx.createError(`Relation's owner is not a subclass of Model`);
  206. }
  207. return ctx;
  208. }
  209. function checkRelatedModelClass(ctx) {
  210. if (!ctx.mapping.modelClass) {
  211. throw ctx.createError('modelClass is not defined');
  212. }
  213. return ctx;
  214. }
  215. function resolveRelatedModelClass(ctx) {
  216. let relatedModelClass;
  217. try {
  218. relatedModelClass = resolveModel(
  219. ctx.mapping.modelClass,
  220. ctx.ownerModelClass.modelPaths,
  221. 'modelClass'
  222. );
  223. } catch (err) {
  224. throw ctx.createError(err.message);
  225. }
  226. return Object.assign(ctx, { relatedModelClass });
  227. }
  228. function checkRelation(ctx) {
  229. if (!ctx.mapping.relation) {
  230. throw ctx.createError('relation is not defined');
  231. }
  232. if (!isSubclassOf(ctx.mapping.relation, Relation)) {
  233. throw ctx.createError('relation is not a subclass of Relation');
  234. }
  235. return ctx;
  236. }
  237. function createJoinProperties(ctx) {
  238. const mapping = ctx.mapping;
  239. if (!mapping.join || !mapping.join.from || !mapping.join.to) {
  240. throw ctx.createError(
  241. 'join must be an object that maps the columns of the related models together. For example: {from: "SomeTable.id", to: "SomeOtherTable.someModelId"}'
  242. );
  243. }
  244. const fromProp = createRelationProperty(ctx, mapping.join.from, 'join.from');
  245. const toProp = createRelationProperty(ctx, mapping.join.to, 'join.to');
  246. let ownerProp;
  247. let relatedProp;
  248. if (fromProp.modelClass.getTableName() === ctx.ownerModelClass.getTableName()) {
  249. ownerProp = fromProp;
  250. relatedProp = toProp;
  251. } else if (toProp.modelClass.getTableName() === ctx.ownerModelClass.getTableName()) {
  252. ownerProp = toProp;
  253. relatedProp = fromProp;
  254. } else {
  255. throw ctx.createError('join: either `from` or `to` must point to the owner model table.');
  256. }
  257. if (ownerProp.props.some(it => it === ctx.name)) {
  258. throw ctx.createError(
  259. `join: relation name and join property '${
  260. ctx.name
  261. }' 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.`
  262. );
  263. }
  264. if (relatedProp.modelClass.getTableName() !== ctx.relatedModelClass.getTableName()) {
  265. throw ctx.createError('join: either `from` or `to` must point to the related model table.');
  266. }
  267. return Object.assign(ctx, { ownerProp, relatedProp });
  268. }
  269. function createRelationProperty(ctx, refString, propName) {
  270. try {
  271. return new RelationProperty(refString, table => {
  272. return [ctx.ownerModelClass, ctx.relatedModelClass].find(it => it.getTableName() === table);
  273. });
  274. } catch (err) {
  275. if (err instanceof RelationProperty.ModelNotFoundError) {
  276. throw ctx.createError(
  277. `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 '${
  278. err.tableName
  279. }' is not correct`
  280. );
  281. } else if (err instanceof RelationProperty.InvalidReferenceError) {
  282. throw ctx.createError(
  283. `${propName} must have format TableName.columnName. For example "SomeTable.id" or in case of composite key ["SomeTable.a", "SomeTable.b"].`
  284. );
  285. } else {
  286. throw err;
  287. }
  288. }
  289. }
  290. function parseModify(ctx) {
  291. const mapping = ctx.mapping;
  292. const value = mapping.modify || mapping.filter;
  293. let modify;
  294. if (value) {
  295. const type = typeof value;
  296. if (type === 'object') {
  297. modify = qb => qb.where(value);
  298. } else if (type === 'function') {
  299. modify = value;
  300. } else if (type === 'string') {
  301. const namedFilters = ctx.relatedModelClass.namedFilters;
  302. modify = namedFilters && namedFilters[value];
  303. if (!modify) {
  304. throw ctx.createError(`Could not find filter "${value}".`);
  305. }
  306. } else {
  307. throw ctx.createError(`Unable to determine modify function from provided value: "${value}".`);
  308. }
  309. } else {
  310. modify = () => {};
  311. }
  312. return Object.assign(ctx, { modify });
  313. }
  314. function parseBeforeInsert(ctx) {
  315. let beforeInsert;
  316. if (typeof ctx.mapping.beforeInsert === 'function') {
  317. beforeInsert = ctx.mapping.beforeInsert;
  318. } else {
  319. beforeInsert = model => model;
  320. }
  321. return Object.assign(ctx, { beforeInsert });
  322. }
  323. function containsNonNull(arr) {
  324. for (let i = 0, l = arr.length; i < l; ++i) {
  325. const val = arr[i];
  326. if (Array.isArray(val) && containsNonNull(val)) {
  327. return true;
  328. } else if (val !== null && val !== undefined) {
  329. return true;
  330. }
  331. }
  332. return false;
  333. }
  334. module.exports = Relation;