include.test.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. 'use strict';
  2. const chai = require('chai');
  3. const expect = chai.expect;
  4. const Support = require('../../support');
  5. const { DataTypes, Op, Sequelize } = require('@sequelize/core');
  6. const {
  7. _validateIncludedElements,
  8. } = require('@sequelize/core/_non-semver-use-at-your-own-risk_/model-internals.js');
  9. const current = Support.sequelize;
  10. describe(Support.getTestDialectTeaser('Model'), () => {
  11. describe('all', () => {
  12. it('can expand nested self-reference', () => {
  13. const Referral = current.define('referral');
  14. Referral.belongsTo(Referral);
  15. const options = { include: [{ all: true, nested: true }] };
  16. Referral._expandIncludeAll(options, Referral);
  17. expect(options.include).to.deep.equal([
  18. { as: 'referral', model: Referral, association: Referral.associations.referral },
  19. ]);
  20. });
  21. });
  22. describe('_validateIncludedElements', () => {
  23. beforeEach(function () {
  24. this.User = this.sequelize.define('User');
  25. this.Task = this.sequelize.define('Task', {
  26. title: DataTypes.STRING,
  27. });
  28. this.Company = this.sequelize.define('Company', {
  29. id: {
  30. type: DataTypes.INTEGER,
  31. primaryKey: true,
  32. autoIncrement: true,
  33. field: 'field_id',
  34. },
  35. name: DataTypes.STRING,
  36. });
  37. this.User.Tasks = this.User.hasMany(this.Task);
  38. this.User.Company = this.User.belongsTo(this.Company);
  39. this.Company.Employees = this.Company.hasMany(this.User);
  40. this.Company.Owner = this.Company.belongsTo(this.User, {
  41. as: 'Owner',
  42. foreignKey: 'ownerId',
  43. });
  44. });
  45. describe('attributes', () => {
  46. it("should not inject the aliased PK again, if it's already there", function () {
  47. let options = _validateIncludedElements({
  48. model: this.User,
  49. include: [
  50. {
  51. model: this.Company,
  52. attributes: ['name'],
  53. },
  54. ],
  55. });
  56. expect(options.include[0].attributes).to.deep.equal([['field_id', 'id'], 'name']);
  57. options = _validateIncludedElements(options);
  58. // Calling validate again shouldn't add the pk again
  59. expect(options.include[0].attributes).to.deep.equal([['field_id', 'id'], 'name']);
  60. });
  61. describe('include / exclude', () => {
  62. it('allows me to include additional attributes', function () {
  63. const options = _validateIncludedElements({
  64. model: this.User,
  65. include: [
  66. {
  67. model: this.Company,
  68. attributes: {
  69. include: ['foobar'],
  70. },
  71. },
  72. ],
  73. });
  74. expect(options.include[0].attributes).to.deep.equal([
  75. ['field_id', 'id'],
  76. 'name',
  77. 'createdAt',
  78. 'updatedAt',
  79. 'ownerId',
  80. 'foobar',
  81. ]);
  82. });
  83. it('allows me to exclude attributes', function () {
  84. const options = _validateIncludedElements({
  85. model: this.User,
  86. include: [
  87. {
  88. model: this.Company,
  89. attributes: {
  90. exclude: ['name'],
  91. },
  92. },
  93. ],
  94. });
  95. expect(options.include[0].attributes).to.deep.equal([
  96. ['field_id', 'id'],
  97. 'createdAt',
  98. 'updatedAt',
  99. 'ownerId',
  100. ]);
  101. });
  102. it('include takes precendence over exclude', function () {
  103. const options = _validateIncludedElements({
  104. model: this.User,
  105. include: [
  106. {
  107. model: this.Company,
  108. attributes: {
  109. exclude: ['name'],
  110. include: ['name'],
  111. },
  112. },
  113. ],
  114. });
  115. expect(options.include[0].attributes).to.deep.equal([
  116. ['field_id', 'id'],
  117. 'createdAt',
  118. 'updatedAt',
  119. 'ownerId',
  120. 'name',
  121. ]);
  122. });
  123. });
  124. });
  125. describe('scope', () => {
  126. beforeEach(function () {
  127. this.Project = this.sequelize.define(
  128. 'project',
  129. {
  130. bar: {
  131. type: DataTypes.STRING,
  132. field: 'foo',
  133. },
  134. },
  135. {
  136. defaultScope: {
  137. where: {
  138. active: true,
  139. },
  140. },
  141. scopes: {
  142. this: {
  143. where: { this: true },
  144. },
  145. that: {
  146. where: { that: false },
  147. limit: 12,
  148. },
  149. attr: {
  150. attributes: ['baz'],
  151. },
  152. foobar: {
  153. where: {
  154. bar: 42,
  155. },
  156. },
  157. },
  158. },
  159. );
  160. this.User.hasMany(this.Project);
  161. this.User.hasMany(this.Project.withScope('this'), { as: 'thisProject' });
  162. });
  163. it('adds the default scope to where', function () {
  164. const options = _validateIncludedElements({
  165. model: this.User,
  166. include: [{ model: this.Project, as: 'projects' }],
  167. });
  168. expect(options.include[0]).to.have.property('where').which.deep.equals({ active: true });
  169. });
  170. it('adds the where from a scoped model', function () {
  171. const options = _validateIncludedElements({
  172. model: this.User,
  173. include: [{ model: this.Project.withScope('that'), as: 'projects' }],
  174. });
  175. expect(options.include[0]).to.have.property('where').which.deep.equals({ that: false });
  176. expect(options.include[0]).to.have.property('limit').which.equals(12);
  177. });
  178. it('adds the attributes from a scoped model', function () {
  179. const options = _validateIncludedElements({
  180. model: this.User,
  181. include: [{ model: this.Project.withScope('attr'), as: 'projects' }],
  182. });
  183. expect(options.include[0]).to.have.property('attributes').which.deep.equals(['baz']);
  184. });
  185. it('merges where with the where from a scoped model', function () {
  186. const options = _validateIncludedElements({
  187. model: this.User,
  188. include: [
  189. { where: { active: false }, model: this.Project.withScope('that'), as: 'projects' },
  190. ],
  191. });
  192. expect(options.include[0].where).to.deep.equal({
  193. [Op.and]: [{ that: false }, { active: false }],
  194. });
  195. });
  196. it('add the where from a scoped associated model', function () {
  197. const options = _validateIncludedElements({
  198. model: this.User,
  199. include: [{ model: this.Project, as: 'thisProject' }],
  200. });
  201. expect(options.include[0]).to.have.property('where').which.deep.equals({ this: true });
  202. });
  203. it('handles a scope with an aliased column (.field)', function () {
  204. const options = _validateIncludedElements({
  205. model: this.User,
  206. include: [{ model: this.Project.withScope('foobar'), as: 'projects' }],
  207. });
  208. expect(options.include[0]).to.have.property('where').which.deep.equals({ bar: 42 });
  209. });
  210. });
  211. describe('duplicating', () => {
  212. it('should tag a hasMany association as duplicating: true if undefined', function () {
  213. const options = _validateIncludedElements({
  214. model: this.User,
  215. include: [this.User.Tasks],
  216. });
  217. expect(options.include[0].duplicating).to.equal(true);
  218. });
  219. it('should respect include.duplicating for a hasMany', function () {
  220. const options = _validateIncludedElements({
  221. model: this.User,
  222. include: [{ association: this.User.Tasks, duplicating: false }],
  223. });
  224. expect(options.include[0].duplicating).to.equal(false);
  225. });
  226. });
  227. describe('_conformInclude', () => {
  228. it('should expand association from string alias', function () {
  229. const options = {
  230. include: ['Owner'],
  231. };
  232. Sequelize.Model._conformIncludes(options, this.Company);
  233. expect(options.include[0]).to.deep.equal({
  234. model: this.User,
  235. association: this.Company.Owner,
  236. as: 'Owner',
  237. });
  238. });
  239. it('should expand string association', function () {
  240. const options = {
  241. include: [
  242. {
  243. association: 'Owner',
  244. attributes: ['id'],
  245. },
  246. ],
  247. };
  248. Sequelize.Model._conformIncludes(options, this.Company);
  249. expect(options.include[0]).to.deep.equal({
  250. model: this.User,
  251. association: this.Company.Owner,
  252. attributes: ['id'],
  253. as: 'Owner',
  254. });
  255. });
  256. it('should throw an error if invalid model is passed', function () {
  257. const options = {
  258. include: [
  259. {
  260. model: null,
  261. },
  262. ],
  263. };
  264. expect(() => {
  265. Sequelize.Model._conformIncludes(options, this.Company);
  266. }).to.throw(
  267. 'Invalid Include received. Include has to be either a Model, an Association, the name of an association, or a plain object compatible with IncludeOptions.',
  268. );
  269. });
  270. it('should throw an error if invalid association is passed', function () {
  271. const options = {
  272. include: [
  273. {
  274. association: null,
  275. },
  276. ],
  277. };
  278. expect(() => {
  279. Sequelize.Model._conformIncludes(options, this.Company);
  280. }).to.throw(
  281. 'Invalid Include received. Include has to be either a Model, an Association, the name of an association, or a plain object compatible with IncludeOptions.',
  282. );
  283. });
  284. });
  285. describe('getAssociationWithModel', () => {
  286. it('returns an association when there is a single unaliased association', function () {
  287. expect(this.User.getAssociationWithModel(this.Task)).to.equal(this.User.Tasks);
  288. });
  289. it('returns an association when there is a single aliased association', function () {
  290. const User = this.sequelize.define('User');
  291. const Task = this.sequelize.define('Task');
  292. const Tasks = Task.belongsTo(User, { as: 'owner' });
  293. expect(Task.getAssociationWithModel(User, 'owner')).to.equal(Tasks);
  294. });
  295. it('returns an association when there are multiple aliased associations', function () {
  296. expect(this.Company.getAssociationWithModel(this.User, 'Owner')).to.equal(
  297. this.Company.Owner,
  298. );
  299. });
  300. });
  301. describe('subQuery', () => {
  302. it('should be true if theres a duplicating association', function () {
  303. const options = _validateIncludedElements({
  304. model: this.User,
  305. include: [{ association: this.User.Tasks }],
  306. limit: 3,
  307. });
  308. expect(options.subQuery).to.equal(true);
  309. });
  310. it('should be false if theres a duplicating association but no limit', function () {
  311. const options = _validateIncludedElements({
  312. model: this.User,
  313. include: [{ association: this.User.Tasks }],
  314. limit: null,
  315. });
  316. expect(options.subQuery).to.equal(false);
  317. });
  318. it('should be true if theres a nested duplicating association', function () {
  319. const options = _validateIncludedElements({
  320. model: this.User,
  321. include: [
  322. {
  323. association: this.User.Company,
  324. include: [this.Company.Employees],
  325. },
  326. ],
  327. limit: 3,
  328. });
  329. expect(options.subQuery).to.equal(true);
  330. });
  331. it('should be false if theres a nested duplicating association but no limit', function () {
  332. const options = _validateIncludedElements({
  333. model: this.User,
  334. include: [
  335. {
  336. association: this.User.Company,
  337. include: [this.Company.Employees],
  338. },
  339. ],
  340. limit: null,
  341. });
  342. expect(options.subQuery).to.equal(false);
  343. });
  344. it('should tag a required hasMany association', function () {
  345. const options = _validateIncludedElements({
  346. model: this.User,
  347. include: [{ association: this.User.Tasks, required: true }],
  348. limit: 3,
  349. });
  350. expect(options.subQuery).to.equal(true);
  351. expect(options.include[0].subQuery).to.equal(false);
  352. expect(options.include[0].subQueryFilter).to.equal(true);
  353. });
  354. it('should not tag a required hasMany association with duplicating false', function () {
  355. const options = _validateIncludedElements({
  356. model: this.User,
  357. include: [{ association: this.User.Tasks, required: true, duplicating: false }],
  358. limit: 3,
  359. });
  360. expect(options.subQuery).to.equal(false);
  361. expect(options.include[0].subQuery).to.equal(false);
  362. expect(options.include[0].subQueryFilter).to.equal(false);
  363. });
  364. it('should not tag a separate hasMany association with subQuery true', function () {
  365. const options = _validateIncludedElements({
  366. model: this.Company,
  367. include: [
  368. {
  369. association: this.Company.Employees,
  370. separate: true,
  371. include: [{ association: this.User.Tasks, required: true }],
  372. },
  373. ],
  374. required: true,
  375. });
  376. expect(options.subQuery).to.equal(false);
  377. expect(options.include[0].subQuery).to.equal(false);
  378. expect(options.include[0].subQueryFilter).to.equal(false);
  379. });
  380. it('should tag a hasMany association with where', function () {
  381. const options = _validateIncludedElements({
  382. model: this.User,
  383. include: [{ association: this.User.Tasks, where: { title: Math.random().toString() } }],
  384. limit: 3,
  385. });
  386. expect(options.subQuery).to.equal(true);
  387. expect(options.include[0].subQuery).to.equal(false);
  388. expect(options.include[0].subQueryFilter).to.equal(true);
  389. });
  390. it('should not tag a hasMany association with where and duplicating false', function () {
  391. const options = _validateIncludedElements({
  392. model: this.User,
  393. include: [
  394. {
  395. association: this.User.Tasks,
  396. where: { title: Math.random().toString() },
  397. duplicating: false,
  398. },
  399. ],
  400. limit: 3,
  401. });
  402. expect(options.subQuery).to.equal(false);
  403. expect(options.include[0].subQuery).to.equal(false);
  404. expect(options.include[0].subQueryFilter).to.equal(false);
  405. });
  406. it('should tag a required belongsTo alongside a duplicating association', function () {
  407. const options = _validateIncludedElements({
  408. model: this.User,
  409. include: [
  410. { association: this.User.Company, required: true },
  411. { association: this.User.Tasks },
  412. ],
  413. limit: 3,
  414. });
  415. expect(options.subQuery).to.equal(true);
  416. expect(options.include[0].subQuery).to.equal(true);
  417. });
  418. it('should not tag a required belongsTo alongside a duplicating association with duplicating false', function () {
  419. const options = _validateIncludedElements({
  420. model: this.User,
  421. include: [
  422. { association: this.User.Company, required: true },
  423. { association: this.User.Tasks, duplicating: false },
  424. ],
  425. limit: 3,
  426. });
  427. expect(options.subQuery).to.equal(false);
  428. expect(options.include[0].subQuery).to.equal(false);
  429. });
  430. it('should tag a belongsTo association with where alongside a duplicating association', function () {
  431. const options = _validateIncludedElements({
  432. model: this.User,
  433. include: [
  434. { association: this.User.Company, where: { name: Math.random().toString() } },
  435. { association: this.User.Tasks },
  436. ],
  437. limit: 3,
  438. });
  439. expect(options.subQuery).to.equal(true);
  440. expect(options.include[0].subQuery).to.equal(true);
  441. });
  442. it('should tag a required belongsTo association alongside a duplicating association with a nested belongsTo', function () {
  443. const options = _validateIncludedElements({
  444. model: this.User,
  445. include: [
  446. {
  447. association: this.User.Company,
  448. required: true,
  449. include: [this.Company.Owner],
  450. },
  451. this.User.Tasks,
  452. ],
  453. limit: 3,
  454. });
  455. expect(options.subQuery).to.equal(true);
  456. expect(options.include[0].subQuery).to.equal(true);
  457. expect(options.include[0].include[0].subQuery).to.equal(false);
  458. expect(options.include[0].include[0].parent.subQuery).to.equal(true);
  459. });
  460. it('should tag a belongsTo association with where alongside a duplicating association with duplicating false', function () {
  461. const options = _validateIncludedElements({
  462. model: this.User,
  463. include: [
  464. { association: this.User.Company, where: { name: Math.random().toString() } },
  465. { association: this.User.Tasks, duplicating: false },
  466. ],
  467. limit: 3,
  468. });
  469. expect(options.subQuery).to.equal(false);
  470. expect(options.include[0].subQuery).to.equal(false);
  471. });
  472. });
  473. });
  474. });