* Adapter contract tests — run the SAME assertions against BOTH NedbStore and
* FileStore to lock in behavioural parity. These directly cover:
* - default `multi:false` for update/remove (matches NeDB semantics)
* - update/remove return a number (the StoreAdapter contract)
* - compound query operators evaluated independently ({ $gte, $lte }, ...)
*/
const fs = require('fs')
const os = require('os')
const path = require('path')
const NedbStore = require('../../src/store/NedbStore')
const FileStore = require('../../src/store/FileStore')
const INDEX_OPTS = { indexes: [{ fieldName: '_id', unique: true }] }
const createdDirs = []
const adapterFactories = [
[
'NedbStore',
() => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'contract-nedb-'))
createdDirs.push(dir)
return new NedbStore({ filename: path.join(dir, 'contract.db'), ...INDEX_OPTS })
}
],
[
'FileStore',
() => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'contract-file-'))
createdDirs.push(dir)
return new FileStore('things', dir, { ...INDEX_OPTS, namingFields: ['name'] })
}
]
]
afterAll(() => {
for (const dir of createdDirs) {
try {
fs.rmSync(dir, { recursive: true, force: true })
} catch {
}
}
})
describe.each(adapterFactories)('%s honours the StoreAdapter contract', (_name, factory) => {
let store
beforeEach(() => {
store = factory()
})
const seedGroup = async () => {
await store.insert({ _id: 'a', name: 'alpha', group: 'g' })
await store.insert({ _id: 'b', name: 'beta', group: 'g' })
await store.insert({ _id: 'c', name: 'gamma', group: 'g' })
}
test('insert returns the document with its _id', async () => {
const doc = await store.insert({ _id: 'a', name: 'alpha' })
expect(doc._id).toBe('a')
})
test('findOne / find read back inserted docs', async () => {
await store.insert({ _id: 'a', name: 'alpha' })
const one = await store.findOne({ _id: 'a' })
expect(one && one.name).toBe('alpha')
const all = await store.find({})
expect(all).toHaveLength(1)
})
test('unique _id constraint rejects a duplicate insert', async () => {
await store.insert({ _id: 'a', name: 'alpha' })
await expect(store.insert({ _id: 'a', name: 'alpha-2' })).rejects.toBeDefined()
})
test('update defaults to a single record and returns a number count', async () => {
await seedGroup()
const affected = await store.update({ group: 'g' }, { $set: { tagged: true } })
expect(affected).toBe(1)
expect(typeof affected).toBe('number')
const tagged = await store.find({ tagged: true })
expect(tagged).toHaveLength(1)
})
test('update with { multi: true } updates all matches and returns the count', async () => {
await seedGroup()
const affected = await store.update({ group: 'g' }, { $set: { tagged: true } }, { multi: true })
expect(affected).toBe(3)
const tagged = await store.find({ tagged: true })
expect(tagged).toHaveLength(3)
})
test('remove defaults to a single record and returns a number count', async () => {
await seedGroup()
const removed = await store.remove({ group: 'g' })
expect(removed).toBe(1)
expect(typeof removed).toBe('number')
const remaining = await store.find({ group: 'g' })
expect(remaining).toHaveLength(2)
})
test('remove with { multi: true } removes all matches', async () => {
await seedGroup()
const removed = await store.remove({ group: 'g' }, { multi: true })
expect(removed).toBe(3)
const remaining = await store.find({ group: 'g' })
expect(remaining).toHaveLength(0)
})
test('compound query { $gte, $lte } evaluates every operator on a field', async () => {
await store.insert({ _id: 'p1', age: 10 })
await store.insert({ _id: 'p2', age: 20 })
await store.insert({ _id: 'p3', age: 40 })
const matches = await store.find({ age: { $gte: 15, $lte: 30 } })
expect(matches).toHaveLength(1)
expect(matches[0]._id).toBe('p2')
})
test('single operators $ne / $in / $nin / $regex behave consistently', async () => {
await store.insert({ _id: 'p1', name: 'foo' })
await store.insert({ _id: 'p2', name: 'bar' })
await store.insert({ _id: 'p3', name: 'baz' })
expect(await store.find({ _id: { $ne: 'p1' } })).toHaveLength(2)
expect(await store.find({ _id: { $in: ['p1', 'p2'] } })).toHaveLength(2)
expect(await store.find({ _id: { $nin: ['p1'] } })).toHaveLength(2)
expect(await store.find({ name: { $regex: /^ba/ } })).toHaveLength(2)
})
test('plain object query value deep-compares instead of matching all', async () => {
await store.insert({ _id: 'o1', config: { mode: 'x', n: 1 } })
await store.insert({ _id: 'o2', config: { mode: 'y', n: 2 } })
await store.insert({ _id: 'o3', config: { mode: 'x', n: 3 } })
const matches = await store.find({ config: { mode: 'x', n: 1 } })
expect(matches).toHaveLength(1)
expect(matches[0]._id).toBe('o1')
expect(await store.find({ config: { mode: 'z' } })).toHaveLength(0)
})
test('array query value deep-compares instead of matching all', async () => {
await store.insert({ _id: 'a1', tags: ['red', 'blue'] })
await store.insert({ _id: 'a2', tags: ['green'] })
const matches = await store.find({ tags: ['red', 'blue'] })
expect(matches).toHaveLength(1)
expect(matches[0]._id).toBe('a1')
expect(await store.find({ tags: ['purple'] })).toHaveLength(0)
})
test('updating a naming field to a case-only variant keeps the document', async () => {
await store.insert({ _id: 'login', name: 'Login' })
const affected = await store.update({ _id: 'login' }, { $set: { name: 'login' } })
expect(affected).toBe(1)
const doc = await store.findOne({ _id: 'login' })
expect(doc).not.toBeNull()
expect(doc.name).toBe('login')
expect(await store.find({})).toHaveLength(1)
})
})