diff --git a/packages/esix/src/base-model.spec.ts b/packages/esix/src/base-model.spec.ts index 22917e9..03fb2ca 100644 --- a/packages/esix/src/base-model.spec.ts +++ b/packages/esix/src/base-model.spec.ts @@ -683,6 +683,89 @@ describe('BaseModel', () => { }) }) + describe('update', () => { + it('updates an existing model', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2023-01-01T10:00:00Z')) + + const book = new Book() + + book.id = '5f347707fdec6e388b5c1d33' + book.title = 'Emma' + book.isbn = '9780141439600' + book.pages = 448 + book.authorId = 'author-1' + book.createdAt = new Date('2022-12-01T10:00:00Z').getTime() + + await book.update({ + pages: 512, + title: 'Emma (Second Edition)' + }) + + expect(book.pages).toEqual(512) + expect(book.title).toEqual('Emma (Second Edition)') + + expect(collection.updateOne).toHaveBeenCalledWith( + { + _id: '5f347707fdec6e388b5c1d33' + }, + { + $set: { + _id: '5f347707fdec6e388b5c1d33', + authorId: 'author-1', + createdAt: new Date('2022-12-01T10:00:00Z').getTime(), + isAvailable: true, + isbn: '9780141439600', + pages: 512, + title: 'Emma (Second Edition)', + updatedAt: new Date('2023-01-01T10:00:00Z').getTime() + } + }, + { + upsert: true + } + ) + }) + + it('inserts a new model when it has no id', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2023-01-01T10:00:00Z')) + + vi.mocked(ObjectId.prototype.toHexString).mockReturnValue( + '5f347707fdec6e388b5c1d33' + ) + + const book = new Book() + + await book.update({ + isbn: '9780141439600', + title: 'Emma' + }) + + expect(book.wasRecentlyCreated).toEqual(true) + + expect(collection.updateOne).toHaveBeenCalledWith( + { + _id: '5f347707fdec6e388b5c1d33' + }, + { + $set: { + _id: '5f347707fdec6e388b5c1d33', + createdAt: new Date('2023-01-01T10:00:00Z').getTime(), + isAvailable: true, + isbn: '9780141439600', + pages: 0, + title: 'Emma', + updatedAt: null + } + }, + { + upsert: true + } + ) + }) + }) + describe('where', () => { it('finds all documets that matches a query', async () => { const cursor = createCursor([ diff --git a/packages/esix/src/base-model.ts b/packages/esix/src/base-model.ts index 3f811be..2f95f31 100644 --- a/packages/esix/src/base-model.ts +++ b/packages/esix/src/base-model.ts @@ -697,6 +697,28 @@ export default class BaseModel { this.id = id } } + + /** + * Assigns the given attributes to the model and persists it to the + * database. It's equivalent to assigning each attribute individually and + * then calling `save()`, so `updatedAt` is stamped automatically on + * existing models. Note that assigning a new `id` upserts a new document + * rather than renaming the existing one. + * + * Example + * ``` + * const product = await Product.find(id); + * + * await product.update({ name: 'Chair 1', price: 42.0 }); + * ``` + * + * @param attributes + */ + async update(attributes: Partial): Promise { + Object.assign(this, attributes) + + await this.save() + } } function getDefaultValues( diff --git a/packages/esix/src/integration-test.spec.ts b/packages/esix/src/integration-test.spec.ts index c5f704c..1b09f96 100644 --- a/packages/esix/src/integration-test.spec.ts +++ b/packages/esix/src/integration-test.spec.ts @@ -199,6 +199,41 @@ describe('Integration', () => { }) }) + it('updates multiple attributes at once', async () => { + const dateSpy = vi.spyOn(Date, 'now') + dateSpy.mockReturnValue(new Date('2023-01-01T10:00:00Z').getTime()) + + const product = await Product.create({ + name: 'Chair', + price: 20.0 + }) + + dateSpy.mockReturnValue(new Date('2023-01-01T10:30:00Z').getTime()) + + const existingProduct = await Product.find(product.id) + + expect(existingProduct).not.toBeNull() + + if (!existingProduct) { + return + } + + await existingProduct.update({ + name: 'Chair 1', + price: 42.0 + }) + + const updatedProduct = await Product.find(product.id) + + expect(updatedProduct).toEqual({ + createdAt: new Date('2023-01-01T10:00:00Z').getTime(), + id: product.id, + name: 'Chair 1', + price: 42.0, + updatedAt: new Date('2023-01-01T10:30:00Z').getTime() + }) + }) + it('persists a new model', async () => { const dateSpy = vi.spyOn(Date, 'now') dateSpy.mockReturnValue(new Date('2023-01-01T10:00:00Z').getTime()) diff --git a/packages/website/docs/inserting-and-updating-models.md b/packages/website/docs/inserting-and-updating-models.md index d3ee36b..5bf3a7c 100644 --- a/packages/website/docs/inserting-and-updating-models.md +++ b/packages/website/docs/inserting-and-updating-models.md @@ -1,9 +1,14 @@ --- title: Inserting & Updating Models -description: Learn how to create, update, and modify records in your MongoDB database using Esix's intuitive model methods and mass assignment features. +description: + Learn how to create, update, and modify records in your MongoDB database using + Esix's intuitive model methods and mass assignment features. --- -When it comes to adding new models to the database, there are two different ways to go about it. You can either use the `create` method and pass it the attributes you want the model to have, or you can create a new instance of the model and call its `save` method. +When it comes to adding new models to the database, there are two different ways +to go about it. You can either use the `create` method and pass it the +attributes you want the model to have, or you can create a new instance of the +model and call its `save` method. ```ts // Using the create method. @@ -38,6 +43,28 @@ await product.save() When you call `save` on an already existing model, the `updatedAt` field will be filled with the current timestamp. +## Updating multiple attributes + +Instead of assigning each attribute individually and calling `save`, you can +pass all the changes to the `update` method in one go. + +```ts +const product = await Product.find('5f5a474b32fa462a5724ff7d') + +// Instead of this... +product.name = 'Chair 1' +product.price = 42.0 + +await product.save() + +// ...you can do this. +await product.update({ name: 'Chair 1', price: 42.0 }) +``` + +The `update` method assigns the given attributes to the model and saves it, so +the `updatedAt` field is filled with the current timestamp just like when you +call `save` yourself. + Both the timestamp properties contain the current time in milliseconds since January 1st, 1970, using JavaScript's [Date.now](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now)