UNPKG

@naturalcycles/db-lib

Version:

Lowest Common Denominator API to supported Databases

298 lines (251 loc) 7.88 kB
import { _truncate } from '@naturalcycles/js-lib/string/string.util.js' import type { BaseDBEntity, ObjectWithId } from '@naturalcycles/js-lib/types' import { _objectAssign } from '@naturalcycles/js-lib/types' import type { Pipeline } from '@naturalcycles/nodejs-lib/stream' import type { CommonDao } from '../commondao/common.dao.js' import type { CommonDaoOptions, CommonDaoReadOptions, CommonDaoStreamDeleteOptions, CommonDaoStreamOptions, } from '../commondao/common.dao.model.js' import type { RunQueryResult } from '../db.model.js' /** * Modeled after Firestore operators (WhereFilterOp type) * * As explained in https://firebase.google.com/docs/firestore/query-data/queries * * 'array-contains' applies to the field of type ARRAY, returns a doc if the array contains the given value, * e.g .filter('languages', 'array-contains', 'en') * where 'languages' can be e.g ['en', 'sv'] * * 'in' applies to a non-ARRAY fields, but allows to pass multiple values to compare with, e.g: * .filter('lang', 'in', ['en', 'sv']) * will returns users that have EITHER en OR sv in their language * * 'array-contains-any' applies to ARRAY field and ARRAY of given arguments, * works like an "intersection". Returns a document if intersection is not empty, e.g: * .filter('languages', 'array-contains-any', ['en', 'sv']) * * You may also look at queryInMemory() for its implementation (it implements all those). */ export type DBQueryFilterOperator = | '<' | '<=' | '==' | '!=' | '>=' | '>' | 'in' | 'not-in' | 'array-contains' | 'array-contains-any' export const dbQueryFilterOperatorValues: DBQueryFilterOperator[] = [ '<', '<=', '==', '!=', '>=', '>', 'in', 'not-in', 'array-contains', 'array-contains-any', ] export interface DBQueryFilter<ROW extends ObjectWithId> { name: keyof ROW op: DBQueryFilterOperator val: any } export interface DBQueryOrder<ROW extends ObjectWithId> { name: keyof ROW descending?: boolean } /** * Lowest Common Denominator Query object. * To be executed by CommonDao / CommonDB. * * Fluent API (returns `this` after each method). * * Methods do MUTATE the query object, be careful. * * <DBM> is the type of **queried** object (so e.g `key of DBM` can be used), not **returned** object. */ export class DBQuery<ROW extends ObjectWithId> { constructor(public table: string) {} /** * Convenience method. */ static create<ROW extends ObjectWithId>(table: string): DBQuery<ROW> { return new DBQuery(table) } static fromPlainObject<ROW extends ObjectWithId>( obj: Partial<DBQuery<ROW>> & { table: string }, ): DBQuery<ROW> { return Object.assign(new DBQuery<ROW>(obj.table), obj) } _filters: DBQueryFilter<ROW>[] = [] _limitValue = 0 // 0 means "no limit" _offsetValue = 0 // 0 means "no offset" _orders: DBQueryOrder<ROW>[] = [] _startCursor?: string _endCursor?: string /** * If defined - only those fields will be selected. * In undefined - all fields (*) will be returned. */ _selectedFieldNames?: (keyof ROW)[] _groupByFieldNames?: (keyof ROW)[] _distinct = false filter(name: keyof ROW, op: DBQueryFilterOperator, val: any): this { this._filters.push({ name, op, val }) return this } filterEq(name: keyof ROW, val: any): this { this._filters.push({ name, op: '==', val }) return this } filterIn(name: keyof ROW, val: any[]): this { this._filters.push({ name, op: 'in', val }) return this } /** * Passing 0 means "no limit". */ limit(limit: number): this { this._limitValue = limit return this } offset(offset: number): this { this._offsetValue = offset return this } order(name: keyof ROW, descending?: boolean): this { this._orders.push({ name, descending, }) return this } select(fieldNames: (keyof ROW)[]): this { this._selectedFieldNames = fieldNames return this } groupBy(fieldNames: (keyof ROW)[]): this { this._groupByFieldNames = fieldNames return this } distinct(distinct = true): this { this._distinct = distinct return this } startCursor(startCursor?: string): this { this._startCursor = startCursor return this } endCursor(endCursor?: string): this { this._endCursor = endCursor return this } clone(): DBQuery<ROW> { return _objectAssign(new DBQuery<ROW>(this.table), { _filters: [...this._filters], _limitValue: this._limitValue, _offsetValue: this._offsetValue, _orders: [...this._orders], _selectedFieldNames: this._selectedFieldNames && [...this._selectedFieldNames], _groupByFieldNames: this._groupByFieldNames && [...this._groupByFieldNames], _distinct: this._distinct, _startCursor: this._startCursor, _endCursor: this._endCursor, }) } pretty(): string { return this.prettyConditions().join(', ') } prettyConditions(): string[] { const tokens: string[] = [] // if (this.name) { // tokens.push(`"${this.name}"`) // } if (this._selectedFieldNames) { tokens.push( `select${this._distinct ? ' distinct' : ''}(${this._selectedFieldNames.join(',')})`, ) } tokens.push( ...this._filters.map(f => `${f.name as string}${f.op}${f.val}`), ...this._orders.map(o => `order by ${o.name as string}${o.descending ? ' desc' : ''}`), ) if (this._groupByFieldNames) { tokens.push(`groupBy(${this._groupByFieldNames.join(',')})`) } if (this._offsetValue) { tokens.push(`offset ${this._offsetValue}`) } if (this._limitValue) { tokens.push(`limit ${this._limitValue}`) } if (this._startCursor) { tokens.push(`startCursor ${_truncate(this._startCursor, 8)}`) } if (this._endCursor) { tokens.push(`endCursor ${_truncate(this._endCursor, 8)}`) } return tokens } } /** * DBQuery that has additional method to support Fluent API style. */ export class RunnableDBQuery< BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID extends string = BM['id'], > extends DBQuery<DBM> { /** * Pass `table` to override table. */ constructor( public dao: CommonDao<BM, DBM, ID>, table?: string, ) { super(table || dao.cfg.table) } async runQuery(opt?: CommonDaoReadOptions): Promise<BM[]> { return await this.dao.runQuery(this, opt) } async runQuerySingleColumn<T = any>(opt?: CommonDaoReadOptions): Promise<T[]> { return await this.dao.runQuerySingleColumn<T>(this, opt) } async runQueryAsDBM(opt?: CommonDaoReadOptions): Promise<DBM[]> { return await this.dao.runQueryAsDBM(this, opt) } async runQueryExtended(opt?: CommonDaoReadOptions): Promise<RunQueryResult<BM>> { return await this.dao.runQueryExtended(this, opt) } async runQueryExtendedAsDBM(opt?: CommonDaoReadOptions): Promise<RunQueryResult<DBM>> { return await this.dao.runQueryExtendedAsDBM(this, opt) } async runQueryCount(opt?: CommonDaoReadOptions): Promise<number> { return await this.dao.runQueryCount(this, opt) } async patchByQuery(patch: Partial<DBM>, opt?: CommonDaoOptions): Promise<number> { return await this.dao.patchByQuery(this, patch, opt) } streamQuery(opt?: CommonDaoStreamOptions<BM>): Pipeline<BM> { return this.dao.streamQuery(this, opt) } streamQueryAsDBM(opt?: CommonDaoStreamOptions<DBM>): Pipeline<DBM> { return this.dao.streamQueryAsDBM(this, opt) } async queryIds(opt?: CommonDaoReadOptions): Promise<ID[]> { return await this.dao.queryIds(this, opt) } streamQueryIds(opt?: CommonDaoStreamOptions<ID>): Pipeline<ID> { return this.dao.streamQueryIds(this, opt) } async deleteByQuery(opt?: CommonDaoStreamDeleteOptions<DBM>): Promise<number> { return await this.dao.deleteByQuery(this, opt) } }