@naturalcycles/db-lib
Version:
Lowest Common Denominator API to supported Databases
288 lines (236 loc) • 8.67 kB
text/typescript
import { _isTruthy } from '@naturalcycles/js-lib'
import type { ObjectWithId, StringMap } from '@naturalcycles/js-lib/types'
import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv'
import { Pipeline } from '@naturalcycles/nodejs-lib/stream'
import { BaseCommonDB } from '../../commondb/base.common.db.js'
import type { CommonDB, CommonDBSupport } from '../../commondb/common.db.js'
import { commonDBFullSupport } from '../../commondb/common.db.js'
import type { RunQueryResult } from '../../db.model.js'
import type { DBQuery } from '../../query/dbQuery.js'
import type {
CacheDBCfg,
CacheDBCreateOptions,
CacheDBOptions,
CacheDBSaveOptions,
CacheDBStreamOptions,
} from './cache.db.model.js'
/**
* CommonDB implementation that proxies requests to downstream CommonDB
* and does in-memory caching.
*
* Queries always hit downstream (unless `onlyCache` is passed)
*/
export class CacheDB extends BaseCommonDB implements CommonDB {
override support: CommonDBSupport = {
...commonDBFullSupport,
transactions: false,
increment: false,
}
constructor(cfg: CacheDBCfg) {
super()
this.cfg = {
logger: console,
...cfg,
}
}
cfg: CacheDBCfg
override async ping(): Promise<void> {
await Promise.all([this.cfg.cacheDB.ping(), this.cfg.downstreamDB.ping()])
}
/**
* Resets InMemory DB data
*/
// This method is no longer in the public API. Call it just on the InMemoryDB if needed.
// async resetCache(table?: string): Promise<void> {
// this.log(`resetCache ${table || 'all'}`)
// await this.cfg.cacheDB.resetCache(table)
// }
override async getTables(): Promise<string[]> {
return await this.cfg.downstreamDB.getTables()
}
override async getTableSchema<ROW extends ObjectWithId>(table: string): Promise<JsonSchema<ROW>> {
return await this.cfg.downstreamDB.getTableSchema<ROW>(table)
}
override async createTable<ROW extends ObjectWithId>(
table: string,
schema: JsonSchema<ROW>,
opt: CacheDBCreateOptions = {},
): Promise<void> {
if (!opt.onlyCache && !this.cfg.onlyCache) {
await this.cfg.downstreamDB.createTable(table, schema, opt)
}
if (!opt.skipCache && !this.cfg.skipCache) {
await this.cfg.cacheDB.createTable(table, schema, opt)
}
}
override async getByIds<ROW extends ObjectWithId>(
table: string,
ids: string[],
opt: CacheDBSaveOptions<ROW> = {},
): Promise<ROW[]> {
const resultMap: StringMap<ROW> = {}
const missingIds: string[] = []
if (!opt.skipCache && !this.cfg.skipCache) {
const results = await this.cfg.cacheDB.getByIds<ROW>(table, ids, opt)
results.forEach(r => (resultMap[r.id] = r))
missingIds.push(...ids.filter(id => !resultMap[id]))
if (this.cfg.logCached) {
this.cfg.logger?.log(
`${table}.getByIds ${results.length} rows from cache: [${results
.map(r => r.id)
.join(', ')}]`,
)
}
}
if (missingIds.length && !opt.onlyCache && !this.cfg.onlyCache) {
const results = await this.cfg.downstreamDB.getByIds<ROW>(table, missingIds, opt)
results.forEach(r => (resultMap[r.id] = r))
if (this.cfg.logDownstream) {
this.cfg.logger?.log(
`${table}.getByIds ${results.length} rows from downstream: [${results
.map(r => r.id)
.join(', ')}]`,
)
}
if (!opt.skipCache) {
const cacheResult = this.cfg.cacheDB.saveBatch(table, results, opt)
if (this.cfg.awaitCache) await cacheResult
}
}
// return in right order
return ids.map(id => resultMap[id]).filter(_isTruthy)
}
override async saveBatch<ROW extends ObjectWithId>(
table: string,
rows: ROW[],
opt: CacheDBSaveOptions<ROW> = {},
): Promise<void> {
if (!opt.onlyCache && !this.cfg.onlyCache) {
await this.cfg.downstreamDB.saveBatch(table, rows, opt)
if (this.cfg.logDownstream) {
this.cfg.logger?.log(
`${table}.saveBatch ${rows.length} rows to downstream: [${rows
.map(r => r.id)
.join(', ')}]`,
)
}
}
if (!opt.skipCache && !this.cfg.skipCache) {
const cacheResult = this.cfg.cacheDB.saveBatch(table, rows, opt).then(() => {
if (this.cfg.logCached) {
this.cfg.logger?.log(
`${table}.saveBatch ${rows.length} rows to cache: [${rows.map(r => r.id).join(', ')}]`,
)
}
})
if (this.cfg.awaitCache) await cacheResult
}
}
override async runQuery<ROW extends ObjectWithId>(
q: DBQuery<ROW>,
opt: CacheDBSaveOptions<ROW> = {},
): Promise<RunQueryResult<ROW>> {
if (!opt.onlyCache && !this.cfg.onlyCache) {
const { rows, ...queryResult } = await this.cfg.downstreamDB.runQuery(q, opt)
if (this.cfg.logDownstream) {
this.cfg.logger?.log(`${q.table}.runQuery ${rows.length} rows from downstream`)
}
// Don't save to cache if it was a projection query
if (!opt.skipCache && !this.cfg.skipCache && !q._selectedFieldNames) {
const cacheResult = this.cfg.cacheDB.saveBatch(q.table, rows as any, opt)
if (this.cfg.awaitCache) await cacheResult
}
return { rows, ...queryResult }
}
if (opt.skipCache || this.cfg.skipCache) return { rows: [] }
const { rows, ...queryResult } = await this.cfg.cacheDB.runQuery(q, opt)
if (this.cfg.logCached) {
this.cfg.logger?.log(`${q.table}.runQuery ${rows.length} rows from cache`)
}
return { rows, ...queryResult }
}
override async runQueryCount<ROW extends ObjectWithId>(
q: DBQuery<ROW>,
opt: CacheDBOptions = {},
): Promise<number> {
if (!opt.onlyCache && !this.cfg.onlyCache) {
return await this.cfg.downstreamDB.runQueryCount(q, opt)
}
const count = await this.cfg.cacheDB.runQueryCount(q, opt)
if (this.cfg.logCached) {
this.cfg.logger?.log(`${q.table}.runQueryCount ${count} rows from cache`)
}
return count
}
override streamQuery<ROW extends ObjectWithId>(
q: DBQuery<ROW>,
opt: CacheDBStreamOptions = {},
): Pipeline<ROW> {
if (!opt.onlyCache && !this.cfg.onlyCache) {
const pipeline = this.cfg.downstreamDB.streamQuery<ROW>(q, opt)
// Don't save to cache if it was a projection query
if (!opt.skipCache && !this.cfg.skipCache && !q._selectedFieldNames) {
// todo: rethink if we really should download WHOLE stream into memory in order to save it to cache
// void obs
// .pipe(toArray())
// .toPromise()
// .then(async dbms => {
// await this.cfg.cacheDB.saveBatch(q.table, dbms as any)
// })
}
return pipeline
}
if (opt.skipCache || this.cfg.skipCache) return Pipeline.fromArray([])
const pipeline = this.cfg.cacheDB.streamQuery<ROW>(q, opt)
// if (this.cfg.logCached) {
// let count = 0
//
// void pMapStream(stream, async () => {
// count++
// }, {concurrency: 10})
// .then(length => {
// this.log(`${q.table}.streamQuery ${length} rows from cache`)
// })
// }
return pipeline
}
override async deleteByQuery<ROW extends ObjectWithId>(
q: DBQuery<ROW>,
opt: CacheDBOptions = {},
): Promise<number> {
if (!opt.onlyCache && !this.cfg.onlyCache) {
const deletedIds = await this.cfg.downstreamDB.deleteByQuery(q, opt)
if (this.cfg.logDownstream) {
this.cfg.logger?.log(
`${q.table}.deleteByQuery ${deletedIds} rows from downstream and cache`,
)
}
if (!opt.skipCache && !this.cfg.skipCache) {
const cacheResult = this.cfg.cacheDB.deleteByQuery(q, opt)
if (this.cfg.awaitCache) await cacheResult
}
return deletedIds
}
if (opt.skipCache || this.cfg.skipCache) return 0
const deletedIds = await this.cfg.cacheDB.deleteByQuery(q, opt)
if (this.cfg.logCached) {
this.cfg.logger?.log(`${q.table}.deleteByQuery ${deletedIds} rows from cache`)
}
return deletedIds
}
override async patchByQuery<ROW extends ObjectWithId>(
q: DBQuery<ROW>,
patch: Partial<ROW>,
opt: CacheDBOptions = {},
): Promise<number> {
let updated: number | undefined
if (!opt.onlyCache && !this.cfg.onlyCache) {
updated = await this.cfg.downstreamDB.patchByQuery(q, patch, opt)
}
if (!opt.skipCache && !this.cfg.skipCache) {
const cacheResult = this.cfg.cacheDB.patchByQuery(q, patch, opt)
if (this.cfg.awaitCache) updated ??= await cacheResult
}
return updated || 0
}
}