import { afterEach, describe, expect, it } from 'vitest' import { DocumentStorageKV, Ed25519Signer, Ed25519Verifier, EventStorageKV, KVStorageComposite, KVStorageEncrypted, KVStorageHttp, KVStorageHttpServer, KVStorageInMemory, type Fields, type Many, } from '../src/index.js' import { collect } from './helpers.js' // The README's examples, run as written (imports from the package entry point), so they stay correct. const closers: (() => Promise)[] = [] afterEach(async () => { for (const close of closers.splice(0)) await close() }) describe('README', () => { it('KV storage', async () => { const db = new KVStorageInMemory() const users = db.child('users') await users.set('1', { name: 'Ada' }) expect(await users.child('1').get()).toEqual({ name: 'Ada' }) expect(await users.get('1')).toEqual({ name: 'Ada' }) await users.setMany([ ['2', { name: 'Bob' }], ['3', { name: 'Cy' }], ]) expect(await users.getMany(['1', 'missing'])).toEqual([{ name: 'Ada' }, undefined]) expect(await collect(users.keys({ limit: 10 }))).toEqual(['1', '2', '3']) expect((await collect(users.entries({ reverse: true }))).map(([key]) => key)).toEqual(['3', '2', '1']) await users.delete('3') await users.deleteTree() expect(await collect(db.scan())).toEqual([]) const settings = db.child('settings') await settings.setFields({ theme: 'dark', fontSize: 14 }) expect(await settings.get('theme')).toBe('dark') expect(await settings.getFields()).toEqual({ fontSize: 14, theme: 'dark' }) const changes: unknown[] = [] db.on('change', ({ changes: c, origin }) => changes.push([origin, c])) await users.set('1', { name: 'Ada' }) expect(changes).toEqual([['local', [{ key: 'users/1', value: { name: 'Ada' } }]]]) }) it('types', async () => { type User = { name: string } type Schema = { children: { users: Many; settings: { children: Fields<{ theme: string; fontSize: number }> } } } const typed = new KVStorageInMemory() await typed.child('users').set('1', { name: 'Ada' }) // @ts-expect-error not a User await typed.child('users').set('2', { nom: 'x' }) await typed.child('settings').set('theme', 'dark') const theme: string | undefined = await typed.child('settings').get('theme') expect(theme).toBe('dark') }) it('event storage', async () => { const events = new EventStorageKV(new KVStorageInMemory()) const orders = events.child('orders') const [id] = await orders.append('o1', [{ type: 'created', total: 30 }]) await orders.child('o1').append([{ type: 'paid' }]) expect((await collect(orders.read('o1'))).map((e) => e.data)).toEqual([{ type: 'created', total: 30 }, { type: 'paid' }]) expect((await collect(orders.read('o1', { after: id }))).map((e) => e.data)).toEqual([{ type: 'paid' }]) expect(await collect(orders.keys())).toEqual(['o1']) }) it('document storage', async () => { const docs = new DocumentStorageKV(new KVStorageInMemory()) const people = docs.child('people') const id = await people.insert({ name: 'Ada', age: 36, tags: ['math'] }) await people.set('grace', { name: 'Grace', age: 45, tags: ['navy', 'cobol'] }) await people.update(id, { $inc: { age: 1 }, $push: { tags: 'engines' } }) expect((await collect(people.find({ age: { $gte: 37 } }, { sort: { age: -1 }, limit: 10 }))).map((r) => r.key)).toEqual(['grace', id]) expect((await people.findOne({ tags: 'cobol' }))?.key).toBe('grace') expect(await people.get(id)).toEqual({ name: 'Ada', age: 37, tags: ['math', 'engines'] }) expect(await people.delete('grace')).toBe(true) }) it('sync, offline and encryption (server and client)', async () => { const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: { verifiers: [new Ed25519Verifier()] }, basePath: '/kv', allowOrigins: ['https://app.example.com'], }) const fetch = (url: string, init: RequestInit) => server.handle(new Request(url, init)) const signer = await Ed25519Signer.generate() const saved = await signer.exportPrivateKey() expect((await Ed25519Signer.fromPrivateKey(saved)).publicKey).toBe(signer.publicKey) const salt = crypto.getRandomValues(new Uint8Array(16)) const key = await KVStorageEncrypted.deriveKey('correct horse battery staple', salt, 1000) const local = new KVStorageInMemory() // stands in for KVStorageIndexedDB const db = new KVStorageComposite({ source: new KVStorageEncrypted(new KVStorageHttp({ url: 'https://api.example.com/kv', signer, fetch }), { key }), replicas: [new KVStorageInMemory(), local], queue: local.child('~sync'), prefix: signer.publicKey, }) closers.push(() => db.close()) const me = db.child(signer.publicKey) await me.child('notes').set('1', { text: 'Works offline' }) expect(await me.child('notes').get('1')).toEqual({ text: 'Works offline' }) type Todo = { title: string; done: boolean } const todos = new DocumentStorageKV<{ children: { todos: Many } }>(me).child('todos') await todos.insert({ title: 'Ship it', done: false }) expect((await collect(todos.find({ done: false }))).map((r) => r.data.title)).toEqual(['Ship it']) // Another device with the same keys sees the data; the server only has ciphertext. const other = new KVStorageComposite({ source: new KVStorageEncrypted(new KVStorageHttp({ url: 'https://api.example.com/kv', signer, fetch }), { key }), replicas: [new KVStorageInMemory()], prefix: signer.publicKey, }) closers.push(() => other.close()) expect(await other.child(signer.publicKey).child('notes').get('1')).toEqual({ text: 'Works offline' }) const http = new KVStorageHttp({ url: 'https://api.example.com/kv', signer, fetch }) closers.push(() => http.close()) const [stored] = await http.readMany([`${signer.publicKey}/notes/1`]) expect(stored).toBeInstanceOf(Uint8Array) const pushed = http.waitFor('change', () => true) await http.child(signer.publicKey).child('notes').subscribe() await me.child('notes').set('2', { text: 'live' }) expect((await pushed).changes[0]!.key).toBe(`${signer.publicKey}/notes/2`) }) })