import "dotenv/config"; import { type ComputedFieldSpec } from "./expression.js"; import { type JournalFileOp } from "./journal.js"; export interface Data { id?: string | number; [key: string]: any; createdAt?: number; updatedAt?: number; } export type FieldType = "string" | "number" | "boolean" | "date" | "email" | "url" | "table" | "object" | "array" | "password" | "html" | "ip" | "json" | "id"; export type Field = { id?: number; key: string; type: FieldType | FieldType[]; required?: boolean; table?: string; unique?: boolean | number | string; children?: FieldType | FieldType[] | Schema; regex?: string; /** Computed-fields expression (see the README). Either a raw expression * string, or the persisted `{ expr, ast }` spec written by Inibase once * the expression has been compiled. */ computed?: string | ComputedFieldSpec; }; export type Schema = Field[]; export interface Options { page?: number; perPage?: number; columns?: string[] | string; sort?: Record | string[] | string; } /** Options for the aggregate actions (`sum`, and later avg/min/max). */ export interface AggregateOptions { /** * Aggregate over the *elements* of an array-of-objects column instead of * whole rows. The summed columns must be single-level child paths of the * same array root (e.g. `"items.quantity"`); `where` keys that resolve as * children of that root become per-element predicates. */ nested?: boolean; } export interface TableConfig { compression?: boolean; cache?: boolean; prepend?: boolean; decodeID?: boolean; /** * Opt a table into the incremental update log (format 2): `put` appends a * small frame instead of rewriting whole columns, and reads merge the log * over the base columns. Requires a schema of simple top-level fields and * is incompatible with `prepend`, transactions and DDL on the table * (those need compaction, shipped in 3.9). */ log?: boolean; } export interface TableObject { schema?: Schema; config: TableConfig; } /** * Per-table state maintained while a database transaction is open: the writer * lock is held for the whole transaction, pagination state is staged in * memory (the live files only change at commit()), and `staged` collects the * journaled ops that commit() publishes (one per table per transaction). */ export interface TxnTableEntry { locked: boolean; /** Live pagination path the first staged op builds on. */ paginationFrom: string; /** Staged last-id / row count (chained across ops in the txn). */ lastId: number; total: number; staged: { ops: JournalFileOp[]; pagination: { from: string; to: string; } | null; }[]; } export type ComparisonOperator = "=" | "!=" | ">" | "<" | ">=" | "<=" | "*" | "!*" | "[]" | "![]"; export type pageInfo = { total?: number; totalPages?: number; } & Options; export type Criteria = ({ [logic in "and" | "or"]?: Criteria | (string | number | boolean | null)[]; } & { [key: string]: string | number | boolean | undefined | Criteria | (string | number | boolean)[]; }) | null; type Entries = { [K in keyof T]: [K, T[K]]; }[keyof T][]; declare global { interface ObjectConstructor { entries(o: T): Entries; } } export declare const ERROR_CODES: readonly ["GROUP_UNIQUE", "FIELD_UNIQUE", "FIELD_REQUIRED", "NO_SCHEMA", "TABLE_EMPTY", "INVALID_ID", "INVALID_TYPE", "INVALID_PARAMETERS", "NO_ENV", "TABLE_EXISTS", "TABLE_NOT_EXISTS", "INVALID_REGEX_MATCH", "INVALID_NAME", "COMPUTED_FIELD_SYNTAX", "COMPUTED_FIELD_UNKNOWN_FIELD", "COMPUTED_FIELD_INVALID_LINK", "COMPUTED_FIELD_INVALID_TARGET", "COMPUTED_FIELD_CONFLICT", "COMPUTED_FIELD_CYCLE", "COMPUTED_FIELD_SETTABLE", "COMPUTED_FIELD_DANGLING_LINK", "COMPUTED_FIELD_ARITHMETIC", "UPDATE_LOG_CORRUPT", "UPDATE_LOG_FRAME_TOO_LARGE", "MAINTENANCE_REQUIRED", "UNSUPPORTED_FORMAT"]; export type ErrorCode = (typeof ERROR_CODES)[number]; export type ErrorLang = "en" | "ar" | "fr" | "es"; export declare const globalConfig: { [database: string]: { tables?: Map; }; } & { salt?: string | Buffer; }; /** * @param {string} database - Database name * @param {string} [mainFolder="."] - Main folder path * @param {ErrorLang} [language="en"] - Language for error messages */ export default class Inibase { pageInfo: Record; language: ErrorLang; fileExtension: string; totalItems: Map; /** Tracks whether a decodeID table's stored ids are the dense sequence * 1..rowCount (no rows ever deleted). When true, `get`/`put`/`delete` can * resolve numeric ids to line numbers arithmetically instead of scanning * the id file. Set false by any partial row deletion. */ private idDensity; /** Per-table computed-plan cache (null = table has no computed fields). * A plan depends only on the table's persisted schema (compiled ASTs are * id-based and rename-proof), so it is rebuilt only when the schema * changes: see the invalidation in `getTable`'s reload branch, * `createTable` and `updateTableLocked`. Bounded by the number of tables. */ private readonly computedPlanCache; /** Per-table `.format` marker + live update-log overlays (format 2 tables). * Keyed by `databasePath\0tableName` so two databases in one process never * share state. Populated lazily by `getOverlay`. */ private readonly tableFormats; private readonly overlays; /** Last observed identity of each table's `.format` file (`mtime:size:ino`), * used to detect a compaction by another process without re-reading the * marker on every access. */ private readonly formatStamps; private databasePath; private uniqueMap; private schemaFileExtension; /** * Open database transaction (see begin/commit/rollback). Holds the * database lock (`/.tmp/.locked`) for its whole lifetime and the * per-table writer lock of every table it mutates, so mutations stage into * the database journal and publish only on commit(). */ private activeTransaction; constructor(database: string, mainFolder?: string, language?: ErrorLang); createError(name: ErrorCode, variable?: string | number | (string | number)[]): Error; private validateName; /** * Validates column paths used in the `columns` option. * * Each column may be a nested path of dot-separated names * (e.g. "address.street", "hobbies.name") and may optionally * start with "!" for exclusion. The "*" wildcard (select all) * and its "!*" counterpart are special values and are skipped. * Every segment of a path is validated individually. */ private validateColumns; private validateSchema; private getFileExtension; private schemaToIdsPath; /** * Create a new table inside database, with predefined schema and config * * @param {string} tableName * @param {Schema} [schema] * @param {TableConfig} [config] */ createTable(tableName: string, schema?: Schema, config?: TableConfig): Promise; private _createTable; private replaceStringInFile; /** * Update table schema or config * * @param {string} tableName * @param {Schema} [schema] * @param {(TableConfig&{name?: string})} [config] */ updateTable(tableName: string, schema?: Schema, config?: TableConfig & { name?: string; }): Promise; private _updateTable; private updateTableLocked; /** * Get table schema and config * * @param {string} tableName * @return {*} {Promise} */ getTable(tableName: string): Promise; private overlayKey; /** True when the table opted into the incremental update log. */ private isLogTable; /** * Resolve a table's format marker, revalidating a cached *log* table when * its `.format` file changed underneath us. A compaction by another process * bumps `baseGen` and empties the log; without re-reading the marker this * process would scan the new (empty) log with the old `baseGen`, and once a * post-compaction frame appears the generation gap would look like * corruption. Legacy tables never change format implicitly, so they skip * the stat entirely and keep the pre-3.8 read path cost. */ /** * Read a table's `.format` marker and refuse a table written by a newer * engine (§4.1) *before* any column is read: a higher `format`, or a * `minReader` above this build, throws UNSUPPORTED_FORMAT rather than being * silently mis-read as format 2/legacy. */ private readValidatedFormat; private currentFormat; /** * Load (or refresh) a table's live overlay. Returns null for legacy tables * (no `.format` marker) so every existing code path stays untouched. With * `forWrite`, repairs a torn tail and rebuilds to the committed boundary; * the caller must already hold the table writer lock. */ private getOverlay; private stampFormat; /** * The format-2 update log stores one encoded cell per changed *top-level * simple* column; nested (object/array/table) and computed fields are not * representable without a compaction step (3.9). A log table whose schema * violates this is rejected at DDL time so the write path can always take * the log and never needs to interleave a whole-column rewrite. */ private assertLogCompatibleSchema; private idFieldOf; /** Read the internal numeric id for each requested line (stable overlay key). */ private overlayLineIds; /** Internal id -> current line number(s), built from the id column. */ private overlayIdToLines; /** * Per-line raw-cell overrides for one column, from the overlay. Used by * `File.search` so criteria, uniqueness and aggregate scans see the merged * (un-compacted) values. Returns undefined when the table has no overlay or * the column is never overlaid. */ private overlayColumnOverrides; /** * Fold every logged update into the base columns and reset the log. * * Safe to call on any table: legacy tables (no `.format` marker) are a * no-op. Semantically neutral — it only materializes already-acknowledged * updates, so a failure never makes an acknowledged `put` disappear. * Returns the new `baseGen` for a compacted table, or `null` when there was * nothing to fold. */ compact(tableName: string): Promise; /** * Compaction core. The caller must hold the table writer lock. Streams each * overlaid base column through `File.replace` (which handles compression), * then publishes the new columns, an empty `updates.log`, and a `.format` * marker with the bumped generation in one journaled commit, so a crash * either rolls every file back (old base + old log + old `baseGen`) or * forward (new base + empty log + new `baseGen`). No acknowledged update is * ever lost. */ private compactLogTable; getTableSchema(tableName: string): Promise; private throwErrorIfTableEmpty; validateData(data: Data | Data[], schema: Schema, skipRequiredField?: boolean): void; private validateTableData; private cleanObject; private formatField; private checkUnique; private formatData; private getDefaultValue; private _combineObjectsToArray; private _CombineData; private joinPathesContents; private _processSchemaDataHelper; private processSchemaData; /** * Fold the table's un-compacted update-log overlay onto decoded rows. * * `RETURN` is keyed by line number and its `id` is the user-facing id, so * the overlay (keyed by the internal numeric id) is matched through a fresh * id-column read of just the requested lines. Only columns present in * `schema` (which is already narrowed to the requested `columns`) are * written, so a partial projection never grows extra keys. Legacy tables * (no `.format` marker) return immediately. */ private mergeOverlayIntoRows; /** * Extract the raw expression from a field's `computed` property (string or * persisted `{ expr, ast }` spec). */ private computedExprOf; /** * Every computed field of a schema as dotted keys (computed children of an * array-of-objects column use their full path, e.g. `items.lineTotal`), * with its raw expression — used for updateTable change detection. */ private computedEntries; /** * Compile every `computed` expression of a schema (which must already have * ids assigned) into its persisted `{ expr, ast }` form, in dependency * order. Throws `COMPUTED_FIELD_CYCLE` on cyclic fields. Used by DDL so the * on-disk schema always carries compiled ASTs. */ private compileComputedFields; /** * Build the evaluation plan (topological order + id index) for a table's * computed fields. Throws on cycles or unresolvable expressions. */ private buildComputedPlan; /** Id index of a table's schema (link targets are re-resolved at write * time, so renames never retarget a compiled expression). */ private tableFieldIndex; /** * Evaluate a batch of (merged) rows against the table's computed fields. * Returns `lineNo -> { computedKey -> value }` in dependency order. * * Batched link-hop reads: when the plan contains any link hop, a dry * collection pass records every (table, column, id) triple the rows' * expressions need (no file I/O), each distinct triple is then resolved * exactly once — deduplicated across rows and fields — and a final pass * evaluates against the warm cache. Plans without hops run the single * evaluation pass unchanged. */ private evaluateComputedRows; private createLinkReader; /** Evaluate every computed field of one row (topological order) and merge * the results back into the row so dependent fields see them. */ private evaluateRowComputed; private evaluateNode; /** Evaluate a path (`ids`, dot-separated link hops) against the current * frame. Returns `null` when an intermediate link value is missing. */ private evaluatePath; /** Id index lookup with a shared per-evaluation cache. */ private indexFor; /** Read a single column of one linked row via `get`, or null when the * row does not exist (dangling link). */ private readLinkedRow; private coerceNumber; private applyBinaryOp; private aggregate; /** Merge evaluated per-line computed values into a `pathesContents` map * as line-numbered replace records (encoded cells). */ private mergeComputedLineRecords; /** Compare a freshly evaluated cell value against a stored (already * decoded) one — both are scalar or index-aligned element arrays. */ private sameCellValue; private isSimpleField; private processSimpleField; private isArrayField; private processArrayField; private isObjectField; private processObjectField; private isTableField; private processTableField; private _setNestedKey; private applyCriteria; private _filterSchemaByColumns; /** * Clear table cache * * @param {string} tableName */ clearCache(tableName: string): Promise; /** * Commit a multi-file mutation crash-atomically: * 1. fsync every freshly-written temp file; * 2. write the journal `begin` entry and fsync it; * 3. rename the pagination metadata file first (atomic publication point: * the row count flips in a single rename, which is what lock-free * readers observe) and then swap each live file aside (backup) and * rename the temp into place; * 4. write the journal `commit` marker and fsync it; * 5. discard backups/temps + the journal, fsync the directories. * * On any failure before `commit`, the journal is rolled back so the table * is left exactly as it was. `renameList` entries are [tempPath, livePath] * pairs; a null tempPath means a pure removal (live file taken out). */ private commitFiles; /** * Runs crash recovery for a table (and any crashed database transaction) * before a read. Mutation paths get the same guarantee implicitly (the * writer lock runs recovery on acquire); reads call this explicitly * because they never take the table lock. */ private ensureTableRecovered; /** * Blocking database-journal recovery, used by mutation paths (writers are * serialized with live transactions on the database lock anyway). */ private ensureDatabaseRecovered; private ensureDatabaseTmpDir; /** Staged per-table entry of the open transaction, or null when none. */ private txnTableEntry; /** Lock a table for the open transaction (idempotent per transaction). */ private ensureTxnLock; /** * Resolve the pagination state a DML op should build on. Outside a * transaction this reads the live pagination file (as before); inside a * transaction the first touch reads it once and the entry keeps the staged * id/count so chained ops (guarded to one per table) and commit() stay * consistent without publishing anything early. */ private resolvePagination; /** * Stage one table mutation into the open transaction: fsync its temps and * append an `op` entry to the database journal (no live file is touched; * commit() performs the actual renames). One staged mutation per table per * transaction (multi-table atomicity; a second touch of the same table * would need read-your-writes composition). */ private stageTxnOp; /** * Begin a database transaction. Mutations issued while the transaction is * open (post/put/delete, including cascade deletes) are staged into the * database journal and published atomically at commit(); rollback() * discards them without touching any live file. * * @param tables Optional table names to pre-lock at begin() in sorted * order (the deadlock-free way to span tables). Tables not listed are * locked on first touch, in first-touch order. */ begin(tables?: string[]): Promise; private _begin; /** * Publish every staged mutation atomically: per table (sorted), the * pagination rename comes first (the atomic publication point readers * observe) and then live->backup + tmp->live swaps, before a single fsynced * `commit` marker makes the whole transaction durable. A crash at any * point is recovered by the journal rule (no marker -> roll back all * tables, marker -> roll forward all tables). */ commit(): Promise; /** * Discard the open transaction: temps and the journal are removed and no * live file is touched (nothing is published before commit()). */ rollback(): Promise; /** * Run `callback` inside a transaction, committing when it resolves and * rolling back when it throws. Prefer this over a bare begin/commit pair: * the database lock taken by begin() is held for the transaction's whole * lifetime, so an exception between begin() and commit() would otherwise * block every later write on this database for the full lock-wait timeout. * * @param tablesOrCallback Pre-lock list (sorted, deadlock-free) or the callback * @param maybeCallback The callback, when tables were passed first * @param tables Pre-lock list when the callback was passed first * @return {*} Whatever the callback resolves to */ transaction(tablesOrCallback: string[] | ((db: Inibase) => Promise), maybeCallback?: (db: Inibase) => Promise, tables?: string[]): Promise; private _transaction; /** * Snapshot the identity (dev:inode:mtime:size) of every column file and * the pagination file. Reading data and then re-verifying this snapshot * lets lock-free readers detect an in-flight writer commit and retry * instead of returning a torn row set. */ private snapshotTableFiles; /** True when every snapshotted file is still present and unchanged. */ private verifyTableFiles; /** * Retrieve item(s) from a table * * @param {string} tableName * @param {(string | number | (string | number)[] | Criteria)} [where] * @param {Options} [options] * @param {boolean} [onlyOne] * @param {boolean} [onlyLinesNumbers] * @return {*} {(Promise)} */ get & Partial>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>; get & Partial>(tableName: string, where: string | number, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>; get & Partial>(tableName: string, where?: string | number | (string | number)[] | Criteria, options?: Options, onlyOne?: boolean, onlyLinesNumbers?: false, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>; get<_TData extends Record & Partial>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: false | undefined, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise; get<_TData extends Record & Partial>(tableName: string, where: string | number | (string | number)[] | Criteria | undefined, options: Options | undefined, onlyOne: true, onlyLinesNumbers: true, _whereIsLinesNumbers?: boolean): Promise; private _get; private getOnce; /** * In-process sort fallback for log tables with a non-empty overlay: the * shell `paste | sort | awk` path reads base cells and would miss puts. * Reads the merged rows for the (optionally filtered) lines, sorts them by * the requested keys with type-aware comparison, then paginates. */ private getOnceSortedWithOverlay; /** * Create new item(s) in a table * * @param {string} tableName * @param {((Data & TData) | (Data & TData)[])} data Can be array of objects or a single object * @param {Options} [options] Pagination options, useful when the returnPostedData param is true * @param {boolean} [returnPostedData] By default function returns void, if you want to get the posted data, set this param to true * @return {*} {Promise} * * @remarks Without `returnPostedData` the return value is the new id: a * `number` on a table created with `decodeID: true`, an encrypted * `string` on every other table. */ post & Partial>(tableName: string, data: Data & TData, options?: Options, returnPostedData?: boolean): Promise; post & Partial>(tableName: string, data: (Data & TData)[], options?: Options, returnPostedData?: boolean): Promise<(string | number)[]>; post & Partial>(tableName: string, data: Data & TData, options: Options | undefined, returnPostedData: true): Promise<(Data & TData) | null>; post & Partial>(tableName: string, data: (Data & TData)[], options: Options | undefined, returnPostedData: true): Promise<(Data & TData)[] | null>; private _post; /** * Update item(s) in a table * * @param {string} tableName * @param {(Data & TData) | (Data & TData[])} data * @param {(number | string | (number | string)[] | Criteria)} [where] * @param {Options} [options] * @param {boolean} [returnUpdatedData] * @return {*} {Promise} */ put & Partial>(tableName: string, data?: (Data & TData) | (Data & TData)[], where?: number | string | (number | string)[] | Criteria | undefined, options?: Options | undefined, returnUpdatedData?: false, _whereIsLinesNumbers?: boolean): Promise; put & Partial>(tableName: string, data: Data & TData, where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | null>; put & Partial>(tableName: string, data: (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData)[] | null>; put & Partial>(tableName: string, data: (Data & TData) | (Data & TData)[], where: number | string | (number | string)[] | Criteria | undefined, options: Options | undefined, returnUpdatedData: true | boolean, _whereIsLinesNumbers?: boolean): Promise<(Data & TData) | (Data & TData)[] | null>; /** * Incremental `put` for format-2 (log) tables. * * Resolves the target rows to their stable internal ids, validates the * payload against the merged view (uniqueness included), and appends one * bounded frame instead of rewriting whole columns. The base files are left * untouched; readers merge the overlay. Returns `undefined` when nothing * matches, matching the legacy `put` contract. */ private putViaLog; private _put; /** * Delete item(s) in a table * * @param {string} tableName * @param {(number | string | (number | string)[] | Criteria)} [where] * @return {boolean | null} {(Promise)} */ delete(tableName: string, where?: number | string | (number | string)[] | Criteria, _whereIsLinesNumbers?: boolean, _cascadeGuard?: Set): Promise; private _delete; /** * Cascade delete: remove rows in other tables whose `table`-typed schema * fields reference the given rows. Reference columns store the numeric * (line-number) id of the referenced row, so deleted ids are matched * against them directly. Recursion into child tables happens through * `delete` itself (which calls this method again); the `guard` set keeps * deep/cyclic reference chains from re-processing the same (table, line). */ private cascadeDelete; /** * Generate sum of column(s) in a table * * @param {string} tableName * @param {string} columns * @param {(number | string | (number | string)[] | Criteria)} [where] * @return {*} {Promise>} */ sum(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria, options?: AggregateOptions): Promise; sum(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria, options?: AggregateOptions): Promise>; /** * Element-wise aggregation over an array-of-objects column (`nested` mode). * * `where` keys that resolve as children of the array root become per-element * predicates (AND); everything else is a row-level filter. Only elements * passing every element predicate, inside rows passing the row filters, are * accumulated. */ private sumNested; /** * Generate average of column(s) in a table * * @param {string} tableName * @param {string} columns * @param {(number | string | (number | string)[] | Criteria)} [where] * @return {*} {Promise>} */ avg(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise; avg(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria): Promise>; /** * Generate max of column(s) in a table * * @param {string} tableName * @param {string} columns * @param {(number | string | (number | string)[] | Criteria)} [where] * @return {*} {Promise} */ max(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise; max(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria): Promise>; /** * Generate min of column(s) in a table * * @param {string} tableName * @param {string} columns * @param {(number | string | (number | string)[] | Criteria)} [where] * @return {*} {Promise} */ min(tableName: string, columns: string, where?: number | string | (number | string)[] | Criteria): Promise; min(tableName: string, columns: string[], where?: number | string | (number | string)[] | Criteria): Promise>; } export {};