associations.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. 'use strict';
  2. const { expect } = require('chai');
  3. const { DataTypes, Op } = require('@sequelize/core');
  4. describe('Model scope with associations', () => {
  5. beforeEach(async function () {
  6. const sequelize = this.sequelize;
  7. this.ScopeMe = this.sequelize.define(
  8. 'ScopeMe',
  9. {
  10. username: DataTypes.STRING,
  11. email: DataTypes.STRING,
  12. access_level: DataTypes.INTEGER,
  13. other_value: DataTypes.INTEGER,
  14. parent_id: DataTypes.INTEGER,
  15. },
  16. {
  17. defaultScope: {
  18. where: {
  19. access_level: {
  20. [Op.gte]: 5,
  21. },
  22. },
  23. },
  24. scopes: {
  25. isTony: {
  26. where: {
  27. username: 'tony',
  28. },
  29. },
  30. includeActiveProjects() {
  31. return {
  32. include: [
  33. {
  34. model: sequelize.models.getOrThrow('company'),
  35. include: [sequelize.models.getOrThrow('project').withScope('active')],
  36. },
  37. ],
  38. };
  39. },
  40. },
  41. },
  42. );
  43. this.Project = this.sequelize.define(
  44. 'project',
  45. {
  46. active: DataTypes.BOOLEAN,
  47. },
  48. {
  49. scopes: {
  50. active: {
  51. where: {
  52. active: true,
  53. },
  54. },
  55. },
  56. },
  57. );
  58. this.Company = this.sequelize.define(
  59. 'company',
  60. {
  61. active: DataTypes.BOOLEAN,
  62. },
  63. {
  64. defaultScope: {
  65. where: { active: true },
  66. },
  67. scopes: {
  68. notActive: {
  69. where: {
  70. active: false,
  71. },
  72. },
  73. reversed: {
  74. order: [['id', 'DESC']],
  75. },
  76. },
  77. },
  78. );
  79. this.Profile = this.sequelize.define(
  80. 'profile',
  81. {
  82. active: DataTypes.BOOLEAN,
  83. },
  84. {
  85. defaultScope: {
  86. where: { active: true },
  87. },
  88. scopes: {
  89. notActive: {
  90. where: {
  91. active: false,
  92. },
  93. },
  94. },
  95. },
  96. );
  97. this.Project.belongsToMany(this.Company, { through: 'CompanyProjects' });
  98. this.Company.belongsToMany(this.Project, { through: 'CompanyProjects' });
  99. this.ScopeMe.hasOne(this.Profile, { foreignKey: 'userId' });
  100. this.ScopeMe.belongsTo(this.Company);
  101. this.UserAssociation = this.Company.hasMany(this.ScopeMe, { as: 'users' });
  102. await this.sequelize.sync({ force: true });
  103. const [u1, u2, u3, u4, u5, c1, c2] = await Promise.all([
  104. this.ScopeMe.create({
  105. id: 1,
  106. username: 'dan',
  107. email: 'dan@sequelizejs.com',
  108. access_level: 5,
  109. other_value: 10,
  110. parent_id: 1,
  111. }),
  112. this.ScopeMe.create({
  113. id: 2,
  114. username: 'tobi',
  115. email: 'tobi@fakeemail.com',
  116. access_level: 10,
  117. other_value: 11,
  118. parent_id: 2,
  119. }),
  120. this.ScopeMe.create({
  121. id: 3,
  122. username: 'tony',
  123. email: 'tony@sequelizejs.com',
  124. access_level: 3,
  125. other_value: 7,
  126. parent_id: 1,
  127. }),
  128. this.ScopeMe.create({
  129. id: 4,
  130. username: 'fred',
  131. email: 'fred@foobar.com',
  132. access_level: 3,
  133. other_value: 7,
  134. parent_id: 1,
  135. }),
  136. this.ScopeMe.create({
  137. id: 5,
  138. username: 'bob',
  139. email: 'bob@foobar.com',
  140. access_level: 1,
  141. other_value: 9,
  142. parent_id: 5,
  143. }),
  144. this.Company.create({ id: 1, active: true }),
  145. this.Company.create({ id: 2, active: false }),
  146. ]);
  147. await Promise.all([c1.setUsers([u1, u2, u3, u4]), c2.setUsers([u5])]);
  148. });
  149. describe('include', () => {
  150. it('should scope columns properly', async function () {
  151. // Will error with ambigous column if id is not scoped properly to `Company`.`id`
  152. await expect(
  153. this.Company.findAll({
  154. where: { id: 1 },
  155. include: [this.UserAssociation],
  156. }),
  157. ).not.to.be.rejected;
  158. });
  159. it('should apply default scope when including an associations', async function () {
  160. const obj = await this.Company.findAll({
  161. include: [this.UserAssociation],
  162. });
  163. const company = await obj[0];
  164. expect(company.users).to.have.length(2);
  165. });
  166. it('should apply default scope when including a model', async function () {
  167. const obj = await this.Company.findAll({
  168. include: [{ model: this.ScopeMe, as: 'users' }],
  169. });
  170. const company = await obj[0];
  171. expect(company.users).to.have.length(2);
  172. });
  173. it('should be able to include a scoped model', async function () {
  174. const obj = await this.Company.findAll({
  175. include: [{ model: this.ScopeMe.withScope('isTony'), as: 'users' }],
  176. });
  177. const company = await obj[0];
  178. expect(company.users).to.have.length(1);
  179. expect(company.users[0].get('username')).to.equal('tony');
  180. });
  181. });
  182. describe('get', () => {
  183. beforeEach(async function () {
  184. const [p, companies] = await Promise.all([
  185. this.Project.create(),
  186. this.Company.withoutScope().findAll(),
  187. ]);
  188. await p.setCompanies(companies);
  189. });
  190. describe('it should be able to unscope', () => {
  191. it('hasMany', async function () {
  192. const company = await this.Company.findByPk(1);
  193. const users = await company.getUsers({ scope: false });
  194. expect(users).to.have.length(4);
  195. });
  196. it('hasOne', async function () {
  197. await this.Profile.create({
  198. active: false,
  199. userId: 1,
  200. });
  201. const user = await this.ScopeMe.findByPk(1);
  202. const profile = await user.getProfile({ scope: false });
  203. expect(profile).to.be.ok;
  204. });
  205. it('belongsTo', async function () {
  206. const user = await this.ScopeMe.withoutScope().findOne({ where: { username: 'bob' } });
  207. const company = await user.getCompany({ scope: false });
  208. expect(company).to.be.ok;
  209. });
  210. it('belongsToMany', async function () {
  211. const obj = await this.Project.findAll();
  212. const p = await obj[0];
  213. const companies = await p.getCompanies({ scope: false });
  214. expect(companies).to.have.length(2);
  215. });
  216. });
  217. describe('it should apply default scope', () => {
  218. it('hasMany', async function () {
  219. const company = await this.Company.findByPk(1);
  220. const users = await company.getUsers();
  221. expect(users).to.have.length(2);
  222. });
  223. it('hasOne', async function () {
  224. await this.Profile.create({
  225. active: false,
  226. userId: 1,
  227. });
  228. const user = await this.ScopeMe.findByPk(1);
  229. const profile = await user.getProfile();
  230. expect(profile).not.to.be.ok;
  231. });
  232. it('belongsTo', async function () {
  233. const user = await this.ScopeMe.withoutScope().findOne({ where: { username: 'bob' } });
  234. const company = await user.getCompany();
  235. expect(company).not.to.be.ok;
  236. });
  237. it('belongsToMany', async function () {
  238. const obj = await this.Project.findAll();
  239. const p = await obj[0];
  240. const companies = await p.getCompanies();
  241. expect(companies).to.have.length(1);
  242. expect(companies[0].get('active')).to.be.ok;
  243. });
  244. });
  245. describe('it should be able to apply another scope', () => {
  246. it('hasMany', async function () {
  247. const company = await this.Company.findByPk(1);
  248. const users = await company.getUsers({ scope: 'isTony' });
  249. expect(users).to.have.length(1);
  250. expect(users[0].get('username')).to.equal('tony');
  251. });
  252. it('hasOne', async function () {
  253. await this.Profile.create({
  254. active: true,
  255. userId: 1,
  256. });
  257. const user = await this.ScopeMe.findByPk(1);
  258. const profile = await user.getProfile({ scope: 'notActive' });
  259. expect(profile).not.to.be.ok;
  260. });
  261. it('belongsTo', async function () {
  262. const user = await this.ScopeMe.withoutScope().findOne({ where: { username: 'bob' } });
  263. const company = await user.getCompany({ scope: 'notActive' });
  264. expect(company).to.be.ok;
  265. });
  266. it('belongsToMany', async function () {
  267. const obj = await this.Project.findAll();
  268. const p = await obj[0];
  269. const companies = await p.getCompanies({ scope: 'reversed' });
  270. expect(companies).to.have.length(2);
  271. expect(companies[0].id).to.equal(2);
  272. expect(companies[1].id).to.equal(1);
  273. });
  274. });
  275. });
  276. describe('scope with includes', () => {
  277. beforeEach(async function () {
  278. const [c, p1, p2] = await Promise.all([
  279. this.Company.findByPk(1),
  280. this.Project.create({ id: 1, active: true }),
  281. this.Project.create({ id: 2, active: false }),
  282. ]);
  283. await c.setProjects([p1, p2]);
  284. });
  285. it('should scope columns properly', async function () {
  286. await expect(this.ScopeMe.withScope('includeActiveProjects').findAll()).not.to.be.rejected;
  287. });
  288. it('should apply scope conditions', async function () {
  289. const user = await this.ScopeMe.withScope('includeActiveProjects').findOne({
  290. where: { id: 1 },
  291. });
  292. expect(user.company.projects).to.have.length(1);
  293. });
  294. describe('with different format', () => {
  295. it('should not throw error', async function () {
  296. const Child = this.sequelize.define('Child');
  297. const Parent = this.sequelize.define(
  298. 'Parent',
  299. {},
  300. {
  301. defaultScope: {
  302. include: [{ model: Child }],
  303. },
  304. scopes: {
  305. children: {
  306. include: [Child],
  307. },
  308. },
  309. },
  310. );
  311. Parent.addScope('alsoChildren', {
  312. include: [{ model: Child }],
  313. });
  314. Child.belongsTo(Parent);
  315. Parent.hasOne(Child);
  316. await this.sequelize.sync({ force: true });
  317. const [child, parent] = await Promise.all([Child.create(), Parent.create()]);
  318. await parent.setChild(child);
  319. await Parent.withScope('children', 'alsoChildren').findOne();
  320. });
  321. });
  322. describe('with find options', () => {
  323. it('should merge includes correctly', async function () {
  324. const Child = this.sequelize.define('Child', { name: DataTypes.STRING });
  325. const Parent = this.sequelize.define('Parent', { name: DataTypes.STRING });
  326. Parent.addScope('testScope1', {
  327. include: [
  328. {
  329. model: Child,
  330. where: {
  331. name: 'child2',
  332. },
  333. },
  334. ],
  335. });
  336. Parent.hasMany(Child);
  337. await this.sequelize.sync({ force: true });
  338. await Promise.all([
  339. Parent.create({ name: 'parent1' }).then(parent => parent.createChild({ name: 'child1' })),
  340. Parent.create({ name: 'parent2' }).then(parent => parent.createChild({ name: 'child2' })),
  341. ]);
  342. const parent = await Parent.withScope('testScope1').findOne({
  343. include: [
  344. {
  345. model: Child,
  346. attributes: { exclude: ['name'] },
  347. },
  348. ],
  349. });
  350. expect(parent.get('name')).to.equal('parent2');
  351. expect(parent.children).to.have.length(1);
  352. expect(parent.children[0].dataValues).not.to.have.property('name');
  353. });
  354. });
  355. });
  356. describe('scope with options', () => {
  357. it('should return correct object included foreign_key', async function () {
  358. const Child = this.sequelize.define(
  359. 'Child',
  360. {
  361. secret: DataTypes.STRING,
  362. },
  363. {
  364. scopes: {
  365. public: {
  366. attributes: {
  367. exclude: ['secret'],
  368. },
  369. },
  370. },
  371. },
  372. );
  373. const Parent = this.sequelize.define('Parent');
  374. Child.belongsTo(Parent);
  375. Parent.hasOne(Child);
  376. await this.sequelize.sync({ force: true });
  377. await Child.create({ secret: 'super secret' });
  378. const user = await Child.withScope('public').findOne();
  379. expect(user.dataValues).to.have.property('parentId');
  380. expect(user.dataValues).not.to.have.property('secret');
  381. });
  382. it('should return correct object included foreign_key with defaultScope', async function () {
  383. const Child = this.sequelize.define(
  384. 'Child',
  385. {
  386. secret: DataTypes.STRING,
  387. },
  388. {
  389. defaultScope: {
  390. attributes: {
  391. exclude: ['secret'],
  392. },
  393. },
  394. },
  395. );
  396. const Parent = this.sequelize.define('Parent');
  397. Child.belongsTo(Parent);
  398. await this.sequelize.sync({ force: true });
  399. await Child.create({ secret: 'super secret' });
  400. const user = await Child.findOne();
  401. expect(user.dataValues).to.have.property('parentId');
  402. expect(user.dataValues).not.to.have.property('secret');
  403. });
  404. it('should not throw error', async function () {
  405. const Clientfile = this.sequelize.define('clientfile');
  406. const Mission = this.sequelize.define('mission', { secret: DataTypes.STRING });
  407. const Building = this.sequelize.define('building');
  408. const MissionAssociation = Clientfile.hasOne(Mission);
  409. const BuildingAssociation = Clientfile.hasOne(Building);
  410. await this.sequelize.sync({ force: true });
  411. await Clientfile.findAll({
  412. include: [
  413. {
  414. association: 'mission',
  415. where: {
  416. secret: 'foo',
  417. },
  418. },
  419. {
  420. association: 'building',
  421. },
  422. ],
  423. });
  424. await Clientfile.findAll({
  425. include: [
  426. {
  427. association: MissionAssociation,
  428. where: {
  429. secret: 'foo',
  430. },
  431. },
  432. {
  433. association: BuildingAssociation,
  434. },
  435. ],
  436. });
  437. });
  438. });
  439. });