/** * @license * Copyright 2025 Steven Roussey * SPDX-License-Identifier: Apache-2.0 */ import { EventEmitter } from "@workglow/util"; import { DataPortSchemaObject, FromSchema, TypedArraySchemaOptions } from "@workglow/util/schema"; import { ITabularMigration, ITabularMigrationApplier, RunTabularMigrationsOptions } from "../migrations"; import { CursorPayload, PageCursor } from "./Cursor"; import { AnyTabularStorage, AutoGeneratedKeys, CoveringIndexQueryOptions, DeleteSearchCriteria, InsertEntity, ITabularStorage, OrderBy, Page, PageRequest, QueryOptions, SearchCriteria, SimplifyPrimaryKey, TabularChangePayload, TabularEventListener, TabularEventListeners, TabularEventName, TabularEventParameters, TabularSubscribeOptions, ValueOptionType } from "./ITabularStorage"; export declare const TABULAR_REPOSITORY: import("@workglow/util").ServiceToken; /** Controls how client-provided values for auto-generated keys are handled. */ export type ClientProvidedKeysOption = "never" | "if-missing" | "always"; export type KeyGenerationStrategy = "autoincrement" | "uuid"; /** * Abstract base class for tabular storage repositories. Splits a schema into * primary-key and value halves, manages indexes and `x-auto-generated` PK * columns, and emits {@link TabularEventListeners} events for all reads and * mutations. */ export declare abstract class BaseTabularStorage, Entity = FromSchema, PrimaryKey = SimplifyPrimaryKey, Value = Omit, InsertType = InsertEntity>> implements ITabularStorage { protected schema: Schema; protected primaryKeyNames: PrimaryKeyNames; protected events: EventEmitter>; protected indexes: Array[]; /** * Compound column tuples that must each be unique across all rows in the * table. Concrete backends translate these into DB-level UNIQUE indexes * (SQLite / Postgres `CREATE UNIQUE INDEX IF NOT EXISTS`) so the * deduplication invariant survives outside the application-layer code that * upserts canonical rows. Stored separately from {@link indexes} so the * prefix-redundancy filter applied to plain indexes does not collapse a * unique tuple that happens to share a prefix. */ protected uniqueIndexes: Array[]; protected primaryKeySchema: DataPortSchemaObject; protected valueSchema: DataPortSchemaObject; /** * Optional declarative migrations evolving older deployments to the * current target schema. When set + non-empty, `setupDatabase()` on * concrete subclasses delegates to {@link applyTabularMigrations}. */ protected readonly tabularMigrations: ReadonlyArray | undefined; /** * Default component name used when an `ITabularMigration.component` is * omitted. Defaults to `tabular:${storageName}` once the subclass has * supplied a name. */ protected migrationComponent: string; /** At most one primary-key column may be marked `x-auto-generated: true`. */ protected autoGeneratedKeyName: keyof Entity | null; protected autoGeneratedKeyStrategy: KeyGenerationStrategy | null; protected clientProvidedKeys: ClientProvidedKeysOption; constructor(schema: Schema, primaryKeyNames: PrimaryKeyNames, indexes?: readonly (keyof NoInfer | readonly (keyof NoInfer)[])[], clientProvidedKeys?: ClientProvidedKeysOption, tabularMigrations?: ReadonlyArray, migrationName?: string, uniqueIndexes?: readonly (readonly (keyof NoInfer)[])[]); protected filterCompoundKeys(primaryKey: Array, potentialKeys: Array[]): Array[]; on(name: Event, fn: TabularEventListener): void; off(name: Event, fn: TabularEventListener): void; once(name: Event, fn: TabularEventListener): void; emit(name: Event, ...args: TabularEventParameters): void; waitOn(name: Event): Promise>; abstract put(value: InsertType): Promise; abstract putBulk(values: InsertType[]): Promise; abstract get(key: PrimaryKey): Promise; abstract delete(key: PrimaryKey | Entity): Promise; abstract getAll(options?: QueryOptions): Promise; abstract deleteAll(): Promise; abstract size(): Promise; /** * Default plural-get implementation: parallel single-key fetches via * {@link get}, with `undefined` results filtered out. Backends with a * cheaper batched path (SQL `WHERE pk IN (...)`) override this. */ getBulk(keys: readonly PrimaryKey[]): Promise; /** Concrete storage implementations can override this with native count APIs. */ count(criteria?: SearchCriteria): Promise; abstract deleteSearch(criteria: DeleteSearchCriteria): Promise; abstract query(criteria: SearchCriteria, options?: QueryOptions): Promise; /** * Strict, projected query served entirely by a covering compound index. * Subclasses that support index-only reads (e.g. IndexedDB, InMemory) should * override this with an efficient implementation. The default throws * {@link CoveringIndexMissingError} so callers get a consistent error when * the storage backend has not implemented index-only projection. */ queryIndex(criteria: SearchCriteria, options: CoveringIndexQueryOptions): Promise[]>; abstract getOffsetPage(offset: number, limit: number): Promise; /** Cursor-based iteration, stable under concurrent writes. */ records(pageSize?: number): AsyncGenerator; /** * Cursor-based iteration, stable under concurrent writes. Each yielded * array is a fresh copy of `Page.items` so callers can mutate it safely. */ pages(pageSize?: number): AsyncGenerator; /** * Default keyset-pagination implementation. Backends that can express * tuple comparison natively (SQL row-value comparisons) should override * this for efficiency, but the default works against any implementation * of {@link query} / {@link getAll}. */ getPage(request?: PageRequest): Promise>; /** Default cursor-paginated form of {@link query}. */ queryPage(criteria: SearchCriteria, request?: PageRequest): Promise>; /** * Shared keyset-pagination engine for both {@link getPage} and * {@link queryPage}. Translates the cursor into AND criteria using the * primary key column(s) and runs the existing {@link query}/{@link getAll} * machinery, then re-encodes the position of the last returned row. * * For composite primary keys this performs the keyset comparison in * memory after fetching a candidate window. SQL backends should override * {@link getPage}/{@link queryPage} to push it down to the database. */ protected runPage(criteria: SearchCriteria | undefined, request: PageRequest): Promise>; /** * Returns the order spec actually used to drive a keyset page: the * caller's `orderBy` (if any) followed by every primary-key column as a * stable tiebreaker. PK columns are appended in ASC direction unless the * caller already specified a direction for them. */ protected buildEffectiveOrderBy(orderBy: ReadonlyArray> | undefined, pkColumns: ReadonlyArray): ReadonlyArray>; /** * Sorts rows in place by the given order spec, using the same null-first * comparison rules as the cursor filter so results are consistent. */ protected sortInMemory(rows: Entity[], orderBy: ReadonlyArray>): Entity[]; /** * In-memory keyset filter: drops rows that come at or before the cursor * row according to `orderBy + primaryKey`. Used by the default * implementation when the backend can't express tuple comparison. */ protected applyKeysetFilter(rows: Entity[], cursor: CursorPayload, effectiveOrderBy: ReadonlyArray>, _pkColumns: ReadonlyArray): Entity[]; /** * Encodes the position of `row` as an opaque cursor. The caller passes * the effective ordering it just used — caller-supplied `orderBy` * followed by any primary-key columns not already covered by `orderBy` * — and this writes one value per effective column, in the same order. * Computing that ordering is the caller's responsibility because they * already have it in hand and we don't want to recompute it here. */ protected buildCursor(row: Entity, effectiveOrderBy: ReadonlyArray>): PageCursor; /** Default throws — override in subclasses that support subscriptions. */ subscribeToChanges(_callback: (change: TabularChangePayload) => void, _options?: TabularSubscribeOptions): () => void; /** * Validates query parameters: criteria columns, options, and operator values. * @throws StorageEmptyCriteriaError if criteria is empty * @throws StorageInvalidLimitError if limit <= 0 * @throws StorageValidationError if offset < 0 * @throws StorageInvalidColumnError if any column name is not in the schema */ protected validateQueryParams(criteria: SearchCriteria, options?: QueryOptions): void; protected validateGetAllOptions(options?: QueryOptions): void; /** * Validates the `orderBy` clause of a {@link PageRequest}: column names * must exist in the schema and directions must be `"ASC"` or `"DESC"`. * * Page paths (`getPage`/`queryPage`) must call this before any backend * builds SQL, because column names from `request.orderBy` end up * interpolated directly into `ORDER BY` and the keyset `WHERE` clauses. * Without this guard a caller passing arbitrary strings would either * trigger a backend SQL error or, worse, allow SQL injection on backends * that emit unquoted identifiers. */ protected validateOrderBy(orderBy: ReadonlyArray> | undefined): void; /** * Validates the components of a cursor-paginated request that are about * to be used to build a query. Centralises the schema-level guarantees * that `runPage`/`runSqlPage`/Supabase's `runSupabasePage` all rely on * before they interpolate column names into SQL or PostgREST filters. */ protected validatePageRequest(request: PageRequest): void; /** * Validates the `select` array in a {@link CoveringIndexQueryOptions}. * Throws {@link StorageValidationError} when `select` is empty or contains * a column that is not in the schema's `properties`. */ protected validateSelect(options: CoveringIndexQueryOptions): void; protected primaryKeyColumns(): Array; protected valueColumns(): Array; /** * The identity emitted on the `delete` event for a bulk `deleteSearch`: the * plain (operator-free) columns of the criteria — which include the owner / * scope columns — as a `Partial`. Search conditions are dropped since * they don't identify a concrete value. */ protected deleteIdentity(criteria: DeleteSearchCriteria): Partial; protected separateKeyValueFromCombined(obj: Entity): { value: Value; key: PrimaryKey; }; protected getKeyAsIdString(key: PrimaryKey): Promise; /** Returns key values ordered to match the schema's primary-key declaration. */ protected getPrimaryKeyAsOrderedArray(key: PrimaryKey): ValueOptionType[]; findBestMatchingIndex(unorderedSearchKey: Array): Array | undefined; protected hasAutoGeneratedKey(): boolean; protected isAutoGeneratedKey(name: string): boolean; protected determineGenerationStrategy(columnName: string, typeDef: any): KeyGenerationStrategy; /** * Generates a key value for client-side key generation. Override in storage * classes that generate keys client-side (InMemory, IndexedDB for UUIDs). * SQL-based storages typically generate keys server-side. */ protected generateKeyValue(columnName: string, strategy: KeyGenerationStrategy): Promise | string | number; /** * Runs `fn` inside a transaction. The default implementation provides no * rollback semantics — it simply invokes `fn(this)`. Concrete subclasses * with native transaction support (SQLite, PostgreSQL) override this. */ withTransaction(fn: (tx: this) => Promise): Promise; /** * Subclass override returning a backend-specific applier. Returning `null` * disables tabular migrations for the storage. The base class returns * `null`; only backends that have wired up their applier override this. */ getMigrationApplier(): ITabularMigrationApplier | null; /** * Runs `this.tabularMigrations` through the orchestrator using the * backend-supplied applier. Idempotent (no-op when no migrations are * declared, or when all are already applied). */ protected applyTabularMigrations(options?: RunTabularMigrationsOptions): Promise; /** * Must be called before using any other methods (except for in-memory * implementations). Default is a no-op; override in subclasses that need setup. */ setupDatabase(): Promise; destroy(): void; [Symbol.asyncDispose](): Promise; [Symbol.dispose](): void; } //# sourceMappingURL=BaseTabularStorage.d.ts.map