scope.test.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. 'use strict';
  2. const chai = require('chai');
  3. const expect = chai.expect;
  4. const Support = require('../support');
  5. const { DataTypes, Op } = require('@sequelize/core');
  6. const upperFirst = require('lodash/upperFirst');
  7. describe(Support.getTestDialectTeaser('associations'), () => {
  8. describe('scope', () => {
  9. beforeEach(function () {
  10. this.Post = this.sequelize.define('post', {});
  11. this.Image = this.sequelize.define('image', {});
  12. this.Question = this.sequelize.define('question', {});
  13. this.Comment = this.sequelize.define('comment', {
  14. title: DataTypes.STRING,
  15. type: DataTypes.STRING,
  16. commentable: DataTypes.STRING,
  17. commentable_id: DataTypes.INTEGER,
  18. isMain: {
  19. field: 'is_main',
  20. type: DataTypes.BOOLEAN,
  21. defaultValue: false,
  22. },
  23. });
  24. this.Comment.prototype.getItem = function () {
  25. return this[`get${upperFirst(this.get('commentable'))}`]();
  26. };
  27. this.Post.addScope('withComments', {
  28. include: [
  29. {
  30. model: this.Comment,
  31. as: 'comments',
  32. },
  33. ],
  34. });
  35. this.Post.addScope('withMainComment', {
  36. include: [
  37. {
  38. model: this.Comment,
  39. as: 'mainComment',
  40. },
  41. ],
  42. });
  43. this.Post.hasMany(this.Comment, {
  44. foreignKey: 'commentable_id',
  45. scope: {
  46. commentable: 'post',
  47. },
  48. foreignKeyConstraints: false,
  49. });
  50. this.Post.hasMany(this.Comment, {
  51. foreignKey: 'commentable_id',
  52. as: 'coloredComments',
  53. scope: {
  54. commentable: 'post',
  55. type: { [Op.in]: ['blue', 'green'] },
  56. },
  57. foreignKeyConstraints: false,
  58. });
  59. this.Post.hasOne(this.Comment, {
  60. foreignKey: 'commentable_id',
  61. as: 'mainComment',
  62. scope: {
  63. commentable: 'post',
  64. isMain: true,
  65. },
  66. foreignKeyConstraints: false,
  67. });
  68. this.Comment.belongsTo(this.Post, {
  69. foreignKey: 'commentable_id',
  70. as: 'post',
  71. foreignKeyConstraints: false,
  72. });
  73. this.Image.hasMany(this.Comment, {
  74. foreignKey: 'commentable_id',
  75. scope: {
  76. commentable: 'image',
  77. },
  78. foreignKeyConstraints: false,
  79. });
  80. this.Comment.belongsTo(this.Image, {
  81. foreignKey: 'commentable_id',
  82. as: 'image',
  83. foreignKeyConstraints: false,
  84. });
  85. this.Question.hasMany(this.Comment, {
  86. foreignKey: 'commentable_id',
  87. scope: {
  88. commentable: 'question',
  89. },
  90. foreignKeyConstraints: false,
  91. inverse: {
  92. as: 'question',
  93. },
  94. });
  95. });
  96. describe('1:1', () => {
  97. it('should create, find and include associations with scope values', async function () {
  98. await this.sequelize.sync({ force: true });
  99. const [post1] = await Promise.all([
  100. this.Post.create(),
  101. this.Comment.create({
  102. title: 'I am a comment',
  103. }),
  104. this.Comment.create({
  105. title: 'I am a main comment',
  106. isMain: true,
  107. }),
  108. ]);
  109. this.post = post1;
  110. const comment0 = await post1.createComment({
  111. title: 'I am a post comment',
  112. });
  113. expect(comment0.get('commentable')).to.equal('post');
  114. expect(comment0.get('isMain')).to.be.false;
  115. const post0 = await this.Post.withScope('withMainComment').findByPk(this.post.get('id'));
  116. expect(post0.mainComment).to.be.null;
  117. const mainComment1 = await post0.createMainComment({
  118. title: 'I am a main post comment',
  119. });
  120. this.mainComment = mainComment1;
  121. expect(mainComment1.get('commentable')).to.equal('post');
  122. expect(mainComment1.get('isMain')).to.be.true;
  123. const post = await this.Post.withScope('withMainComment').findByPk(this.post.id);
  124. expect(post.mainComment.get('id')).to.equal(this.mainComment.get('id'));
  125. const mainComment0 = await post.getMainComment();
  126. expect(mainComment0.get('commentable')).to.equal('post');
  127. expect(mainComment0.get('isMain')).to.be.true;
  128. const comment = await this.Comment.create({
  129. title: 'I am a future main comment',
  130. });
  131. await this.post.setMainComment(comment);
  132. const mainComment = await this.post.getMainComment();
  133. expect(mainComment.get('commentable')).to.equal('post');
  134. expect(mainComment.get('isMain')).to.be.true;
  135. expect(mainComment.get('title')).to.equal('I am a future main comment');
  136. });
  137. it('should create included association with scope values', async function () {
  138. await this.sequelize.sync({ force: true });
  139. const post0 = await this.Post.create(
  140. {
  141. mainComment: {
  142. title: 'I am a main comment created with a post',
  143. },
  144. },
  145. {
  146. include: [{ model: this.Comment, as: 'mainComment' }],
  147. },
  148. );
  149. expect(post0.mainComment.get('commentable')).to.equal('post');
  150. expect(post0.mainComment.get('isMain')).to.be.true;
  151. const post = await this.Post.withScope('withMainComment').findByPk(post0.id);
  152. expect(post.mainComment.get('commentable')).to.equal('post');
  153. expect(post.mainComment.get('isMain')).to.be.true;
  154. });
  155. });
  156. describe('1:M', () => {
  157. it('should create, find and include associations with scope values', async function () {
  158. await this.sequelize.sync({ force: true });
  159. const [post1, image1, question1, commentA, commentB] = await Promise.all([
  160. this.Post.create(),
  161. this.Image.create(),
  162. this.Question.create(),
  163. this.Comment.create({
  164. title: 'I am a image comment',
  165. }),
  166. this.Comment.create({
  167. title: 'I am a question comment',
  168. }),
  169. ]);
  170. this.post = post1;
  171. this.image = image1;
  172. this.question = question1;
  173. await Promise.all([
  174. post1.createComment({
  175. title: 'I am a post comment',
  176. }),
  177. image1.addComment(commentA),
  178. question1.setComments([commentB]),
  179. ]);
  180. const comments = await this.Comment.findAll();
  181. for (const comment of comments) {
  182. expect(comment.get('commentable')).to.be.ok;
  183. }
  184. expect(
  185. comments
  186. .map(comment => {
  187. return comment.get('commentable');
  188. })
  189. .sort(),
  190. ).to.deep.equal(['image', 'post', 'question']);
  191. const [postComments, imageComments, questionComments] = await Promise.all([
  192. this.post.getComments(),
  193. this.image.getComments(),
  194. this.question.getComments(),
  195. ]);
  196. expect(postComments.length).to.equal(1);
  197. expect(postComments[0].get('title')).to.equal('I am a post comment');
  198. expect(imageComments.length).to.equal(1);
  199. expect(imageComments[0].get('title')).to.equal('I am a image comment');
  200. expect(questionComments.length).to.equal(1);
  201. expect(questionComments[0].get('title')).to.equal('I am a question comment');
  202. const [postComment, imageComment, questionComment] = [
  203. postComments[0],
  204. imageComments[0],
  205. questionComments[0],
  206. ];
  207. const [post0, image0, question0] = await Promise.all([
  208. postComment.getItem(),
  209. imageComment.getItem(),
  210. questionComment.getItem(),
  211. ]);
  212. expect(post0).to.be.instanceof(this.Post);
  213. expect(image0).to.be.instanceof(this.Image);
  214. expect(question0).to.be.instanceof(this.Question);
  215. const [post, image, question] = await Promise.all([
  216. this.Post.findOne({
  217. include: [
  218. {
  219. model: this.Comment,
  220. as: 'comments',
  221. },
  222. ],
  223. }),
  224. this.Image.findOne({
  225. include: [
  226. {
  227. model: this.Comment,
  228. as: 'comments',
  229. },
  230. ],
  231. }),
  232. this.Question.findOne({
  233. include: [
  234. {
  235. model: this.Comment,
  236. as: 'comments',
  237. },
  238. ],
  239. }),
  240. ]);
  241. expect(post.comments.length).to.equal(1);
  242. expect(post.comments[0].get('title')).to.equal('I am a post comment');
  243. expect(image.comments.length).to.equal(1);
  244. expect(image.comments[0].get('title')).to.equal('I am a image comment');
  245. expect(question.comments.length).to.equal(1);
  246. expect(question.comments[0].get('title')).to.equal('I am a question comment');
  247. });
  248. it('should make the same query if called multiple time (#4470)', async function () {
  249. const logs = [];
  250. const logging = function (log) {
  251. // removing 'executing(<uuid> || 'default'}) :' from logs
  252. logs.push(log.slice(Math.max(0, log.indexOf(':') + 1)));
  253. };
  254. await this.sequelize.sync({ force: true });
  255. const post = await this.Post.create();
  256. await post.createComment({
  257. title: 'I am a post comment',
  258. });
  259. await this.Post.withScope('withComments').findAll({
  260. logging,
  261. });
  262. await this.Post.withScope('withComments').findAll({
  263. logging,
  264. });
  265. expect(logs[0]).to.equal(logs[1]);
  266. });
  267. it('should created included association with scope values', async function () {
  268. await this.sequelize.sync({ force: true });
  269. let post = await this.Post.create(
  270. {
  271. comments: [
  272. {
  273. title: 'I am a comment created with a post',
  274. },
  275. {
  276. title: 'I am a second comment created with a post',
  277. },
  278. ],
  279. },
  280. {
  281. include: [{ model: this.Comment, as: 'comments' }],
  282. },
  283. );
  284. this.post = post;
  285. for (const comment of post.comments) {
  286. expect(comment.get('commentable')).to.equal('post');
  287. }
  288. post = await this.Post.withScope('withComments').findByPk(this.post.id);
  289. for (const comment of post.comments) {
  290. expect(comment.get('commentable')).to.equal('post');
  291. }
  292. });
  293. it('should include associations with operator scope values', async function () {
  294. await this.sequelize.sync({ force: true });
  295. const [post0, commentA, commentB, commentC] = await Promise.all([
  296. this.Post.create(),
  297. this.Comment.create({
  298. title: 'I am a blue comment',
  299. type: 'blue',
  300. }),
  301. this.Comment.create({
  302. title: 'I am a red comment',
  303. type: 'red',
  304. }),
  305. this.Comment.create({
  306. title: 'I am a green comment',
  307. type: 'green',
  308. }),
  309. ]);
  310. this.post = post0;
  311. await post0.addComments([commentA, commentB, commentC]);
  312. const post = await this.Post.findByPk(this.post.id, {
  313. include: [
  314. {
  315. model: this.Comment,
  316. as: 'coloredComments',
  317. },
  318. ],
  319. });
  320. expect(post.coloredComments.length).to.equal(2);
  321. for (const comment of post.coloredComments) {
  322. expect(comment.type).to.match(/blue|green/);
  323. }
  324. });
  325. it('should not mutate scope when running SELECT query (#12868)', async function () {
  326. await this.sequelize.sync({ force: true });
  327. await this.Post.findOne({
  328. where: {},
  329. include: [
  330. {
  331. association: this.Post.associations.mainComment,
  332. attributes: ['id'],
  333. required: true,
  334. where: {},
  335. },
  336. ],
  337. });
  338. expect(this.Post.associations.mainComment.scope.isMain).to.equal(true);
  339. });
  340. });
  341. if (Support.getTestDialect() !== 'sqlite3') {
  342. describe('N:M', () => {
  343. describe('on the target', () => {
  344. beforeEach(function () {
  345. this.Post = this.sequelize.define('post', {});
  346. this.Tag = this.sequelize.define('tag', {
  347. type: DataTypes.STRING,
  348. });
  349. this.PostTag = this.sequelize.define('post_tag');
  350. this.Post.belongsToMany(this.Tag, {
  351. as: 'categories',
  352. through: this.PostTag,
  353. scope: { type: 'category' },
  354. });
  355. this.Post.belongsToMany(this.Tag, {
  356. as: 'tags',
  357. through: this.PostTag,
  358. scope: { type: 'tag' },
  359. });
  360. return this.sequelize.sync({ force: true });
  361. });
  362. it('should create, find and include associations with scope values', async function () {
  363. const [postA0, postB0, postC0, categoryA, categoryB, tagA, tagB] = await Promise.all([
  364. this.Post.create(),
  365. this.Post.create(),
  366. this.Post.create(),
  367. this.Tag.create({ type: 'category' }),
  368. this.Tag.create({ type: 'category' }),
  369. this.Tag.create({ type: 'tag' }),
  370. this.Tag.create({ type: 'tag' }),
  371. ]);
  372. this.postA = postA0;
  373. this.postB = postB0;
  374. this.postC = postC0;
  375. await Promise.all([
  376. postA0.addCategory(categoryA),
  377. postA0.createTag(),
  378. postB0.addTag(tagA),
  379. postC0.createCategory(),
  380. ]);
  381. // we're calling 'setX' methods after the different 'addX' methods because
  382. // setCategories is not supposed to overwrite tags and vice-versa.
  383. // tags & categories use the same through table so this could happen is the association scope is not handled correctly.
  384. await postB0.setCategories([categoryB]);
  385. await postC0.setTags([tagB]);
  386. const [
  387. postACategories,
  388. postBCategories,
  389. postCCategories,
  390. postATags,
  391. postBTags,
  392. postCTags,
  393. ] = await Promise.all([
  394. this.postA.getCategories(),
  395. this.postB.getCategories(),
  396. this.postC.getCategories(),
  397. this.postA.getTags(),
  398. this.postB.getTags(),
  399. this.postC.getTags(),
  400. ]);
  401. expect([
  402. postACategories.length,
  403. postATags.length,
  404. postBCategories.length,
  405. postBTags.length,
  406. postCCategories.length,
  407. postCTags.length,
  408. ]).to.eql([1, 1, 1, 1, 1, 1]);
  409. expect([
  410. postACategories[0].get('type'),
  411. postATags[0].get('type'),
  412. postBCategories[0].get('type'),
  413. postBTags[0].get('type'),
  414. postCCategories[0].get('type'),
  415. postCTags[0].get('type'),
  416. ]).to.eql(['category', 'tag', 'category', 'tag', 'category', 'tag']);
  417. const [postA, postB, postC] = await Promise.all([
  418. this.Post.findOne({
  419. where: {
  420. id: this.postA.get('id'),
  421. },
  422. include: [
  423. { model: this.Tag, as: 'tags' },
  424. { model: this.Tag, as: 'categories' },
  425. ],
  426. }),
  427. this.Post.findOne({
  428. where: {
  429. id: this.postB.get('id'),
  430. },
  431. include: [
  432. { model: this.Tag, as: 'tags' },
  433. { model: this.Tag, as: 'categories' },
  434. ],
  435. }),
  436. this.Post.findOne({
  437. where: {
  438. id: this.postC.get('id'),
  439. },
  440. include: [
  441. { model: this.Tag, as: 'tags' },
  442. { model: this.Tag, as: 'categories' },
  443. ],
  444. }),
  445. ]);
  446. expect(postA.get('categories').length).to.equal(1);
  447. expect(postA.get('tags').length).to.equal(1);
  448. expect(postB.get('categories').length).to.equal(1);
  449. expect(postB.get('tags').length).to.equal(1);
  450. expect(postC.get('categories').length).to.equal(1);
  451. expect(postC.get('tags').length).to.equal(1);
  452. expect(postA.get('categories')[0].get('type')).to.equal('category');
  453. expect(postA.get('tags')[0].get('type')).to.equal('tag');
  454. expect(postB.get('categories')[0].get('type')).to.equal('category');
  455. expect(postB.get('tags')[0].get('type')).to.equal('tag');
  456. expect(postC.get('categories')[0].get('type')).to.equal('category');
  457. expect(postC.get('tags')[0].get('type')).to.equal('tag');
  458. });
  459. });
  460. describe('on the through model', () => {
  461. beforeEach(function () {
  462. this.Post = this.sequelize.define('post', {});
  463. this.Image = this.sequelize.define('image', {});
  464. this.Question = this.sequelize.define('question', {});
  465. this.ItemTag = this.sequelize.define('item_tag', {
  466. id: {
  467. type: DataTypes.INTEGER,
  468. primaryKey: true,
  469. autoIncrement: true,
  470. },
  471. tag_id: {
  472. type: DataTypes.INTEGER,
  473. unique: 'item_tag_taggable',
  474. },
  475. taggable: {
  476. type: DataTypes.STRING,
  477. unique: 'item_tag_taggable',
  478. },
  479. taggable_id: {
  480. type: DataTypes.INTEGER,
  481. unique: 'item_tag_taggable',
  482. references: null,
  483. },
  484. });
  485. this.Tag = this.sequelize.define('tag', {
  486. name: DataTypes.STRING,
  487. });
  488. this.Post.belongsToMany(this.Tag, {
  489. through: {
  490. model: this.ItemTag,
  491. unique: false,
  492. scope: {
  493. taggable: 'post',
  494. },
  495. },
  496. foreignKey: 'taggable_id',
  497. otherKey: 'tag_id',
  498. foreignKeyConstraints: false,
  499. inverse: {
  500. foreignKeyConstraints: false,
  501. },
  502. });
  503. this.Image.belongsToMany(this.Tag, {
  504. through: {
  505. model: this.ItemTag,
  506. unique: false,
  507. scope: {
  508. taggable: 'image',
  509. },
  510. },
  511. foreignKey: 'taggable_id',
  512. otherKey: 'tag_id',
  513. foreignKeyConstraints: false,
  514. inverse: {
  515. foreignKeyConstraints: false,
  516. },
  517. });
  518. this.Question.belongsToMany(this.Tag, {
  519. through: {
  520. model: this.ItemTag,
  521. unique: false,
  522. scope: {
  523. taggable: 'question',
  524. },
  525. },
  526. foreignKey: 'taggable_id',
  527. otherKey: 'tag_id',
  528. foreignKeyConstraints: false,
  529. inverse: {
  530. foreignKeyConstraints: false,
  531. },
  532. });
  533. });
  534. it('should create, find and include associations with scope values', async function () {
  535. await Promise.all([
  536. this.Post.sync({ force: true }),
  537. this.Image.sync({ force: true }),
  538. this.Question.sync({ force: true }),
  539. this.Tag.sync({ force: true }),
  540. ]);
  541. await this.ItemTag.sync({ force: true });
  542. const [post0, image0, question0, tagA, tagB, tagC] = await Promise.all([
  543. this.Post.create(),
  544. this.Image.create(),
  545. this.Question.create(),
  546. this.Tag.create({ name: 'tagA' }),
  547. this.Tag.create({ name: 'tagB' }),
  548. this.Tag.create({ name: 'tagC' }),
  549. ]);
  550. this.post = post0;
  551. this.image = image0;
  552. this.question = question0;
  553. await Promise.all([
  554. post0.setTags([tagA]).then(async () => {
  555. return Promise.all([post0.createTag({ name: 'postTag' }), post0.addTag(tagB)]);
  556. }),
  557. image0.setTags([tagB]).then(async () => {
  558. return Promise.all([image0.createTag({ name: 'imageTag' }), image0.addTag(tagC)]);
  559. }),
  560. question0.setTags([tagC]).then(async () => {
  561. return Promise.all([
  562. question0.createTag({ name: 'questionTag' }),
  563. question0.addTag(tagA),
  564. ]);
  565. }),
  566. ]);
  567. const [postTags, imageTags, questionTags] = await Promise.all([
  568. this.post.getTags(),
  569. this.image.getTags(),
  570. this.question.getTags(),
  571. ]);
  572. expect(postTags.length).to.equal(3);
  573. expect(imageTags.length).to.equal(3);
  574. expect(questionTags.length).to.equal(3);
  575. expect(
  576. postTags
  577. .map(tag => {
  578. return tag.name;
  579. })
  580. .sort(),
  581. ).to.deep.equal(['postTag', 'tagA', 'tagB']);
  582. expect(
  583. imageTags
  584. .map(tag => {
  585. return tag.name;
  586. })
  587. .sort(),
  588. ).to.deep.equal(['imageTag', 'tagB', 'tagC']);
  589. expect(
  590. questionTags
  591. .map(tag => {
  592. return tag.name;
  593. })
  594. .sort(),
  595. ).to.deep.equal(['questionTag', 'tagA', 'tagC']);
  596. const [post, image, question] = await Promise.all([
  597. this.Post.findOne({
  598. where: {},
  599. include: [this.Tag],
  600. }),
  601. this.Image.findOne({
  602. where: {},
  603. include: [this.Tag],
  604. }),
  605. this.Question.findOne({
  606. where: {},
  607. include: [this.Tag],
  608. }),
  609. ]);
  610. expect(post.tags.length).to.equal(3);
  611. expect(image.tags.length).to.equal(3);
  612. expect(question.tags.length).to.equal(3);
  613. expect(
  614. post.tags
  615. .map(tag => {
  616. return tag.name;
  617. })
  618. .sort(),
  619. ).to.deep.equal(['postTag', 'tagA', 'tagB']);
  620. expect(
  621. image.tags
  622. .map(tag => {
  623. return tag.name;
  624. })
  625. .sort(),
  626. ).to.deep.equal(['imageTag', 'tagB', 'tagC']);
  627. expect(
  628. question.tags
  629. .map(tag => {
  630. return tag.name;
  631. })
  632. .sort(),
  633. ).to.deep.equal(['questionTag', 'tagA', 'tagC']);
  634. });
  635. });
  636. });
  637. }
  638. });
  639. });