Skip to content

Commit

Permalink
Fix insert many and coverage for create event
Browse files Browse the repository at this point in the history
  • Loading branch information
ilovepixelart committed Apr 10, 2023
1 parent a390697 commit d009d28
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 49 deletions.
5 changes: 2 additions & 3 deletions src/interfaces/IContext.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { HydratedDocument, Types } from 'mongoose'
import type { HydratedDocument } from 'mongoose'

interface IContext<T> {
op: string
modelName: string
collectionName: string
isNew?: boolean
oldDoc?: HydratedDocument<T>
createdDocs?: HydratedDocument<T>[]
deletedDocs?: HydratedDocument<T>[]
updatedIds?: Types.ObjectId[]
}

export default IContext
78 changes: 47 additions & 31 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,37 +62,33 @@ async function updatePatch<T> (opts: IPluginOptions<T>, context: IContext<T>, cu
})
}

async function createPatch<T> (opts: IPluginOptions<T>, context: IContext<T>, current: HydratedDocument<T>): Promise<void> {
if (opts.patchHistoryDisabled) return
async function bulkPatch<T> (opts: IPluginOptions<T>, context: IContext<T>, eventKey: 'eventCreated' | 'eventDeleted', docsKey: 'createdDocs' | 'deletedDocs'): Promise<void> {
const event = opts[eventKey]
const docs = context[docsKey]

await History.create({
op: context.op,
modelName: context.modelName,
collectionName: context.collectionName,
collectionId: current._id as Types.ObjectId,
doc: current
})
}
if (_.isEmpty(docs) || (!event && opts.patchHistoryDisabled)) return

async function deletePatch<T> (opts: IPluginOptions<T>, context: IContext<T>): Promise<void> {
if (_.isEmpty(context.deletedDocs) || (!opts.eventDeleted && opts.patchHistoryDisabled)) return

const chunks = _.chunk(context.deletedDocs, 1000)
const chunks = _.chunk(context[docsKey], 1000)
for await (const chunk of chunks) {
const bulk = []
for (const oldDoc of chunk) {
if (opts.eventDeleted) {
em.emit(opts.eventDeleted, { oldDoc })
for (const doc of chunk) {
if (event && eventKey === 'eventCreated') {
em.emit(event, { doc })
}

if (event && eventKey === 'eventDeleted') {
em.emit(event, { oldDoc: doc })
}

if (!opts.patchHistoryDisabled) {
bulk.push({
insertOne: {
document: {
op: context.op,
modelName: context.modelName,
collectionName: context.collectionName,
collectionId: oldDoc._id as Types.ObjectId,
doc: oldDoc,
collectionId: doc._id as Types.ObjectId,
doc,
version: 0
}
}
Expand All @@ -110,6 +106,14 @@ async function deletePatch<T> (opts: IPluginOptions<T>, context: IContext<T>): P
}
}

async function createPatch<T> (opts: IPluginOptions<T>, context: IContext<T>): Promise<void> {
await bulkPatch(opts, context, 'eventCreated', 'createdDocs')
}

async function deletePatch<T> (opts: IPluginOptions<T>, context: IContext<T>): Promise<void> {
await bulkPatch(opts, context, 'eventDeleted', 'deletedDocs')
}

/**
* @description Patch patch event emitter
*/
Expand All @@ -129,15 +133,13 @@ export const patchHistoryPlugin = function plugin<T> (schema: Schema<T>, opts: I
const context: IContext<T> = {
op: this.isNew ? 'create' : 'update',
modelName: opts.modelName ?? model.modelName,
collectionName: opts.collectionName ?? model.collection.collectionName
collectionName: opts.collectionName ?? model.collection.collectionName,
createdDocs: [current]
}

try {
if (this.isNew) {
if (opts.eventCreated) {
em.emit(opts.eventCreated, { doc: current })
}
await createPatch(opts, context, current)
await createPatch(opts, context)
} else {
const original = await model.findById(current._id).exec()
if (original) {
Expand All @@ -150,7 +152,18 @@ export const patchHistoryPlugin = function plugin<T> (schema: Schema<T>, opts: I
}
})

schema.pre(['findOneAndUpdate', 'update', 'updateOne', 'updateMany'], async function (this: IHookContext<T>, next) {
schema.post('insertMany', async function (docs) {
const context = {
op: 'create',
modelName: opts.modelName ?? this.modelName,
collectionName: opts.collectionName ?? this.collection.collectionName,
createdDocs: docs as unknown as HydratedDocument<T>[]
}

await createPatch(opts, context)
})

schema.pre(['update', 'updateOne', 'updateMany', 'findOneAndUpdate'], async function (this: IHookContext<T>, next) {
const filter = this.getFilter()
const update = this.getUpdate() as Record<string, Partial<T>> | null
const options = this.getOptions()
Expand All @@ -177,6 +190,7 @@ export const patchHistoryPlugin = function plugin<T> (schema: Schema<T>, opts: I
const cursor = this.model.find<HydratedDocument<T>>(filter).cursor()
await cursor.eachAsync(async (doc) => {
let current = doc.toObject({ depopulate: true }) as HydratedDocument<T>
const original = doc.toObject({ depopulate: true }) as HydratedDocument<T>
current = assign(current, update)
_.forEach(commands, (command) => {
try {
Expand All @@ -185,26 +199,28 @@ export const patchHistoryPlugin = function plugin<T> (schema: Schema<T>, opts: I
// we catch assign keys that are not implemented
}
})
await updatePatch(opts, this._context, current, doc.toObject({ depopulate: true }) as HydratedDocument<T>)
await updatePatch(opts, this._context, current, original)
})
next()
} catch (error) {
next(error as CallbackError)
}
})

schema.post(['findOneAndUpdate', 'update', 'updateOne', 'updateMany'], async function (this: IHookContext<T>) {
schema.post(['update', 'updateOne', 'updateMany', 'findOneAndUpdate'], async function (this: IHookContext<T>) {
const update = this.getUpdate()

if (update && this._context.isNew) {
const cursor = this.model.findOne<HydratedDocument<T>>(update).cursor()
await cursor.eachAsync(async (doc) => {
await cursor.eachAsync((doc) => {
const current = doc.toObject({ depopulate: true }) as HydratedDocument<T>
if (opts.eventCreated) {
em.emit(opts.eventCreated, { doc: current })
if (this._context.createdDocs) {
this._context.createdDocs.push(current)
} else {
this._context.createdDocs = [current]
}
await createPatch(opts, this._context, current)
})
await createPatch(opts, this._context)
}
})

Expand Down
135 changes: 135 additions & 0 deletions tests/plugin-event-created..test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import mongoose, { model } from 'mongoose'

import UserSchema from './schemas/UserSchema'
import { patchHistoryPlugin } from '../src/plugin'
import History from '../src/models/History'

import em from '../src/em'
import { USER_CREATED } from './constants/events'

jest.mock('../src/em', () => {
return {
emit: jest.fn()
}
})

describe('plugin - event created & patch history disabled', () => {
const uri = `${globalThis.__MONGO_URI__}${globalThis.__MONGO_DB_NAME__}`

UserSchema.plugin(patchHistoryPlugin, {
eventCreated: USER_CREATED,
patchHistoryDisabled: true
})

const User = model('User', UserSchema)

beforeAll(async () => {
await mongoose.connect(uri)
})

afterAll(async () => {
await mongoose.connection.close()
})

beforeEach(async () => {
await mongoose.connection.collection('users').deleteMany({})
await mongoose.connection.collection('history').deleteMany({})
})

it('should save and emit one create event', async () => {
const john = new User({ name: 'John', role: 'user' })
await john.save()

const history = await History.find({})
expect(history).toHaveLength(0)

expect(em.emit).toHaveBeenCalledTimes(1)
expect(em.emit).toHaveBeenCalledWith(USER_CREATED, {
doc: expect.objectContaining({
_id: john._id,
name: john.name,
role: john.role,
createdAt: john.createdAt,
updatedAt: john.updatedAt
})
})
})

it('should create and emit one create event', async () => {
const user = await User.create({ name: 'John', role: 'user' })

const history = await History.find({})
expect(history).toHaveLength(0)

expect(em.emit).toHaveBeenCalledTimes(1)
expect(em.emit).toHaveBeenCalledWith(USER_CREATED, {
doc: expect.objectContaining({
_id: user._id,
name: user.name,
role: user.role,
createdAt: user.createdAt,
updatedAt: user.updatedAt
})
})
})

it('should insertMany and emit one create event', async () => {
const [user] = await User.insertMany([{ name: 'John', role: 'user' }])

const history = await History.find({})
expect(history).toHaveLength(0)

expect(em.emit).toHaveBeenCalledTimes(1)
expect(em.emit).toHaveBeenCalledWith(USER_CREATED, {
doc: expect.objectContaining({
_id: user._id,
name: user.name,
role: user.role,
createdAt: user.createdAt,
updatedAt: user.updatedAt
})
})
})

it('should insertMany and emit three create events', async () => {
const users = await User.insertMany([
{ name: 'John', role: 'user' },
{ name: 'Alice', role: 'user' },
{ name: 'Bob', role: 'user' }
])

const [john, alice, bob] = users

const history = await History.find({})
expect(history).toHaveLength(0)

expect(em.emit).toHaveBeenCalledTimes(3)
expect(em.emit).toHaveBeenNthCalledWith(1, USER_CREATED, {
doc: expect.objectContaining({
_id: john._id,
name: john.name,
role: john.role,
createdAt: john.createdAt,
updatedAt: john.updatedAt
})
})
expect(em.emit).toHaveBeenNthCalledWith(2, USER_CREATED, {
doc: expect.objectContaining({
_id: alice._id,
name: alice.name,
role: alice.role,
createdAt: alice.createdAt,
updatedAt: alice.updatedAt
})
})
expect(em.emit).toHaveBeenNthCalledWith(3, USER_CREATED, {
doc: expect.objectContaining({
_id: bob._id,
name: bob.name,
role: bob.role,
createdAt: bob.createdAt,
updatedAt: bob.updatedAt
})
})
})
})
3 changes: 1 addition & 2 deletions tests/plugin-event-deleted.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import mongoose, { model } from 'mongoose'

import UserSchema from './schemas/UserSchema'
Expand All @@ -14,7 +13,7 @@ jest.mock('../src/em', () => {
}
})

describe('plugin - patch history disabled', () => {
describe('plugin - event delete & patch history disabled', () => {
const uri = `${globalThis.__MONGO_URI__}${globalThis.__MONGO_DB_NAME__}`

UserSchema.plugin(patchHistoryPlugin, {
Expand Down
13 changes: 0 additions & 13 deletions tests/utils/filesystem.ts

This file was deleted.

0 comments on commit d009d28

Please sign in to comment.