import { LevelUp } from 'levelup' import { Transform } from 'stream' export function Document(db: LevelUp) { return { createCollection } function createCollection({ collection }: { collection: string }) { return < Indexers extends { [name: string]: { prefix: string reducer: (value: T) => string | number | Array } } >( indexers: Indexers, ): Record & { put: (key: string, value: T) => Promise get: (key: string) => Promise del: (key: string) => Promise } => { return { batch: async ( ops: Array<{ type: 'put'; key: string; value: T } | { type: 'del'; key: string }>, ): Promise => { let readyToDelete = [] as { type: 'del'; key: string }[] const putOps = ops.filter(it => it.type === 'put') as { type: 'put'; key: string; value: T }[] const delOps = ops.filter(it => it.type === 'del') as { type: 'del'; key: string }[] let items = (await (db as any).getMany(delOps.map(it => it.key))) as T[] if (items.length !== delOps.length) { for (const delOp of delOps) { const item = await db.get(delOp.key) typeof item !== undefined ? del(delOp.key, item).forEach(it => readyToDelete.push(it)) : null } } else { items.forEach((it, i) => del(delOps[i]!.key, it).forEach(it => readyToDelete.push(it))) } return db.batch([...putOps.flatMap(it => add(it.key, it.value)), ...readyToDelete]) }, put: async (key: string, value: T): Promise => { // delete the old indexes return db.batch(add(key, value)) }, get: async (key: string): Promise => { return db.get(key) }, del: async (key: string): Promise => { const item = (await db.get(key)) as T | undefined if (!item) { return } return db.batch(del(key, item)) }, ...Object.keys(indexers).reduce((acc, it) => { const reducer = indexers[it]! acc[it as keyof Indexers] = { createReadStream } return acc function createReadStream(args: { gt?: string gte?: string lt?: string lte?: string reverse?: boolean limit?: number }) { const templatedArgs = ['gt', 'gte', 'lt', 'lte'].reduce( (acc, it) => { if (it in args) { acc[it as keyof typeof acc] = `${collection}!${reducer.prefix}!${ args[it as 'gt' | 'gte' | 'lt' | 'lte'] }` } return acc }, {} as { gt?: string gte?: string lt?: string lte?: string }, ) const fetch = new Transform({ readableObjectMode: true, writableObjectMode: true, transform(chunk, encoding, callback) { db.get(chunk.value, (err, value) => { if (err) return callback(err) callback(null, { key: chunk.value.replace(`${collection}!default!`, ''), value }) }) }, }) return db .createReadStream({ ...args, ...templatedArgs, }) .pipe(fetch) } }, {} as Record), } function add(key: string, value: T): { type: 'put'; key: string; value: unknown }[] { return [ { type: 'put', key: `${collection}!default!${key}`, value }, ...Object.values(indexers).flatMap(it => { const reducedValue = it.reducer(value) const reducedValues = Array.isArray(reducedValue) ? reducedValue : [reducedValue] return reducedValues.map(value => { return { type: 'put' as const, key: `${collection}!${it.prefix}!${value}`, value: `${collection}!default!${key}`, } }) }), ] } function del(key: string, item: T): { type: 'del'; key: string }[] { return [ { type: 'del', key: `${collection}!default!${key}` }, ...Object.values(indexers).flatMap(it => { const reducedValue = it.reducer(item!) const reducedValues = Array.isArray(reducedValue) ? reducedValue : [reducedValue] return reducedValues.map(value => { return { type: 'del' as const, key: `${collection}!${it.prefix}!${value}`, } }) }), ] } } } }