ManyToManyRelation.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. const getModel = () => require('../../model/Model');
  2. const Relation = require('../Relation');
  3. const RelationProperty = require('../RelationProperty');
  4. const { ref } = require('../../queryBuilder/ReferenceBuilder');
  5. const { isSqlite, isMySql } = require('../../utils/knexUtils');
  6. const { inheritModel } = require('../../model/inheritModel');
  7. const { resolveModel } = require('../../utils/resolveModel');
  8. const { mapAfterAllReturn } = require('../../utils/promiseUtils');
  9. const ManyToManyFindOperation = require('./find/ManyToManyFindOperation');
  10. const ManyToManyInsertOperation = require('./insert/ManyToManyInsertOperation');
  11. const ManyToManyRelateOperation = require('./relate/ManyToManyRelateOperation');
  12. const ManyToManyUnrelateOperation = require('./unrelate/ManyToManyUnrelateOperation');
  13. const ManyToManyUnrelateMySqlOperation = require('./unrelate/ManyToManyUnrelateMySqlOperation');
  14. const ManyToManyUnrelateSqliteOperation = require('./unrelate/ManyToManyUnrelateSqliteOperation');
  15. const ManyToManyUpdateOperation = require('./update/ManyToManyUpdateOperation');
  16. const ManyToManyUpdateMySqlOperation = require('./update/ManyToManyUpdateMySqlOperation');
  17. const ManyToManyUpdateSqliteOperation = require('./update/ManyToManyUpdateSqliteOperation');
  18. const ManyToManyDeleteOperation = require('./delete/ManyToManyDeleteOperation');
  19. const ManyToManyDeleteMySqlOperation = require('./delete/ManyToManyDeleteMySqlOperation');
  20. const ManyToManyDeleteSqliteOperation = require('./delete/ManyToManyDeleteSqliteOperation');
  21. class ManyToManyRelation extends Relation {
  22. setMapping(mapping) {
  23. const retVal = super.setMapping(mapping);
  24. let ctx = {
  25. mapping,
  26. ownerModelClass: this.ownerModelClass,
  27. relatedModelClass: this.relatedModelClass,
  28. ownerProp: this.ownerProp,
  29. relatedProp: this.relatedProp,
  30. joinTableModelClass: null,
  31. joinTableOwnerProp: null,
  32. joinTableRelatedProp: null,
  33. joinTableBeforeInsert: null,
  34. joinTableExtras: [],
  35. createError: msg => this.createError(msg)
  36. };
  37. ctx = checkThroughObject(ctx);
  38. ctx = resolveJoinModelClassIfDefined(ctx);
  39. ctx = createJoinProperties(ctx);
  40. ctx = parseExtras(ctx);
  41. ctx = parseBeforeInsert(ctx);
  42. ctx = finalizeJoinModelClass(ctx);
  43. this.joinTableExtras = ctx.joinTableExtras;
  44. this.joinTableModelClass = ctx.joinTableModelClass;
  45. this.joinTableOwnerProp = ctx.joinTableOwnerProp;
  46. this.joinTableRelatedProp = ctx.joinTableRelatedProp;
  47. this.joinTableBeforeInsert = ctx.joinTableBeforeInsert;
  48. return retVal;
  49. }
  50. get forbiddenMappingProperties() {
  51. return [];
  52. }
  53. joinTableAlias(builder) {
  54. const table = builder.tableRefFor(this.joinTableModelClass);
  55. return `${table}_rel_${this.name}`;
  56. }
  57. findQuery(builder, opt) {
  58. const joinTableOwnerRefs = this.joinTableOwnerProp.refs(builder);
  59. builder.join(this.joinTable, join => {
  60. for (let i = 0, l = this.relatedProp.size; i < l; ++i) {
  61. const relatedRef = this.relatedProp.ref(builder, i);
  62. const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i);
  63. join.on(relatedRef, joinTableRelatedRef);
  64. }
  65. });
  66. if (opt.isColumnRef) {
  67. for (let i = 0, l = joinTableOwnerRefs.length; i < l; ++i) {
  68. builder.where(joinTableOwnerRefs[i], ref(opt.ownerIds[i]));
  69. }
  70. } else if (containsNonNull(opt.ownerIds)) {
  71. builder.whereInComposite(joinTableOwnerRefs, opt.ownerIds);
  72. } else {
  73. builder.resolve([]);
  74. }
  75. return builder.modify(this.modify);
  76. }
  77. join(
  78. builder,
  79. {
  80. joinOperation = 'join',
  81. relatedTableAlias = this.relatedTableAlias(builder),
  82. relatedJoinSelectQuery = this.relatedModelClass.query().childQueryOf(builder),
  83. relatedTable = builder.tableNameFor(this.relatedModelClass),
  84. ownerTable = builder.tableRefFor(this.ownerModelClass),
  85. joinTableAlias = `${relatedTableAlias}_join`
  86. } = {}
  87. ) {
  88. const joinTableAsAlias = `${this.joinTable} as ${joinTableAlias}`;
  89. let relatedJoinSelect = relatedJoinSelectQuery.modify(this.modify).as(relatedTableAlias);
  90. if (relatedJoinSelect.isSelectAll()) {
  91. // No need to join a subquery if the query is `select * from "RelatedTable"`.
  92. relatedJoinSelect = `${relatedTable} as ${relatedTableAlias}`;
  93. }
  94. return builder[joinOperation](joinTableAsAlias, join => {
  95. const ownerProp = this.ownerProp;
  96. const joinTableOwnerProp = this.joinTableOwnerProp;
  97. for (let i = 0, l = ownerProp.size; i < l; ++i) {
  98. const joinTableOwnerRef = joinTableOwnerProp.ref(builder, i).table(joinTableAlias);
  99. const ownerRef = ownerProp.ref(builder, i).table(ownerTable);
  100. join.on(joinTableOwnerRef, ownerRef);
  101. }
  102. })[joinOperation](relatedJoinSelect, join => {
  103. const relatedProp = this.relatedProp;
  104. const joinTableRelatedProp = this.joinTableRelatedProp;
  105. for (let i = 0, l = relatedProp.size; i < l; ++i) {
  106. const joinTableRelatedRef = joinTableRelatedProp.ref(builder, i).table(joinTableAlias);
  107. const relatedRef = relatedProp.ref(builder, i).table(relatedTableAlias);
  108. join.on(joinTableRelatedRef, relatedRef);
  109. }
  110. });
  111. }
  112. find(builder, owners) {
  113. return new ManyToManyFindOperation('find', {
  114. relation: this,
  115. owners: owners
  116. });
  117. }
  118. insert(builder, owner) {
  119. return new ManyToManyInsertOperation('insert', {
  120. relation: this,
  121. owner: owner
  122. });
  123. }
  124. update(builder, owner) {
  125. if (isSqlite(builder.knex())) {
  126. return new ManyToManyUpdateSqliteOperation('update', {
  127. relation: this,
  128. owner: owner
  129. });
  130. } else if (isMySql(builder.knex())) {
  131. return new ManyToManyUpdateMySqlOperation('update', {
  132. relation: this,
  133. owner: owner
  134. });
  135. } else {
  136. return new ManyToManyUpdateOperation('update', {
  137. relation: this,
  138. owner: owner
  139. });
  140. }
  141. }
  142. patch(builder, owner) {
  143. if (isSqlite(builder.knex())) {
  144. return new ManyToManyUpdateSqliteOperation('patch', {
  145. modelOptions: { patch: true },
  146. relation: this,
  147. owner: owner
  148. });
  149. } else if (isMySql(builder.knex())) {
  150. return new ManyToManyUpdateMySqlOperation('patch', {
  151. modelOptions: { patch: true },
  152. relation: this,
  153. owner: owner
  154. });
  155. } else {
  156. return new ManyToManyUpdateOperation('patch', {
  157. modelOptions: { patch: true },
  158. relation: this,
  159. owner: owner
  160. });
  161. }
  162. }
  163. delete(builder, owner) {
  164. if (isSqlite(builder.knex())) {
  165. return new ManyToManyDeleteSqliteOperation('delete', {
  166. relation: this,
  167. owner: owner
  168. });
  169. } else if (isMySql(builder.knex())) {
  170. return new ManyToManyDeleteMySqlOperation('delete', {
  171. relation: this,
  172. owner: owner
  173. });
  174. } else {
  175. return new ManyToManyDeleteOperation('delete', {
  176. relation: this,
  177. owner: owner
  178. });
  179. }
  180. }
  181. relate(builder, owner) {
  182. return new ManyToManyRelateOperation('relate', {
  183. relation: this,
  184. owner: owner
  185. });
  186. }
  187. unrelate(builder, owner) {
  188. if (isSqlite(builder.knex())) {
  189. return new ManyToManyUnrelateSqliteOperation('unrelate', {
  190. relation: this,
  191. owner: owner
  192. });
  193. } else if (isMySql(builder.knex())) {
  194. return new ManyToManyUnrelateMySqlOperation('unrelate', {
  195. relation: this,
  196. owner: owner
  197. });
  198. } else {
  199. return new ManyToManyUnrelateOperation('unrelate', {
  200. relation: this,
  201. owner: owner
  202. });
  203. }
  204. }
  205. createJoinModels(ownerId, related) {
  206. const joinModels = new Array(related.length);
  207. for (let i = 0, lr = related.length; i < lr; ++i) {
  208. const rel = related[i];
  209. let joinModel = {};
  210. for (let j = 0, lp = this.joinTableOwnerProp.size; j < lp; ++j) {
  211. this.joinTableOwnerProp.setProp(joinModel, j, ownerId[j]);
  212. }
  213. for (let j = 0, lp = this.joinTableRelatedProp.size; j < lp; ++j) {
  214. this.joinTableRelatedProp.setProp(joinModel, j, this.relatedProp.getProp(rel, j));
  215. }
  216. for (let j = 0, lp = this.joinTableExtras.length; j < lp; ++j) {
  217. const extra = this.joinTableExtras[j];
  218. const extraValue = rel[extra.aliasProp];
  219. if (extraValue !== undefined) {
  220. joinModel[extra.joinTableProp] = extraValue;
  221. }
  222. }
  223. joinModels[i] = joinModel;
  224. }
  225. return joinModels;
  226. }
  227. omitExtraProps(models) {
  228. if (this.joinTableExtras && this.joinTableExtras.length) {
  229. const props = this.joinTableExtras.map(extra => extra.aliasProp);
  230. for (let i = 0, l = models.length; i < l; ++i) {
  231. const queryProps = models[i].$$queryProps;
  232. // Omit extra properties instead of deleting them from the models so that they can
  233. // be used in the `$before` and `$after` hooks.
  234. models[i].$omitFromDatabaseJson(props);
  235. if (queryProps) {
  236. // We can delete the query properties since they shouldn't be used by anything
  237. // other than `$toDatabaseJson()`.
  238. for (let j = 0; j < props.length; ++j) {
  239. const prop = props[j];
  240. if (prop in queryProps) {
  241. delete queryProps[prop];
  242. }
  243. }
  244. }
  245. }
  246. }
  247. }
  248. executeJoinTableBeforeInsert(models, queryContext, result) {
  249. return mapAfterAllReturn(
  250. models,
  251. model => this.joinTableBeforeInsert(model, queryContext),
  252. result
  253. );
  254. }
  255. }
  256. function checkThroughObject(ctx) {
  257. const mapping = ctx.mapping;
  258. if (!mapping.join.through || typeof mapping.join.through !== 'object') {
  259. throw ctx.createError('join must have a `through` object that describes the join table.');
  260. }
  261. if (!mapping.join.through.from || !mapping.join.through.to) {
  262. throw ctx.createError(
  263. 'join.through must be an object that describes the join table. For example: {from: "JoinTable.someId", to: "JoinTable.someOtherId"}'
  264. );
  265. }
  266. return ctx;
  267. }
  268. function resolveJoinModelClassIfDefined(ctx) {
  269. let joinTableModelClass = null;
  270. if (ctx.mapping.join.through.modelClass) {
  271. try {
  272. joinTableModelClass = resolveModel(
  273. ctx.mapping.join.through.modelClass,
  274. ctx.ownerModelClass.modelPaths,
  275. 'join.through.modelClass'
  276. );
  277. } catch (err) {
  278. throw ctx.createError(err.message);
  279. }
  280. }
  281. return Object.assign(ctx, { joinTableModelClass });
  282. }
  283. function createJoinProperties(ctx) {
  284. let ret;
  285. let fromProp;
  286. let toProp;
  287. let relatedProp;
  288. let ownerProp;
  289. ret = createRelationProperty(ctx, ctx.mapping.join.through.from, 'join.through.from');
  290. fromProp = ret.prop;
  291. ctx = ret.ctx;
  292. ret = createRelationProperty(ctx, ctx.mapping.join.through.to, 'join.through.to');
  293. toProp = ret.prop;
  294. ctx = ret.ctx;
  295. if (fromProp.modelClass.getTableName() !== toProp.modelClass.getTableName()) {
  296. throw ctx.createError('join.through `from` and `to` must point to the same join table.');
  297. }
  298. if (ctx.relatedProp.modelClass.getTableName() === fromProp.modelClass.getTableName()) {
  299. relatedProp = fromProp;
  300. ownerProp = toProp;
  301. } else {
  302. relatedProp = toProp;
  303. ownerProp = fromProp;
  304. }
  305. return Object.assign(ctx, {
  306. joinTableOwnerProp: ownerProp,
  307. joinTableRelatedProp: relatedProp
  308. });
  309. }
  310. function createRelationProperty(ctx, refString, messagePrefix) {
  311. let prop = null;
  312. let joinTableModelClass = ctx.joinTableModelClass;
  313. const resolveModelClass = table => {
  314. if (joinTableModelClass === null) {
  315. joinTableModelClass = inheritModel(getModel());
  316. joinTableModelClass.tableName = table;
  317. joinTableModelClass.idColumn = null;
  318. joinTableModelClass.concurrency = 1;
  319. }
  320. if (joinTableModelClass.getTableName() === table) {
  321. return joinTableModelClass;
  322. } else {
  323. return null;
  324. }
  325. };
  326. try {
  327. prop = new RelationProperty(refString, resolveModelClass);
  328. } catch (err) {
  329. if (err instanceof RelationProperty.ModelNotFoundError) {
  330. throw ctx.createError('join.through `from` and `to` must point to the same join table.');
  331. } else {
  332. throw ctx.createError(
  333. `${messagePrefix} must have format JoinTable.columnName. For example "JoinTable.someId" or in case of composite key ["JoinTable.a", "JoinTable.b"].`
  334. );
  335. }
  336. }
  337. return {
  338. ctx: Object.assign(ctx, { joinTableModelClass }),
  339. prop
  340. };
  341. }
  342. function parseExtras(ctx) {
  343. let extraDef = ctx.mapping.join.through.extra;
  344. if (!extraDef) {
  345. return ctx;
  346. }
  347. if (Array.isArray(extraDef)) {
  348. extraDef = extraDef.reduce((extraDef, col) => {
  349. extraDef[col] = col;
  350. return extraDef;
  351. }, {});
  352. }
  353. const joinTableExtras = Object.keys(extraDef).map(key => {
  354. const val = extraDef[key];
  355. return {
  356. joinTableCol: val,
  357. joinTableProp: ctx.joinTableModelClass.columnNameToPropertyName(val),
  358. aliasCol: key,
  359. aliasProp: ctx.joinTableModelClass.columnNameToPropertyName(key)
  360. };
  361. });
  362. return Object.assign(ctx, { joinTableExtras });
  363. }
  364. function parseBeforeInsert(ctx) {
  365. let joinTableBeforeInsert;
  366. if (typeof ctx.mapping.join.through.beforeInsert === 'function') {
  367. joinTableBeforeInsert = ctx.mapping.join.through.beforeInsert;
  368. } else {
  369. joinTableBeforeInsert = model => model;
  370. }
  371. return Object.assign(ctx, { joinTableBeforeInsert });
  372. }
  373. function finalizeJoinModelClass(ctx) {
  374. if (ctx.joinTableModelClass.getIdColumn() === null) {
  375. // We cannot know if the join table has a primary key. Therefore we set some
  376. // known column as the idColumn so that inserts will work.
  377. ctx.joinTableModelClass.idColumn = ctx.joinTableRelatedProp.cols;
  378. }
  379. return ctx;
  380. }
  381. function containsNonNull(arr) {
  382. for (let i = 0, l = arr.length; i < l; ++i) {
  383. const val = arr[i];
  384. if (Array.isArray(val) && containsNonNull(val)) {
  385. return true;
  386. } else if (val !== null && val !== undefined) {
  387. return true;
  388. }
  389. }
  390. return false;
  391. }
  392. module.exports = ManyToManyRelation;