import { RebaseApiError } from "../errors"; import type { EntityStatus, EntityValues } from "../types/entities"; import type { CollectionConfig, FilterValues } from "../types/collections"; import type { OrderByTuple } from "../types/filter-operators"; import type { RebaseCallContext } from "../call_context"; import type { LogicalCondition } from "./data"; /** * @internal */ export interface FetchOneProps = Record> { path: string; id: string | number; databaseId?: string; collection?: CollectionConfig; } /** * @internal */ export type ListenOneProps = Record> = FetchOneProps & { onUpdate: (row: Record | null) => void; onError?: (error: Error) => void; }; /** * Configuration for vector similarity search queries. * Vector search applies an ORDER BY distance expression and optionally * filters results by a distance threshold. */ export interface VectorSearchParams { /** Property name containing the vector column */ property: string; /** Query vector to compare against */ vector: number[]; /** Distance function (default: "cosine") */ distance?: "cosine" | "l2" | "inner_product"; /** Only return results within this distance threshold */ threshold?: number; } /** Rows returned for a plain / text-search list read when the client sends no `limit`. */ export declare const DEFAULT_LIST_LIMIT = 50; /** Rows returned for a vector-search list read when the client sends no `limit`. */ export declare const DEFAULT_VECTOR_LIST_LIMIT = 10; /** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */ export declare const MAX_LIST_LIMIT = 1000; /** Overridable bounds for {@link resolveClientListLimit}. */ export interface ListLimitBounds { /** Default page size for plain and text-search reads. */ defaultLimit?: number; /** Default page size for vector-search reads. */ vectorDefaultLimit?: number; /** Largest limit a client may ask for. A larger one is rejected, not clamped. */ maxLimit?: number; } /** * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling. * * @group Errors */ export declare class ListLimitError extends RebaseApiError { /** The ceiling that was exceeded — what the caller should page by instead. */ readonly maxLimit: number; constructor(message: string, maxLimit: number); } /** * Resolve a client-supplied list `limit` into a safe, always-defined value. * * - An absent / blank limit falls back to the mode default: * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`. * - A limit that is present must be an integer in `[1, maxLimit]`. Anything * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws * {@link ListLimitError} rather than being coerced into range, because every * coercion answers a question the caller did not ask with a page it cannot * tell apart from the whole collection. * * The return is never `undefined` — no ingress that routes its client limit * through this can produce an unbounded read. * * @throws {ListLimitError} when a present `limit` is not an integer in range. */ export declare function resolveClientListLimit(rawLimit: number | string | null | undefined, opts?: ListLimitBounds & { vectorSearch?: boolean; }): number; /** * @internal */ export interface FetchCollectionProps = Record> { path: string; collection?: CollectionConfig; filter?: FilterValues>; /** * An `or(...)`/`and(...)` group, applied alongside `filter`. * * The REST layer parsed `?or=` into this and then had nowhere to put it, so * the group was dropped and the read ran unfiltered — returning every row * the caller's policies allowed rather than the ones they asked for. */ logical?: LogicalCondition; limit?: number; offset?: number; startAfter?: unknown; /** * The sort, in either of two spellings: * * - a field name, whose direction is the separate `order` below — the * original single-column contract, which every existing driver reads; * - a list of `[field, direction]` tuples applied in order of significance, * which carries a multi-column sort and ignores `order` entirely. * * `normalizeDriverOrderBy` in `@rebasepro/common` collapses the pair to the * list form. A driver that has not been taught the list form should read it * through that helper rather than assume a string: handed an array, `String()` * would produce a field name like `roles,asc` and the sort would 400 (or, * with unknown-field warnings on, silently vanish). */ orderBy?: string | OrderByTuple[]; searchString?: string; /** Ask each row which declared search field matched — populates `_matches`. */ searchExplain?: boolean; /** Direction for the string form of `orderBy`. Ignored when `orderBy` is a list. */ order?: "desc" | "asc"; /** Vector similarity search configuration */ vectorSearch?: VectorSearchParams; } /** * @internal */ export type ListenCollectionProps = Record> = FetchCollectionProps & { onUpdate: (rows: Record[]) => void; onError?: (error: Error) => void; }; /** * @internal */ export interface SaveProps = Record> { path: string; values: Partial>; id?: string | number; previousValues?: Partial>; collection?: CollectionConfig; status: EntityStatus; /** * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key * instead of choosing between insert and update up front. * * One statement, so it does not lose the race a read-then-write can, and it * succeeds whether or not the row is already there — what a re-runnable * import needs. Requires every primary key column to be present; without * them there is no conflict target and the row is inserted normally. */ upsert?: boolean; } /** * @internal */ export interface SaveManyProps = Record> { path: string; /** * The rows to write. A row carrying its primary key updates (or, with * `upsert`, inserts-or-updates) that row; one without inserts. */ rows: Partial>[]; collection?: CollectionConfig; /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */ upsert?: boolean; } /** * @internal */ export interface UpdateManyProps = Record> { path: string; /** * The rows to update, each named by its address. * * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the * values and is insert-shaped — `saveMany` passes `status: "new"` and no * `id`, so it cannot express "update exactly this row". This can, and it is * why bulk update is a separate driver method rather than a flag on that one. */ updates: { id: string | number; values: Partial>; }[]; collection?: CollectionConfig; } /** * @internal */ export interface DeleteProps = Record> { row: { id: string | number; path: string; values?: Partial>; }; collection?: CollectionConfig; } /** * @internal */ export interface DeleteManyProps = Record> { path: string; ids: (string | number)[]; collection?: CollectionConfig; } export type FilterCombinationValidProps = { path: string; databaseId?: string; collection: CollectionConfig; filterValues: FilterValues; sortBy?: [string, "asc" | "desc"]; }; /** * The integration SPI for plugging a data backend into Rebase. * * Implement this interface to connect a custom backend (or use a built-in * driver such as the Firestore one) and register it on * ``. Rebase wraps drivers via `buildRebaseData` and * routes collections to them by their `dataSource` key. * * For *consuming* data in application code, use `RebaseData` / * `context.data` instead — this interface is only for providing it. * * @group Datasource */ export interface DataDriver { /** * Key that identifies this driver */ key?: string; /** * If the driver has been initialised */ initialised?: boolean; /** * Fetch data from a collection * @param props * @return Promise of flat rows */ fetchCollection = Record>(props: FetchCollectionProps): Promise[]>; /** * Listen to a collection in a given path. If you don't implement this method * `fetchCollection` will be used instead, with no real time updates. * @param props * @return Function to cancel subscription */ listenCollection? = Record>(props: ListenCollectionProps): () => void; /** * Retrieve a single row given a path and a collection * @param props */ fetchOne = Record>(props: FetchOneProps): Promise | undefined>; /** * Get realtime updates on one row. * @param props * @return Function to cancel subscription */ listenOne? = Record>(props: ListenOneProps): () => void; /** * Save a row to the specified path * @param props */ save = Record>(props: SaveProps): Promise>; /** * Save many rows as one unit of work. * * Every row runs the same pipeline as {@link save} — callbacks, relations * and row-level security all still apply — but they share a single * transaction, so the batch either lands whole or not at all. That, and the * single round trip, is what makes importing tens of thousands of rows * viable without dropping to raw SQL. * * Optional: drivers that cannot do this leave it undefined and callers fall * back to `save` per row. */ saveMany? = Record>(props: SaveManyProps): Promise[]>; /** * Update many rows in one transaction, each addressed by id. * * Optional for the same reason `saveMany` is: a driver that cannot make the * batch atomic should not pretend to. The REST layer reports * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single * writes, which would be neither atomic nor one round trip — the two things * a caller reaches for a batch to get. */ updateMany? = Record>(props: UpdateManyProps): Promise[]>; /** * Delete the row `props.row` addresses. * * **Resolving means the row is gone because this call removed it.** A * delete that matched nothing must reject with a not-found error * (`ApiError.notFound`, `statusCode: 404`) rather than resolving quietly. * * The rule is here rather than in each driver because the two * implementations answered differently and each had a test pinning its own * habit: Postgres threw, Mongo logged a warning and resolved. Three things * decide it in favour of rejecting. * * The REST layer already says 404 — `DELETE /api/data//` reads the * row before removing it — so a quiet resolve made the driver API disagree * with the HTTP API about the same operation, and only in-process * `rebase.data` callers could see the difference. * * A caller cannot tell "deleted" from "there was nothing there" without it, * and those are different facts: one means the caller's model of the data * was right, the other that it was stale. Silence hands back the wrong one * and the caller carries on. * * And on a driver with row-level security, "matched nothing" is *also* how * a policy refusal arrives — Postgres filters `DELETE` through `USING` * rather than raising. A driver that resolves on zero rows therefore * reports a refused delete as a completed one, which is the defect * `explainZeroRowWrite` exists to prevent (see `write-denial.ts`). * * Conformance for both server drivers lives in * `packages/server/test/contract/delete-contract.ts`, run by each driver's * own suite against its own database. `packages/firebase`'s Firestore * driver does not honour it: `deleteDoc` resolves for a missing document * and reporting otherwise would cost a read on every delete. It runs in the * browser against Firestore's own semantics rather than behind * `rebase.data`, and that exception is stated here rather than left to be * discovered. */ delete = Record>(props: DeleteProps): Promise; /** * Delete all entities from a collection. * @param path Collection path */ deleteAll?(path: string): Promise; /** * Delete many rows in one transaction, addressed by id. * * Ids rather than a filter, deliberately — see * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is. */ deleteMany? = Record>(props: DeleteManyProps): Promise; /** * Check if the given property is unique in the given collection * @param path Collection path * @param name of the property * @param value * @param id * @param collection * @return `true` if there are no other fields besides the given entity */ checkUniqueField(path: string, name: string, value: unknown, id?: string | number, collection?: CollectionConfig): Promise; /** * Count the number of entities in a collection */ count? = Record>(props: FetchCollectionProps): Promise; /** * Check if the given filter combination is valid * @param props */ isFilterCombinationValid?(props: Omit & { databaseId?: string; }): boolean; /** * Get the object to generate the current time in the driver */ currentTime?: () => unknown; delegateToCMSModel?: (data: unknown) => unknown; cmsToDelegateModel?: (data: unknown) => unknown; initTextSearch?: (props: { context: RebaseCallContext; path: string; databaseId?: string; collection: CollectionConfig; parentCollectionSlugs?: string[]; parentEntityIds?: string[]; }) => Promise; /** * Flag to indicate if the driver has requested the initialization of the text search index */ needsInitTextSearch?: boolean; /** * Optional REST-optimised fetch service. When present, the REST API * generator uses these methods instead of the generic `fetchOne` / * `fetchCollection` pipeline, enabling include-aware eager-loading. */ restFetchService?: RestFetchService; /** * Return the admin capabilities of this driver. * @see SQLAdmin * @see DocumentAdmin * @see SchemaAdmin */ admin?: import("../types/backend").DatabaseAdmin; } /** * REST-optimised fetch service exposed by drivers that support * eager-loading of relations via `include`. * * The methods return flattened rows — exactly the table's columns, under their * own names and with the types the database returned — and included relations * inlined as plain nested rows. This is the shape served to app developers * through the REST API / SDK client. * * No synthesized `id`: identity is a primary key, which may be named anything * and span several columns, so an address is derived by whoever needs one (see * `buildCompositeId`) rather than written into the row on top of the data. * * @group DataDriver */ export interface RestFetchService { /** * Fetch a collection of flattened entities with optional relation includes. */ fetchCollectionForRest(collectionPath: string, options?: { filter?: FilterValues; /** An `or(...)`/`and(...)` group, applied alongside `filter`. */ logical?: LogicalCondition; /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */ orderBy?: string | OrderByTuple[]; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record; searchString?: string; /** Ask each row which declared search fields matched — populates `_matches`. */ searchExplain?: boolean; databaseId?: string; vectorSearch?: VectorSearchParams; }, include?: string[]): Promise[]>; /** * `count`/`sum`/`avg`/`min`/`max` over the rows a filter selects, * optionally grouped. * * Optional, and the REST route answers 501 where a driver does not * implement it — an aggregate is not a thing to approximate, and an empty * result set would read as "nothing matched". * * Any implementation **must apply the same row-level authorization as a * read**. An aggregate is an efficient way to learn about rows you cannot * select, and `count(*)` over a table whose policies would return nothing * has to be zero. */ aggregate?(collectionPath: string, options: { aggregates: { fn: "count" | "sum" | "avg" | "min" | "max"; field?: string; alias: string; }[]; groupBy?: string[]; filter?: FilterValues; logical?: LogicalCondition; searchString?: string; limit?: number; }): Promise[]>; /** * Fetch a single flattened entity with optional relation includes. */ fetchOneForRest(collectionPath: string, id: string | number, include?: string[], databaseId?: string): Promise | null>; }