reload.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import type {
  2. CreationOptional,
  3. HasManyCreateAssociationMixin,
  4. HasManySetAssociationsMixin,
  5. InferAttributes,
  6. InferCreationAttributes,
  7. NonAttribute,
  8. } from '@sequelize/core';
  9. import { DataTypes, InstanceError, Model } from '@sequelize/core';
  10. import { Attribute, BelongsTo, HasMany, NotNull, Table } from '@sequelize/core/decorators-legacy';
  11. import { expect } from 'chai';
  12. import { describe } from 'mocha';
  13. import sinon from 'sinon';
  14. import {
  15. beforeAll2,
  16. createSingleTransactionalTestSequelizeInstance,
  17. sequelize,
  18. setResetMode,
  19. } from '../support';
  20. describe('Model#reload', () => {
  21. context('test-shared models', () => {
  22. setResetMode('destroy');
  23. const vars = beforeAll2(async () => {
  24. const clock = sinon.useFakeTimers();
  25. class Book extends Model<InferAttributes<Book>, InferCreationAttributes<Book>> {
  26. declare id: CreationOptional<number>;
  27. declare updatedAt: CreationOptional<Date>;
  28. @Attribute(DataTypes.STRING)
  29. declare title: string | null;
  30. @Attribute(DataTypes.INTEGER)
  31. declare integer1: number | null;
  32. @Attribute(DataTypes.INTEGER)
  33. declare integer2: number | null;
  34. declare pages?: NonAttribute<Page[]>;
  35. declare setPages: HasManySetAssociationsMixin<Page, Page['id']>;
  36. }
  37. class Page extends Model<InferAttributes<Page>, InferCreationAttributes<Page>> {
  38. declare id: CreationOptional<number>;
  39. declare updatedAt: CreationOptional<Date>;
  40. @Attribute(DataTypes.STRING)
  41. declare content: string | null;
  42. @BelongsTo(() => Book, {
  43. foreignKey: 'bookId',
  44. inverse: {
  45. as: 'pages',
  46. type: 'hasMany',
  47. },
  48. })
  49. declare book?: NonAttribute<Book | null>;
  50. declare bookId: number | null;
  51. }
  52. sequelize.addModels([Book, Page]);
  53. await sequelize.sync({ force: true });
  54. return { Book, Page, clock };
  55. });
  56. afterEach(() => {
  57. vars.clock.reset();
  58. });
  59. after(() => {
  60. vars.clock.restore();
  61. });
  62. it('returns a reference to the same instance instead of creating a new one', async () => {
  63. const original = await vars.Book.create({ title: 'Book Title 1' });
  64. await original.update({ title: 'Book Title 2' });
  65. const updated = await original.reload();
  66. expect(original === updated).to.be.true;
  67. });
  68. it('updates local value based on the values in the database', async () => {
  69. const originalBook = await vars.Book.create({ title: 'Title 1' });
  70. const updatedBook = await vars.Book.findByPk(originalBook.id, { rejectOnEmpty: true });
  71. await updatedBook.update({ title: 'Title 2' });
  72. // We used a different reference when calling update, so originalBook is now out of sync
  73. expect(originalBook.title).to.equal('Title 1');
  74. await originalBook.reload();
  75. expect(originalBook.title).to.equal('Title 2');
  76. });
  77. it('uses its own "where" condition', async () => {
  78. const book1 = await vars.Book.create({ title: 'First Book' });
  79. const book2 = await vars.Book.create({ title: 'Second Book' });
  80. const primaryKey = book1.get('id');
  81. await book1.reload();
  82. expect(book1.get('id')).to.equal(primaryKey);
  83. // @ts-expect-error -- where is not a supported option in "reload"
  84. await book1.reload({ where: { id: book2.get('id') } });
  85. expect(book1.get('id')).to.equal(primaryKey).and.not.equal(book2.get('id'));
  86. });
  87. it('supports updating a subset of attributes', async () => {
  88. const book1 = await vars.Book.create({
  89. integer1: 1,
  90. integer2: 1,
  91. });
  92. await vars.Book.update(
  93. {
  94. integer1: 2,
  95. integer2: 2,
  96. },
  97. {
  98. where: {
  99. id: book1.get('id'),
  100. },
  101. },
  102. );
  103. const user = await book1.reload({
  104. attributes: ['integer1'],
  105. });
  106. expect(user.get('integer1')).to.equal(2);
  107. expect(user.get('integer2')).to.equal(1);
  108. });
  109. it('updates timestamp attributes', async () => {
  110. const originalBook = await vars.Book.create({ title: 'Title 1' });
  111. const originallyUpdatedAt = originalBook.updatedAt;
  112. // Wait for a second, so updatedAt will actually be different
  113. vars.clock.tick(1000);
  114. const updatedBook = await vars.Book.findByPk(originalBook.id, { rejectOnEmpty: true });
  115. await updatedBook.update({ title: 'Title 2' });
  116. await originalBook.reload();
  117. expect(originalBook.updatedAt).to.be.above(originallyUpdatedAt);
  118. expect(updatedBook.updatedAt).to.be.above(originallyUpdatedAt);
  119. });
  120. it('returns an error when reload fails', async () => {
  121. const user = await vars.Book.create({ title: 'Title' });
  122. await user.destroy();
  123. await expect(user.reload()).to.be.rejectedWith(
  124. InstanceError,
  125. 'Instance could not be reloaded because it does not exist anymore (find call returned null)',
  126. );
  127. });
  128. it('updates internal options of the instance', async () => {
  129. const { Book, Page } = vars;
  130. const [book, page] = await Promise.all([
  131. Book.create({ title: 'A very old book' }),
  132. Page.create(),
  133. ]);
  134. await book.setPages([page]);
  135. const fetchedBook = await Book.findOne({
  136. where: { id: book.id },
  137. rejectOnEmpty: true,
  138. });
  139. // @ts-expect-error -- testing internal option
  140. const oldOptions = fetchedBook._options;
  141. await fetchedBook.reload({
  142. include: [Page],
  143. });
  144. // @ts-expect-error -- testing internal option
  145. expect(oldOptions).not.to.equal(fetchedBook._options);
  146. // @ts-expect-error -- testing internal option
  147. expect(fetchedBook._options.include.length).to.equal(1);
  148. expect(fetchedBook.pages!.length).to.equal(1);
  149. // @ts-expect-error -- type this correctly
  150. expect(fetchedBook.get({ plain: true }).pages!.length).to.equal(1);
  151. });
  152. it('reloads included associations', async () => {
  153. const { Book, Page } = vars;
  154. const [book, page] = await Promise.all([
  155. Book.create({ title: 'A very old book' }),
  156. Page.create({ content: 'om nom nom' }),
  157. ]);
  158. await book.setPages([page]);
  159. const leBook = await Book.findOne({
  160. where: { id: book.id },
  161. include: [Page],
  162. rejectOnEmpty: true,
  163. });
  164. const page0 = await page.update({ content: 'something totally different' });
  165. expect(leBook.pages!.length).to.equal(1);
  166. expect(leBook.pages![0].content).to.equal('om nom nom');
  167. expect(page0.content).to.equal('something totally different');
  168. await leBook.reload();
  169. expect(leBook.pages!.length).to.equal(1);
  170. expect(leBook.pages![0].content).to.equal('something totally different');
  171. expect(page0.content).to.equal('something totally different');
  172. });
  173. it('should set an association to null after deletion, 1-1', async () => {
  174. const { Book, Page } = vars;
  175. const page = await Page.create(
  176. {
  177. content: 'the brand',
  178. // @ts-expect-error -- TODO: properly type this
  179. book: {
  180. title: 'hello',
  181. },
  182. },
  183. { include: [Book] },
  184. );
  185. const reloadedPage = await Page.findOne({
  186. where: { id: page.id },
  187. include: [Book],
  188. rejectOnEmpty: true,
  189. });
  190. expect(reloadedPage.book).not.to.be.null;
  191. await page.book!.destroy();
  192. await reloadedPage.reload();
  193. expect(reloadedPage.book).to.be.null;
  194. });
  195. it('should set an association to empty after all deletion, 1-N', async () => {
  196. const { Book, Page } = vars;
  197. const book = await Book.create(
  198. {
  199. title: 'title',
  200. // @ts-expect-error -- TODO: properly type this
  201. pages: [
  202. {
  203. content: 'page 1',
  204. },
  205. {
  206. content: 'page 2',
  207. },
  208. ],
  209. },
  210. { include: [Page] },
  211. );
  212. const refetchedBook = await Book.findOne({
  213. where: { id: book.id },
  214. include: [Page],
  215. rejectOnEmpty: true,
  216. });
  217. expect(refetchedBook.pages).not.to.be.empty;
  218. await refetchedBook.pages![1].destroy();
  219. await refetchedBook.pages![0].destroy();
  220. await refetchedBook.reload();
  221. expect(refetchedBook.pages).to.be.empty;
  222. });
  223. it('changed should be false after reload', async () => {
  224. const account0 = await vars.Book.create({ title: 'Title 1' });
  225. account0.title = 'Title 2';
  226. // @ts-expect-error -- TODO: rework "changed" to avoid overloading
  227. expect(account0.changed()[0]).to.equal('title');
  228. const account = await account0.reload();
  229. expect(account.changed()).to.equal(false);
  230. });
  231. });
  232. context('test-specific models', () => {
  233. if (sequelize.dialect.supports.transactions) {
  234. it('supports transactions', async () => {
  235. const transactionSequelize =
  236. await createSingleTransactionalTestSequelizeInstance(sequelize);
  237. class User extends Model<InferAttributes<User>> {
  238. @Attribute(DataTypes.STRING)
  239. @NotNull
  240. declare username: string;
  241. }
  242. transactionSequelize.addModels([User]);
  243. await transactionSequelize.sync({ force: true });
  244. const user = await User.create({ username: 'foo' });
  245. const t = await transactionSequelize.startUnmanagedTransaction();
  246. try {
  247. await User.update({ username: 'bar' }, { where: { username: 'foo' }, transaction: t });
  248. const user1 = await user.reload();
  249. expect(user1.username).to.equal('foo');
  250. const user0 = await user1.reload({ transaction: t });
  251. expect(user0.username).to.equal('bar');
  252. } finally {
  253. await t.rollback();
  254. }
  255. });
  256. }
  257. it('is disallowed if no primary key is present', async () => {
  258. const Foo = sequelize.define('Foo', {}, { noPrimaryKey: true });
  259. await Foo.sync({ force: true });
  260. const instance = await Foo.create({});
  261. await expect(instance.reload()).to.be.rejectedWith(
  262. 'but the model does not have a primary key attribute definition.',
  263. );
  264. });
  265. it('should inject default scope when reloading', async () => {
  266. class Bar extends Model<InferAttributes<Bar>> {
  267. @Attribute(DataTypes.STRING)
  268. @NotNull
  269. declare name: string;
  270. declare fooId: number;
  271. }
  272. @Table({
  273. defaultScope: {
  274. include: [{ model: Bar }],
  275. },
  276. })
  277. class Foo extends Model<InferAttributes<Foo>, InferCreationAttributes<Foo>> {
  278. declare id: CreationOptional<number>;
  279. @HasMany(() => Bar, 'fooId')
  280. declare bars?: NonAttribute<Bar[]>;
  281. declare createBar: HasManyCreateAssociationMixin<Bar, 'fooId'>;
  282. }
  283. sequelize.addModels([Foo, Bar]);
  284. await sequelize.sync();
  285. const foo = await Foo.create();
  286. await foo.createBar({ name: 'bar' });
  287. const fooFromFind = await Foo.findByPk(foo.id, { rejectOnEmpty: true });
  288. expect(fooFromFind.bars).to.be.ok;
  289. expect(fooFromFind.bars![0].name).to.equal('bar');
  290. await foo.reload();
  291. expect(foo.bars).to.be.ok;
  292. expect(foo.bars![0].name).to.equal('bar');
  293. });
  294. });
  295. });