UNPKG

@naturalcycles/db-lib

Version:

Lowest Common Denominator API to supported Databases

379 lines (332 loc) 11.4 kB
import type { ValidationFunction } from '@naturalcycles/js-lib' import type { AppError, ErrorMode } from '@naturalcycles/js-lib/error' import type { CommonLogger } from '@naturalcycles/js-lib/log' import type { BaseDBEntity, NumberOfMilliseconds, Promisable, UnixTimestamp, } from '@naturalcycles/js-lib/types' import type { TransformLogProgressOptions, TransformMapOptions, } from '@naturalcycles/nodejs-lib/stream' import type { CommonDB } from '../commondb/common.db.js' import type { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions } from '../db.model.js' export interface CommonDaoHooks<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID = BM['id']> { /** * Allows to override the id generation function. * By default it uses `stringId` from nodejs-lib * (which uses lowercase alphanumberic alphabet and the size of 16). */ createRandomId: () => ID /** * createNaturalId hook is called (tried) first. * If it doesn't exist - createRandomId is called. */ createNaturalId: (obj: DBM | BM) => ID /** * It's a counter-part of `createNaturalId`. * Allows to provide a parser function to parse "natural id" into * DBM components (e.g accountId and some other property that is part of the id). */ parseNaturalId: (id: ID) => Partial<DBM> /** * It is called only on `dao.create` method. * Dao.create method is called in: * * - getByIdOrEmpty, getByIdAsDBMOrEmpty * - patch, patchAsDBM */ beforeCreate: (bm: Partial<BM>) => Partial<BM> beforeDBMToBM: (dbm: DBM) => Partial<BM> | Promise<Partial<BM>> beforeBMToDBM: (bm: BM) => Partial<DBM> | Promise<Partial<DBM>> /** * Allows to access the DBM just after it has been loaded from the DB. * * Normally does nothing. * * You can change the DBM as you want here: ok to mutate or not, but you need to return the DBM * to pass it further. * * You can return `null` to make it look "not found". * * You can do validations as needed here and throw errors, they will be propagated. */ afterLoad?: (dbm: DBM) => Promisable<DBM | null> /** * Allows to access the DBM just before it's supposed to be saved to the DB. * * Normally does nothing. * * You can change the DBM as you want here: ok to mutate or not, but you need to return the DBM * to pass it further. * * You can return `null` to prevent it from being saved, without throwing an error. * `.save` method will then return the BM/DBM as it has entered the method (it **won't** return the null value!). * * You can do validations as needed here and throw errors, they will be propagated. */ beforeSave?: (dbm: DBM) => Promisable<DBM | null> /** * Called in: * - dbmToBM (applied before DBM becomes BM) * - anyToDBM * * Hook only allows to apply anonymization to DBM (not to BM). * It still applies to BM "transitively", during dbmToBM * (e.g after loaded from the Database). */ anonymize: (dbm: DBM) => DBM /** * If hook is defined - allows to prevent or modify the error thrown. * Return `false` to prevent throwing an error. * Return original `err` to pass the error through (will be thrown in CommonDao). * Return modified/new `Error` if needed. */ onValidationError: (err: AppError) => Error | false } export enum CommonDaoLogLevel { /** * Same as undefined */ NONE = 0, /** * Log operations (e.g "getById returned 1 row"), but not data */ OPERATIONS = 10, /** * Log operations and data for single operations (e.g getById), but not batch operations. */ DATA_SINGLE = 20, /** * Log EVERYTHING - all data passing in and out (max 10 rows). Very verbose! */ DATA_FULL = 30, } export interface CommonDaoCfg< BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, ID = BM['id'], > { db: CommonDB table: string /** * Experimental alternative to bmSchema. * "Bring your own validation function". * It removes the knowledge from CommonDao about the validation library used * and abstracts it away. */ validateBM?: ValidationFunction<BM, any> excludeFromIndexes?: (keyof DBM)[] /** * Defaults to true. * If set to false - load (read) operations will skip validation (and conversion). */ validateOnLoad?: boolean /** * Defaults to true. * If set to false - save (write) operations will skip validation (and conversion). */ validateOnSave?: boolean /** * Defaults to false. * Setting it to true will set saveMethod to `insert` for save/saveBatch, which will * fail for rows that already exist in the DB (if CommonDB implementation supports it). * * `delete*` and `patch` will throw. * * You can still override saveMethod, or set opt.allowMutability to allow deletion. */ immutable?: boolean /** * Defaults to false. * Set to true to limit DB writing (will throw an error in such case). */ readOnly?: boolean /** * Defaults to `console` */ logger?: CommonLogger /** * @default NONE */ logLevel?: CommonDaoLogLevel /** * @default false */ logStarted?: boolean // Hooks are designed with inspiration from got/ky interface hooks?: Partial<CommonDaoHooks<BM, DBM, ID>> /** * Defaults to true. * Set to false to disable auto-generation of `id`. * Useful e.g when your DB is generating ids by itself (e.g mysql auto_increment). */ generateId?: boolean /** * See the same option in CommonDB. * Defaults to false normally. */ assignGeneratedIds?: boolean /** * Defaults to true * Set to false to disable `created` field management. */ useCreatedProperty?: boolean /** * Defaults to true * Set to false to disable `updated` field management. */ useUpdatedProperty?: boolean /** * Defaults to false. * If true - run patch operations (patch, patchById, patchByIdOrCreate) in a Transaction. * * @experimental */ patchInTransaction?: boolean } /** * All properties default to undefined. */ export interface CommonDaoOptions extends CommonDBOptions { /** * Defaults to false. * * If set to true - will disable validation. * One possible use case of doing this is - performance (as validation/transformation takes time, especially with Joi). */ skipValidation?: boolean /** * Defaults to undefined. * * Undefined means that it's up for the underlying validation library (implementation) * to mutate or not. * E.g joi and zod would deep-clone, while ajv would MUTATE. * * False ensures that the input is not mutated by the Validation function (`validateBM`). * * True ensures the opposite - that the Validation function will mutate the input object * if it needs to apply transformations, such as: * - stripping unknown properties * - converting types (e.g. string to number) * - applying transformations (which as string trim, toLowerCase, etc) */ mutateInput?: boolean /** * @default false */ preserveUpdatedCreated?: boolean /** * @default false (for streams). Setting to true enables deletion of immutable objects */ allowMutability?: boolean /** * If true - data will be anonymized (by calling a BaseDao.anonymize() hook that you can extend in your Dao implementation). * Only applicable to loading/querying/streaming_loading operations (n/a for saving). * There is additional validation applied AFTER Anonymization, so your anonymization implementation should keep the object valid. */ anonymize?: boolean /** * Allows to override the Table that this Dao is connected to, only in the context of this call. * * Useful e.g in AirtableDB where you can have one Dao to control multiple tables. */ table?: string } export interface CommonDaoReadOptions extends CommonDaoOptions { /** * If provided (and supported by the DB) - will read the data at that point in time (aka "Time machine" feature). * This feature is named PITR (point-in-time-recovery) query in Datastore. */ readAt?: UnixTimestamp } export interface CommonDaoSaveOptions<BM extends BaseDBEntity, DBM extends BaseDBEntity> extends CommonDaoSaveBatchOptions<DBM> { /** * If provided - a check will be made. * If the object for saving equals to the object passed to `skipIfEquals` - save operation will be skipped. * * Equality is checked with _deepJsonEquals (aka "deep equals after JSON.stringify/parse", which removes keys with undefined values). * * It's supposed to be used to prevent "unnecessary saves", when data is not changed. */ skipIfEquals?: BM } export interface CommonDaoPatchByIdOptions<DBM extends BaseDBEntity> extends CommonDaoSaveBatchOptions<DBM> { /** * Defaults to false. * With false, if the row doesn't exist - it will throw an error. * With true, if the row doesn't exist - it will be auto-created with `dao.create`. * * Use true when you expect the row to exist and it would be an error if it doesn't. */ createIfMissing?: boolean } export interface CommonDaoPatchOptions<DBM extends BaseDBEntity> extends CommonDaoSaveBatchOptions<DBM> { /** * If true - patch will skip loading from DB, and will just optimistically patch passed object. * * Consequently, when the row doesn't exist - it will be auto-created with `dao.create`. */ skipDBRead?: boolean } /** * All properties default to undefined. */ export interface CommonDaoSaveBatchOptions<DBM extends BaseDBEntity> extends CommonDaoOptions, CommonDBSaveOptions<DBM> { /** * @default false * * True would make sure that auto-generated id (only auto-generated, not passed!) is unique (not already present in DB). * If id is already present - auto-generator will retry auto-generating it few times, until it finds unused id. * If failed X times - will throw an error. * * Only applies to auto-generated ids! Does not apply to passed id. */ ensureUniqueId?: boolean } export interface CommonDaoStreamDeleteOptions<DBM extends BaseDBEntity> extends CommonDaoStreamOptions<DBM> {} export interface CommonDaoStreamSaveOptions<DBM extends BaseDBEntity> extends CommonDaoSaveBatchOptions<DBM>, CommonDaoStreamOptions<DBM> {} export interface CommonDaoStreamForEachOptions<IN> extends CommonDaoStreamOptions<IN>, TransformMapOptions<IN, any> {} export interface CommonDaoStreamOptions<IN> extends CommonDaoReadOptions, TransformLogProgressOptions<IN> { /** * @default true (for streams) */ skipValidation?: boolean /** * @default ErrorMode.SUPPRESS for returning ReadableStream, because .pipe() has no concept of "error propagation" * @default ErrorMode.SUPPRESS for .forEach() streams as well, but overridable */ errorMode?: ErrorMode /** * Applicable to some of stream operations, e.g deleteByQuery. * If set - `deleteByQuery` won't execute it "all at once", but in batches (chunks). * * Defaults to undefined, so the operation is executed "all at once". */ chunkSize?: number /** * When chunkSize is set - this option controls how many chunks to run concurrently. * Defaults to 32. */ chunkConcurrency?: number } export type CommonDaoCreateOptions = CommonDBCreateOptions export interface OnValidationTimeData { tookMillis: NumberOfMilliseconds table: string obj: any }