save.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import type {
  2. CreationOptional,
  3. HasManySetAssociationsMixin,
  4. InferAttributes,
  5. InferCreationAttributes,
  6. NonAttribute,
  7. } from '@sequelize/core';
  8. import { DataTypes, Model, ValidationError, sql } from '@sequelize/core';
  9. import {
  10. Attribute,
  11. BelongsTo,
  12. ColumnName,
  13. Default,
  14. PrimaryKey,
  15. Table,
  16. } from '@sequelize/core/decorators-legacy';
  17. import { IsInt, Len } from '@sequelize/validator.js';
  18. import { expect } from 'chai';
  19. import { describe } from 'mocha';
  20. import assert from 'node:assert';
  21. import sinon from 'sinon';
  22. import {
  23. beforeAll2,
  24. createSingleTransactionalTestSequelizeInstance,
  25. sequelize,
  26. setResetMode,
  27. } from '../support';
  28. describe('Model#save', () => {
  29. context('test-shared models', () => {
  30. setResetMode('destroy');
  31. const vars = beforeAll2(async () => {
  32. const clock = sinon.useFakeTimers();
  33. class Book extends Model<InferAttributes<Book>, InferCreationAttributes<Book>> {
  34. declare id: CreationOptional<number>;
  35. declare updatedAt: CreationOptional<Date>;
  36. @Attribute(DataTypes.STRING)
  37. declare title: string | null;
  38. @Attribute(DataTypes.DATE)
  39. @Default(DataTypes.NOW)
  40. declare publishedAt: Date | null;
  41. @Attribute(DataTypes.INTEGER)
  42. declare integer1: number | null;
  43. @Attribute(DataTypes.INTEGER)
  44. declare integer2: number | null;
  45. @Attribute(DataTypes.INTEGER)
  46. @IsInt
  47. declare validateTest: number | null;
  48. @Attribute(DataTypes.STRING)
  49. @Len({ msg: 'Length failed.', args: [1, 20] })
  50. declare validateCustom: string | null;
  51. declare pages?: NonAttribute<Page[]>;
  52. declare setPages: HasManySetAssociationsMixin<Page, Page['id']>;
  53. }
  54. class Page extends Model<InferAttributes<Page>, InferCreationAttributes<Page>> {
  55. declare id: CreationOptional<number>;
  56. declare updatedAt: CreationOptional<Date>;
  57. @Attribute(DataTypes.STRING)
  58. declare content: string | null;
  59. @BelongsTo(() => Book, {
  60. foreignKey: 'bookId',
  61. inverse: {
  62. as: 'pages',
  63. type: 'hasMany',
  64. },
  65. })
  66. declare book?: NonAttribute<Book | null>;
  67. declare bookId: number | null;
  68. }
  69. sequelize.addModels([Book, Page]);
  70. await sequelize.sync({ force: true });
  71. return { Book, Page, clock };
  72. });
  73. afterEach(() => {
  74. vars.clock.reset();
  75. });
  76. after(() => {
  77. vars.clock.restore();
  78. });
  79. it('inserts an entry in the database', async () => {
  80. const { Book } = vars;
  81. const title = 'user';
  82. const user = Book.build({
  83. title,
  84. publishedAt: new Date(1984, 8, 23),
  85. });
  86. const books = await Book.findAll();
  87. expect(books).to.have.length(0);
  88. await user.save();
  89. const users0 = await Book.findAll();
  90. expect(users0).to.have.length(1);
  91. expect(users0[0].title).to.equal(title);
  92. expect(users0[0].publishedAt).to.be.instanceof(Date);
  93. expect(users0[0].publishedAt).to.equalDate(new Date(1984, 8, 23));
  94. });
  95. it('only updates fields in passed array', async () => {
  96. const date = new Date(1990, 1, 1);
  97. const book = await vars.Book.create({
  98. title: 'foo',
  99. publishedAt: new Date(),
  100. });
  101. book.title = 'fizz';
  102. book.publishedAt = date;
  103. await book.save({ fields: ['title'] });
  104. const reloadedBook = await vars.Book.findByPk(book.id, { rejectOnEmpty: true });
  105. expect(reloadedBook.title).to.equal('fizz');
  106. expect(reloadedBook.publishedAt).not.to.equal(date);
  107. });
  108. it('sets the timestamps on insert', async () => {
  109. const { Book, clock } = vars;
  110. const now = new Date();
  111. now.setMilliseconds(0);
  112. const book = Book.build({});
  113. clock.tick(1000);
  114. await book.save();
  115. expect(book).have.property('updatedAt').afterTime(now);
  116. });
  117. it('sets the timestamps on update', async () => {
  118. const { Book, clock } = vars;
  119. const now = new Date();
  120. now.setMilliseconds(0);
  121. const user = await Book.create({});
  122. const firstUpdatedAt = user.updatedAt;
  123. user.title = 'title';
  124. clock.tick(1000);
  125. await user.save();
  126. expect(user).have.property('updatedAt').afterTime(firstUpdatedAt);
  127. });
  128. it('does not update timestamps if nothing changed', async () => {
  129. const book = await vars.Book.create({ title: 'title' });
  130. const updatedAt = book.updatedAt;
  131. vars.clock.tick(2000);
  132. const newlySavedUser = await book.save();
  133. expect(newlySavedUser.updatedAt).to.equalTime(updatedAt);
  134. });
  135. it('does not update timestamps when option "silent=true" is used', async () => {
  136. const book = await vars.Book.create({ title: 'title 1' });
  137. const updatedAt = book.updatedAt;
  138. vars.clock.tick(1000);
  139. book.title = 'title 2';
  140. await book.save({
  141. silent: true,
  142. });
  143. expect(book.updatedAt).to.equalTime(updatedAt);
  144. });
  145. it('updates with function and column value', async () => {
  146. const book = await vars.Book.create({
  147. integer1: 42,
  148. });
  149. // @ts-expect-error -- TODO: forbid this, but allow doing it via instance.update()
  150. book.integer2 = sql.attribute('integer1');
  151. // @ts-expect-error -- TODO: forbid this, but allow doing it via instance.update()
  152. book.title = sql.fn('upper', 'sequelize');
  153. await book.save();
  154. const refreshedBook = await vars.Book.findByPk(book.id, { rejectOnEmpty: true });
  155. expect(refreshedBook.title).to.equal('SEQUELIZE');
  156. expect(refreshedBook.integer2).to.equal(42);
  157. });
  158. it('validates saved attributes', async () => {
  159. try {
  160. await vars.Book.build({ validateCustom: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' }).save();
  161. } catch (error) {
  162. assert(error instanceof ValidationError);
  163. expect(error.get('validateCustom')).to.exist;
  164. expect(error.get('validateCustom')).to.be.instanceof(Array);
  165. expect(error.get('validateCustom')[0]).to.exist;
  166. expect(error.get('validateCustom')[0].message).to.equal('Length failed.');
  167. }
  168. });
  169. it('does not validate non-saved attributes', async () => {
  170. await vars.Book.build({
  171. // @ts-expect-error -- invalid value, but not saved so not validated
  172. validateTest: 'cake',
  173. validateCustom: '1',
  174. }).save({
  175. fields: ['validateCustom'],
  176. });
  177. });
  178. it('supports nullish values', async () => {
  179. const user = await vars.Book.build({ integer1: 0 }).save({ fields: ['integer1'] });
  180. expect(user.integer1).to.equal(0);
  181. });
  182. it('does not lose eagerly-loaded associations', async () => {
  183. const { Book, Page } = vars;
  184. const book = await Book.create({ title: 'title', integer1: 1 });
  185. await Promise.all([
  186. Page.create({ bookId: book.id, content: 'page 1' }),
  187. Page.create({ bookId: book.id, content: 'page 2' }),
  188. ]);
  189. const book1 = await Book.findOne({
  190. where: { id: book.id },
  191. include: ['pages'],
  192. rejectOnEmpty: true,
  193. });
  194. expect(book1.title).to.equal('title');
  195. expect(book1.pages).to.exist;
  196. expect(book1.pages!.length).to.equal(2);
  197. book1.integer1! += 1;
  198. await book1.save();
  199. expect(book1.title).to.equal('title');
  200. expect(book1.integer1).to.equal(2);
  201. expect(book1.pages).to.exist;
  202. expect(book1.pages!.length).to.equal(2);
  203. });
  204. describe('hooks', () => {
  205. it('should update attributes added in hooks when default fields are used', async () => {
  206. const { Book } = vars;
  207. const unhook = Book.hooks.addListener('beforeUpdate', instance => {
  208. instance.set('title', 'B');
  209. });
  210. try {
  211. const book0 = await Book.create({
  212. title: 'A',
  213. integer1: 1,
  214. });
  215. await book0
  216. .set({
  217. integer1: 2,
  218. })
  219. .save();
  220. const book = await Book.findOne({ rejectOnEmpty: true });
  221. expect(book.get('title')).to.equal('B');
  222. expect(book.get('integer1')).to.equal(2);
  223. } finally {
  224. unhook();
  225. }
  226. });
  227. it('should update attributes changed in hooks when default fields are used', async () => {
  228. const { Book } = vars;
  229. const unhook = Book.hooks.addListener('beforeUpdate', instance => {
  230. instance.set('email', 'C');
  231. });
  232. try {
  233. const book0 = await Book.create({
  234. title: 'A',
  235. integer1: 1,
  236. });
  237. await book0
  238. .set({
  239. title: 'B',
  240. integer1: 2,
  241. })
  242. .save();
  243. const book = await Book.findOne({ rejectOnEmpty: true });
  244. expect(book.get('title')).to.equal('B');
  245. expect(book.get('integer1')).to.equal(2);
  246. } finally {
  247. unhook();
  248. }
  249. });
  250. it('validates attributes changed in hooks', async () => {
  251. const { Book } = vars;
  252. // validateTest
  253. const unhook = Book.hooks.addListener('beforeUpdate', instance => {
  254. instance.set('validateTest', 'B');
  255. });
  256. try {
  257. const book0 = await Book.create({
  258. validateTest: 1,
  259. });
  260. await expect(
  261. book0
  262. .set({
  263. title: 'new title',
  264. })
  265. .save(),
  266. ).to.be.rejectedWith(ValidationError);
  267. const book = await Book.findOne({ rejectOnEmpty: true });
  268. expect(book.get('validateTest')).to.equal(1);
  269. } finally {
  270. unhook();
  271. }
  272. });
  273. });
  274. });
  275. context('test-specific models', () => {
  276. if (sequelize.dialect.supports.transactions) {
  277. it('supports transactions', async () => {
  278. const transactionSequelize =
  279. await createSingleTransactionalTestSequelizeInstance(sequelize);
  280. const User = transactionSequelize.define('User', { username: DataTypes.STRING });
  281. await User.sync({ force: true });
  282. const transaction = await transactionSequelize.startUnmanagedTransaction();
  283. try {
  284. await User.build({ username: 'foo' }).save({ transaction });
  285. const count1 = await User.count();
  286. const count2 = await User.count({ transaction });
  287. expect(count1).to.equal(0);
  288. expect(count2).to.equal(1);
  289. } finally {
  290. await transaction.rollback();
  291. }
  292. });
  293. }
  294. it('is disallowed if no primary key is present', async () => {
  295. const Foo = sequelize.define('Foo', {});
  296. await Foo.sync({ force: true });
  297. const instance = await Foo.build({}, { isNewRecord: false });
  298. await expect(instance.save()).to.be.rejectedWith(
  299. 'You attempted to save an instance with no primary key',
  300. );
  301. });
  302. it('should not throw ER_EMPTY_QUERY if changed only virtual fields', async () => {
  303. const User = sequelize.define(
  304. `User`,
  305. {
  306. name: DataTypes.STRING,
  307. bio: {
  308. type: DataTypes.VIRTUAL,
  309. get: () => 'swag',
  310. },
  311. },
  312. {
  313. timestamps: false,
  314. },
  315. );
  316. await User.sync({ force: true });
  317. // TODO: attempting to set a value on a virtual attribute that does not have a setter should throw
  318. // the test can remain, but add a setter that does nothing
  319. const user = await User.create({ name: 'John', bio: 'swag 1' });
  320. await user.update({ bio: 'swag 2' });
  321. });
  322. it(`doesn't update the updatedAt attribute if timestamps attributes are disabled`, async () => {
  323. @Table({
  324. timestamps: false,
  325. })
  326. class User extends Model<InferAttributes<User>> {
  327. declare id: number;
  328. @Attribute(DataTypes.DATE)
  329. declare updatedAt: Date | null;
  330. }
  331. sequelize.addModels([User]);
  332. await User.sync();
  333. const johnDoe = await User.create({ id: 1 });
  334. // TODO: nullable attributes should always be set to null - https://github.com/sequelize/sequelize/issues/14671
  335. expect(johnDoe.updatedAt).to.beNullish();
  336. });
  337. it('still updates createdAt if updatedAt is disabled', async () => {
  338. @Table({
  339. updatedAt: false,
  340. })
  341. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  342. declare createdAt: CreationOptional<Date>;
  343. }
  344. sequelize.addModels([User]);
  345. await User.sync();
  346. const johnDoe = await User.create({});
  347. expect(johnDoe).to.not.have.property('updatedAt');
  348. expect(johnDoe.createdAt).to.notBeNullish();
  349. });
  350. it('still updates updatedAt if createdAt is disabled', async () => {
  351. @Table({
  352. createdAt: false,
  353. })
  354. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  355. declare updatedAt: CreationOptional<Date>;
  356. }
  357. sequelize.addModels([User]);
  358. await User.sync();
  359. const johnDoe = await User.create({});
  360. expect(johnDoe).to.not.have.property('createdAt');
  361. expect(johnDoe.updatedAt).to.notBeNullish();
  362. });
  363. it('should map the correct fields when saving instance (#10589)', async () => {
  364. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  365. @ColumnName('id2')
  366. @Attribute(DataTypes.INTEGER)
  367. declare id: number;
  368. @ColumnName('id3')
  369. @Attribute(DataTypes.INTEGER)
  370. declare id2: number;
  371. @ColumnName('id')
  372. @Attribute(DataTypes.INTEGER)
  373. @PrimaryKey
  374. declare id3: number;
  375. }
  376. sequelize.addModels([User]);
  377. await sequelize.sync({ force: true });
  378. await User.create({ id3: 94, id: 87, id2: 943 });
  379. const user = await User.findByPk(94, { rejectOnEmpty: true });
  380. await user.set('id2', 8877);
  381. await user.save();
  382. expect((await User.findByPk(94, { rejectOnEmpty: true })).id2).to.equal(8877);
  383. });
  384. });
  385. });