- BaseKVStorage / BaseEventStorage / BaseDocumentStorage define the API once; implementations provide only raw primitives (client and server share an API) - Drop KV indexes, query planner, unique indexes; document stores take index fields - Escape every char <= '/' so child keys stay contiguous; skip-scan is now simple - Event-sourced documents = event storage + any document store for projections - New folder layout (kv-storage, event-storage, document-storage, utils); tests only in test/ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
87 lines
4.3 KiB
TypeScript
87 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { BaseDocumentStorage } from '../../src/document-storage/base-document-storage.js'
|
|
import { collect } from '../helpers.js'
|
|
|
|
type User = { name?: string; email?: string; age?: number; tags?: string[]; logins?: number; address?: { city?: string } }
|
|
|
|
/** Behaviour every document storage must have. `make` returns an empty storage. */
|
|
export function runDocumentStorageSuite(name: string, make: () => BaseDocumentStorage): void {
|
|
describe(`document storage: ${name}`, () => {
|
|
const users = () => make().child('users')
|
|
|
|
it('inserts with time-sortable ids, gets and lists', async () => {
|
|
const u = users()
|
|
const a = await u.insert({ name: 'Ada' })
|
|
const b = await u.insert({ name: 'Bob' })
|
|
expect(a < b).toBe(true)
|
|
expect(await u.get(a)).toEqual({ name: 'Ada' })
|
|
expect(await collect(u.keys())).toEqual([a, b])
|
|
})
|
|
|
|
it('set / get / delete in self and child forms', async () => {
|
|
const u = users()
|
|
await u.set('1', { name: 'Ada' })
|
|
expect(await u.child('1').get()).toEqual({ name: 'Ada' })
|
|
await u.child('1').set({ name: 'Ada L.' })
|
|
expect(await u.getMany(['1', '2'])).toEqual([{ name: 'Ada L.' }, undefined])
|
|
expect(await u.delete('1')).toBe(true)
|
|
expect(await u.child('1').delete()).toBe(false)
|
|
expect(await u.get('1')).toBeUndefined()
|
|
await u.set('1', { name: 'Again' })
|
|
expect(await u.get('1')).toEqual({ name: 'Again' })
|
|
})
|
|
|
|
it('updates with operators, returns the new state, and supports upsert', async () => {
|
|
const u = users()
|
|
await u.set('1', { name: 'Ada', tags: ['a'], logins: 1 })
|
|
expect(await u.update('1', { $inc: { logins: 2 }, $push: { tags: 'b' }, $set: { 'address.city': 'London' } })).toEqual({
|
|
name: 'Ada',
|
|
tags: ['a', 'b'],
|
|
logins: 3,
|
|
address: { city: 'London' },
|
|
})
|
|
expect(await u.child('1').update({ $unset: { address: 1 } })).toEqual({ name: 'Ada', tags: ['a', 'b'], logins: 3 })
|
|
expect(await u.update('missing', { $set: { a: 1 } })).toBeUndefined()
|
|
expect(await u.update('missing', { $inc: { n: 1 } }, { upsert: true })).toEqual({ n: 1 })
|
|
})
|
|
|
|
it('rejects bad keys and documents', async () => {
|
|
const u = users()
|
|
await expect(u.get(undefined as never)).rejects.toThrow(TypeError)
|
|
await expect(u.update(undefined as never, { $set: {} })).rejects.toThrow(TypeError)
|
|
await expect(make().get()).rejects.toThrow(/root/)
|
|
await expect(u.set('x', [] as never)).rejects.toThrow(/plain object/)
|
|
})
|
|
|
|
it('nests documents under documents', async () => {
|
|
const u = users()
|
|
await u.set('1', { name: 'Ada' })
|
|
await u.child('1').child('posts').set('p1', { title: 'Hi' })
|
|
expect(await u.child('1').child('posts').get('p1')).toEqual({ title: 'Hi' })
|
|
expect((await collect(u.find())).map((r) => r.key)).toEqual(['1'])
|
|
})
|
|
|
|
it('finds with equality, comparisons, $in, dot paths, sort, skip and limit', async () => {
|
|
const u = users()
|
|
const rows: [string, User][] = [
|
|
['a', { name: 'Ada', age: 36, email: 'ada@x', tags: ['x', 'y'], address: { city: 'London' } }],
|
|
['b', { name: 'Bob', age: 20, email: 'bob@x', tags: ['y'] }],
|
|
['c', { name: 'Cy', age: 51, address: { city: 'London' } }],
|
|
['d', { name: 'Di', age: 20 }],
|
|
]
|
|
for (const [k, v] of rows) await u.set(k, v)
|
|
const keys = async (filter: Record<string, unknown>, options = {}) => (await collect(u.find(filter, options))).map((r) => r.key)
|
|
expect(await keys({ age: 20 })).toEqual(['b', 'd'])
|
|
expect(await keys({ age: { $gte: 20, $lt: 40 } })).toEqual(['a', 'b', 'd'])
|
|
expect(await keys({ age: { $in: [36, 51] } })).toEqual(['a', 'c'])
|
|
expect(await keys({ 'address.city': 'London', age: { $gt: 40 } })).toEqual(['c'])
|
|
expect(await keys({ tags: 'y' })).toEqual(['a', 'b'])
|
|
expect(await keys({ email: null })).toEqual(['c', 'd'])
|
|
expect(await keys({}, { sort: { age: -1 } })).toEqual(['c', 'a', 'b', 'd'])
|
|
expect(await keys({}, { sort: { age: 1 }, skip: 1, limit: 2 })).toEqual(['d', 'a'])
|
|
expect(await u.findOne({ email: 'bob@x' })).toEqual({ key: 'b', data: rows[1]![1] })
|
|
expect(await u.findOne({ email: 'nobody' })).toBeUndefined()
|
|
})
|
|
})
|
|
}
|