ts-patch-mongoose
Version:
Patch history & events for mongoose models
546 lines (436 loc) • 19.5 kB
text/typescript
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import mongoose, { model } from 'mongoose'
import em from '../src/em'
import { patchHistoryPlugin } from '../src/index'
import { HistoryModel } from '../src/model'
import { GLOBAL_CREATED, GLOBAL_DELETED, GLOBAL_UPDATED } from './constants/events'
import server from './mongo/server'
import { type Product, ProductSchema } from './schemas/Product'
import { type User, UserSchema } from './schemas/User'
vi.mock('../src/em', () => ({ default: { emit: vi.fn() } }))
describe('plugin - global', () => {
const instance = server('plugin-global')
mongoose.plugin(patchHistoryPlugin, {
eventCreated: GLOBAL_CREATED,
eventUpdated: GLOBAL_UPDATED,
eventDeleted: GLOBAL_DELETED,
omit: ['__v', 'createdAt', 'updatedAt'],
})
const UserModel = model<User>('User', UserSchema)
const ProductModel = model<Product>('Product', ProductSchema)
beforeAll(async () => {
await instance.create()
})
afterAll(async () => {
await instance.destroy()
})
beforeEach(async () => {
await mongoose.connection.collection('users').deleteMany({})
await mongoose.connection.collection('products').deleteMany({})
await mongoose.connection.collection('history').deleteMany({})
})
afterEach(async () => {
vi.clearAllMocks()
})
it('should save array', async () => {
const product = await ProductModel.create({ name: 'paper', groups: [] })
expect(product.name).toBe('paper')
product.groups = ['office']
await product.save()
product.groups.push('school')
await product.save()
const history = await HistoryModel.find({})
expect(history).toHaveLength(3)
const [first, second, third] = history
// 1 create
expect(first.version).toBe(0)
expect(first.op).toBe('create')
expect(first.modelName).toBe('Product')
expect(first.collectionName).toBe('products')
expect(first.collectionId).toEqual(product._id)
expect(first.doc).toHaveProperty('_id', product._id)
expect(first.doc).toHaveProperty('name', 'paper')
expect(first.doc).toHaveProperty('groups', [])
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(first.patch).toHaveLength(0)
// 2 update
expect(second.version).toBe(1)
expect(second.op).toBe('update')
expect(second.modelName).toBe('Product')
expect(second.collectionName).toBe('products')
expect(second.collectionId).toEqual(product._id)
expect(second.doc).toBeUndefined()
expect(second.patch).toHaveLength(1)
expect(second.patch).toMatchObject([{ op: 'add', path: '/groups/0', value: 'office' }])
// 3 update
expect(third.version).toBe(2)
expect(third.op).toBe('update')
expect(third.modelName).toBe('Product')
expect(third.collectionName).toBe('products')
expect(third.collectionId).toEqual(product._id)
expect(third.doc).toBeUndefined()
expect(third.patch).toHaveLength(1)
expect(third.patch).toMatchObject([{ op: 'add', path: '/groups/1', value: 'school' }])
expect(em.emit).toHaveBeenCalledTimes(3)
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: first.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', groups: [] }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', groups: ['office'] }),
patch: second.patch,
})
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', groups: ['office'] }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', groups: ['office', 'school'] }),
patch: third.patch,
})
})
it('should update array', async () => {
const product = await ProductModel.create({ name: 'paper', groups: [] })
expect(product.name).toBe('paper')
await product
.updateOne({
groups: ['office'],
})
.exec()
await product
.updateOne({
$push: { groups: 'school' },
})
.exec()
const history = await HistoryModel.find({})
expect(history).toHaveLength(3)
const [first, second, third] = history
// 1 create
expect(first.version).toBe(0)
expect(first.op).toBe('create')
expect(first.modelName).toBe('Product')
expect(first.collectionName).toBe('products')
expect(first.collectionId).toEqual(product._id)
expect(first.doc).toHaveProperty('_id', product._id)
expect(first.doc).toHaveProperty('name', 'paper')
expect(first.doc).toHaveProperty('groups', [])
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(first.patch).toHaveLength(0)
// 2 update
expect(second.version).toBe(1)
expect(second.op).toBe('updateOne')
expect(second.modelName).toBe('Product')
expect(second.collectionName).toBe('products')
expect(second.collectionId).toEqual(product._id)
expect(second.doc).toBeUndefined()
expect(second.patch).toHaveLength(1)
expect(second.patch).toMatchObject([{ op: 'add', path: '/groups/0', value: 'office' }])
// 3 update
expect(third.version).toBe(2)
expect(third.op).toBe('updateOne')
expect(third.modelName).toBe('Product')
expect(third.collectionName).toBe('products')
expect(third.collectionId).toEqual(product._id)
expect(third.doc).toBeUndefined()
expect(third.patch).toHaveLength(1)
expect(third.patch).toMatchObject([{ op: 'add', path: '/groups/1', value: 'school' }])
expect(em.emit).toHaveBeenCalledTimes(3)
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: first.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', groups: [] }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', groups: ['office'] }),
patch: second.patch,
})
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', groups: ['office'] }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', groups: ['office', 'school'] }),
patch: third.patch,
})
})
it('should save nested schema', async () => {
const product = await ProductModel.create({ name: 'paper', description: { summary: 'test1' } })
expect(product.name).toBe('paper')
product.description = { summary: 'test2' }
await product.save()
product.description.summary = 'test3'
await product.save()
const history = await HistoryModel.find({})
expect(history).toHaveLength(3)
const [first, second, third] = history
// 1 create
expect(first.version).toBe(0)
expect(first.op).toBe('create')
expect(first.modelName).toBe('Product')
expect(first.collectionName).toBe('products')
expect(first.collectionId).toEqual(product._id)
expect(first.doc).toHaveProperty('_id', product._id)
expect(first.doc).toHaveProperty('name', 'paper')
expect(first.doc).toHaveProperty('description', { summary: 'test1' })
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(first.patch).toHaveLength(0)
// 2 update
expect(second.version).toBe(1)
expect(second.op).toBe('update')
expect(second.modelName).toBe('Product')
expect(second.collectionName).toBe('products')
expect(second.collectionId).toEqual(product._id)
expect(second.doc).toBeUndefined()
expect(second.patch).toHaveLength(2)
expect(second.patch).toMatchObject([
{ op: 'test', path: '/description/summary', value: 'test1' },
{ op: 'replace', path: '/description/summary', value: 'test2' },
])
// 3 update
expect(third.version).toBe(2)
expect(third.op).toBe('update')
expect(third.modelName).toBe('Product')
expect(third.collectionName).toBe('products')
expect(third.collectionId).toEqual(product._id)
expect(third.doc).toBeUndefined()
expect(third.patch).toHaveLength(2)
expect(third.patch).toMatchObject([
{ op: 'test', path: '/description/summary', value: 'test2' },
{ op: 'replace', path: '/description/summary', value: 'test3' },
])
expect(em.emit).toHaveBeenCalledTimes(3)
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: first.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test1' } }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test2' } }),
patch: second.patch,
})
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test2' } }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test3' } }),
patch: third.patch,
})
})
it('should update nested schema', async () => {
const product = await ProductModel.create({ name: 'paper', description: { summary: 'test1' } })
expect(product.name).toBe('paper')
await product
.updateOne({
description: { summary: 'test2' },
})
.exec()
await product
.updateOne({
$set: { 'description.summary': 'test3' },
})
.exec()
const history = await HistoryModel.find({})
expect(history).toHaveLength(3)
const [first, second, third] = history
// 1 create
expect(first.version).toBe(0)
expect(first.op).toBe('create')
expect(first.modelName).toBe('Product')
expect(first.collectionName).toBe('products')
expect(first.collectionId).toEqual(product._id)
expect(first.doc).toHaveProperty('_id', product._id)
expect(first.doc).toHaveProperty('name', 'paper')
expect(first.doc).toHaveProperty('description', { summary: 'test1' })
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(first.patch).toHaveLength(0)
// 2 update
expect(second.version).toBe(1)
expect(second.op).toBe('updateOne')
expect(second.modelName).toBe('Product')
expect(second.collectionName).toBe('products')
expect(second.collectionId).toEqual(product._id)
expect(second.doc).toBeUndefined()
expect(second.patch).toHaveLength(2)
expect(second.patch).toMatchObject([
{ op: 'test', path: '/description/summary', value: 'test1' },
{ op: 'replace', path: '/description/summary', value: 'test2' },
])
// 3 update
expect(third.version).toBe(2)
expect(third.op).toBe('updateOne')
expect(third.modelName).toBe('Product')
expect(third.collectionName).toBe('products')
expect(third.collectionId).toEqual(product._id)
expect(third.doc).toBeUndefined()
expect(third.patch).toHaveLength(2)
expect(third.patch).toMatchObject([
{ op: 'test', path: '/description/summary', value: 'test2' },
{ op: 'replace', path: '/description/summary', value: 'test3' },
])
expect(em.emit).toHaveBeenCalledTimes(3)
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: first.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test1' } }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test2' } }),
patch: second.patch,
})
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test2' } }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', description: { summary: 'test3' } }),
patch: third.patch,
})
})
it('should save objectID', async () => {
const john = await UserModel.create({ name: 'John', role: 'user' })
expect(john.name).toBe('John')
const alice = await UserModel.create({ name: 'Alice', role: 'user' })
expect(alice.name).toBe('Alice')
const product = await ProductModel.create({ name: 'paper', addedBy: john })
expect(product.name).toBe('paper')
product.addedBy = alice._id
await product.save()
const history = await HistoryModel.find({})
expect(history).toHaveLength(4)
const [first, second, third, fourth] = history
// 1 create
expect(first.version).toBe(0)
expect(first.op).toBe('create')
expect(first.modelName).toBe('User')
expect(first.collectionName).toBe('users')
expect(first.collectionId).toEqual(john._id)
expect(first.doc).toHaveProperty('_id', john._id)
expect(first.doc).toHaveProperty('name', 'John')
expect(first.doc).toHaveProperty('role', 'user')
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(first.patch).toHaveLength(0)
// 2 create
expect(second.version).toBe(0)
expect(second.op).toBe('create')
expect(second.modelName).toBe('User')
expect(second.collectionName).toBe('users')
expect(second.collectionId).toEqual(alice._id)
expect(second.doc).toHaveProperty('_id', alice._id)
expect(second.doc).toHaveProperty('name', 'Alice')
expect(second.doc).toHaveProperty('role', 'user')
expect(second.doc).not.toHaveProperty('createdAt')
expect(second.doc).not.toHaveProperty('updatedAt')
expect(second.patch).toHaveLength(0)
// 3 create
expect(third.version).toBe(0)
expect(third.op).toBe('create')
expect(third.modelName).toBe('Product')
expect(third.collectionName).toBe('products')
expect(third.collectionId).toEqual(product._id)
expect(third.doc).toHaveProperty('_id', product._id)
expect(third.doc).toHaveProperty('name', 'paper')
expect(third.doc).toHaveProperty('addedBy', john._id)
expect(third.doc).not.toHaveProperty('createdAt')
expect(third.doc).not.toHaveProperty('updatedAt')
expect(third.patch).toHaveLength(0)
// 4 update
expect(fourth.version).toBe(1)
expect(fourth.op).toBe('update')
expect(fourth.modelName).toBe('Product')
expect(fourth.collectionName).toBe('products')
expect(fourth.collectionId).toEqual(product._id)
expect(fourth.doc).toBeUndefined()
expect(fourth.patch).toHaveLength(2)
expect(fourth.patch).toMatchObject([
{ op: 'test', path: '/addedBy', value: john._id.toString() },
{ op: 'replace', path: '/addedBy', value: alice._id.toString() },
])
expect(em.emit).toHaveBeenCalledTimes(4)
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: first.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: second.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: third.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', addedBy: john._id }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', addedBy: alice._id }),
patch: fourth.patch,
})
})
it('should update objectID', async () => {
const john = await UserModel.create({ name: 'John', role: 'user' })
expect(john.name).toBe('John')
const alice = await UserModel.create({ name: 'Alice', role: 'user' })
expect(alice.name).toBe('Alice')
const product = await ProductModel.create({ name: 'paper', addedBy: john })
expect(product.name).toBe('paper')
await product
.updateOne({
addedBy: alice,
})
.exec()
await product
.updateOne({
addedBy: { _id: john._id, name: 'John', role: 'manager' },
})
.exec()
const history = await HistoryModel.find({})
expect(history).toHaveLength(5)
const [first, second, third, fourth, fifth] = history
// 1 create
expect(first.version).toBe(0)
expect(first.op).toBe('create')
expect(first.modelName).toBe('User')
expect(first.collectionName).toBe('users')
expect(first.collectionId).toEqual(john._id)
expect(first.doc).toHaveProperty('_id', john._id)
expect(first.doc).toHaveProperty('name', 'John')
expect(first.doc).toHaveProperty('role', 'user')
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(first.patch).toHaveLength(0)
// 2 create
expect(second.version).toBe(0)
expect(second.op).toBe('create')
expect(second.modelName).toBe('User')
expect(second.collectionName).toBe('users')
expect(second.collectionId).toEqual(alice._id)
expect(second.doc).toHaveProperty('_id', alice._id)
expect(second.doc).toHaveProperty('name', 'Alice')
expect(second.doc).toHaveProperty('role', 'user')
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(second.patch).toHaveLength(0)
// 3 create
expect(third.version).toBe(0)
expect(third.op).toBe('create')
expect(third.modelName).toBe('Product')
expect(third.collectionName).toBe('products')
expect(third.collectionId).toEqual(product._id)
expect(third.doc).toHaveProperty('_id', product._id)
expect(third.doc).toHaveProperty('name', 'paper')
expect(third.doc).toHaveProperty('addedBy', john._id)
expect(first.doc).not.toHaveProperty('createdAt')
expect(first.doc).not.toHaveProperty('updatedAt')
expect(third.patch).toHaveLength(0)
// 4 update
expect(fourth.version).toBe(1)
expect(fourth.op).toBe('updateOne')
expect(fourth.modelName).toBe('Product')
expect(fourth.collectionName).toBe('products')
expect(fourth.collectionId).toEqual(product._id)
expect(fourth.doc).toBeUndefined()
expect(fourth.patch).toHaveLength(2)
expect(fourth.patch).toMatchObject([
{ op: 'test', path: '/addedBy', value: john._id.toString() },
{ op: 'replace', path: '/addedBy', value: alice._id.toString() },
])
// 5 update
expect(fifth.version).toBe(2)
expect(fifth.op).toBe('updateOne')
expect(fifth.modelName).toBe('Product')
expect(fifth.collectionName).toBe('products')
expect(fifth.collectionId).toEqual(product._id)
expect(fifth.doc).toBeUndefined()
expect(fifth.patch).toHaveLength(2)
expect(fifth.patch).toMatchObject([
{ op: 'test', path: '/addedBy', value: alice._id.toString() },
{ op: 'replace', path: '/addedBy', value: john._id.toString() },
])
expect(em.emit).toHaveBeenCalledTimes(5)
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: first.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: second.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_CREATED, { doc: third.doc })
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', addedBy: john._id }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', addedBy: alice._id }),
patch: fourth.patch,
})
expect(em.emit).toHaveBeenCalledWith(GLOBAL_UPDATED, {
oldDoc: expect.objectContaining({ _id: product._id, name: 'paper', addedBy: alice._id }),
doc: expect.objectContaining({ _id: product._id, name: 'paper', addedBy: john._id }),
patch: fifth.patch,
})
})
})