@rebasepro/types 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/call_context.d.ts +23 -2
- package/dist/controllers/data_driver.d.ts +33 -0
- package/dist/errors.d.ts +0 -15
- package/dist/index.es.js +25 -22
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +9 -4
- package/dist/types/project_manifest.d.ts +64 -3
- package/dist/types/websockets.d.ts +120 -0
- package/package.json +1 -1
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","names":[],"sources":["../src/errors.ts","../src/types/entities.ts","../src/types/filter-operators.ts","../src/types/admin_block.ts","../src/types/data_source.ts","../src/types/collections.ts","../src/types/search.ts","../src/types/relations.ts","../src/types/policy.ts","../src/types/rls-functions.ts","../src/types/tenancy.ts","../src/types/backend.ts","../src/types/schema_editing.ts","../src/types/channel_bus.ts","../src/types/resources.ts","../src/types/storage_source.ts","../src/types/resource_kinds.ts","../src/types/component_ref.ts","../src/types/project_manifest.ts","../src/types/collection_contract.ts","../src/types/schema_version.ts","../src/controllers/data.ts","../src/controllers/data_driver.ts","../src/controllers/storage.ts"],"sourcesContent":["/**\n * The error codes every route can produce, as `RebaseApiError.code`.\n *\n * These are the defaults on `ApiError`'s static constructors server-side, so\n * any endpoint can answer with one. They are **not** the complete set: routes\n * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,\n * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.\n *\n * Hence the union is deliberately open rather than closed. It exists to give\n * autocomplete and to catch a typo in the common cases — `code` was a bare\n * `string`, so `e.code === \"NOT_FOUND\"` and `e.code === \"NOTFOUND\"` were\n * equally valid and only one of them worked. Closing it would be a lie that\n * broke the moment a route added a code.\n *\n * @example\n * if (e instanceof RebaseApiError) {\n * switch (e.code) {\n * case \"NOT_FOUND\": return null; // completed\n * case \"FORBIDDEN\": return redirect();\n * default: throw e; // routes' own codes land here\n * }\n * }\n *\n * @group Errors\n */\nexport type RebaseErrorCode =\n | \"BAD_REQUEST\"\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"INTERNAL_ERROR\"\n | \"SERVICE_UNAVAILABLE\"\n | \"NETWORK_ERROR\"\n | \"OFFLINE\"\n | \"DB_PERMISSION_DENIED\"\n | \"SCHEMA_DRIFT\"\n // `string & {}` keeps the union open while preserving completion on the\n // literals above — a bare `| string` would collapse them and offer nothing.\n | (string & {});\n\n/**\n * Structured initializer for {@link RebaseApiError}.\n *\n * @group Errors\n */\nexport interface RebaseErrorInit {\n /**\n * HTTP status code, when the error originated from an HTTP response.\n *\n * Three states, and they mean different things:\n *\n * - a real status — the server answered, and this is what it said;\n * - **`0`** — the request never reached a server: DNS, a refused\n * connection, CORS, an abort. `XMLHttpRequest` has always spelled that\n * `0`, and a fabricated 5xx would be indistinguishable from one the\n * server actually sent. The original failure is on `cause`;\n * - `undefined` — nothing was sent at all: a realtime/WebSocket failure,\n * or a client-side logic error raised before any request.\n */\n status?: number;\n /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */\n code?: RebaseErrorCode;\n /** Structured error payload returned by the server, when present. */\n details?: unknown;\n /**\n * The server's correlation id for the request that failed, when it sent\n * one — the `requestId` in the error envelope, which also comes back on the\n * `X-Request-ID` header.\n *\n * The envelope has carried it for a while; the client dropped it on the\n * floor, so a bug report from an app could never quote the one string that\n * finds the server-side line.\n */\n requestId?: string;\n /**\n * Seconds to wait before retrying, from the response's `Retry-After`\n * header. Present on a 429 and on some 503s.\n *\n * Also dropped. The offline queue's own backoff therefore ignored a server\n * that had said exactly how long to wait — the one number that turns a\n * retry storm into a queue that drains.\n */\n retryAfterSeconds?: number;\n /** The underlying error this one wraps, if any. */\n cause?: unknown;\n}\n\n/**\n * The single error type thrown across the entire Rebase client surface —\n * HTTP data/control-plane calls, realtime/WebSocket operations, and\n * client-side logic errors (e.g. an unknown collection accessor). A `catch`\n * block only ever needs to check for this one class:\n *\n * ```ts\n * import { RebaseApiError } from \"@rebasepro/client\"; // re-exported\n *\n * try {\n * await client.data.products.update(id, { price: 9 });\n * } catch (e) {\n * if (e instanceof RebaseApiError) {\n * if (e.status === 404) { ... } // HTTP failures carry a status\n * console.error(e.code, e.details);\n * }\n * }\n * ```\n *\n * `status` is present for HTTP failures and `undefined` otherwise, so its\n * presence distinguishes transport-level errors from realtime/logic errors.\n *\n * @group Errors\n */\nexport class RebaseApiError extends Error {\n /** HTTP status code, or `undefined` for non-HTTP errors. */\n readonly status?: number;\n /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */\n readonly code?: RebaseErrorCode;\n /** Structured error payload from the server, when present. */\n readonly details?: unknown;\n /** See {@link RebaseErrorInit.requestId}. Quote it in a bug report. */\n readonly requestId?: string;\n /** See {@link RebaseErrorInit.retryAfterSeconds}. */\n readonly retryAfterSeconds?: number;\n\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message);\n this.name = \"RebaseApiError\";\n this.status = init.status;\n this.code = init.code;\n this.details = init.details;\n this.requestId = init.requestId;\n this.retryAfterSeconds = init.retryAfterSeconds;\n if (init.cause !== undefined) {\n // `cause` is standard on Error but not always in the lib target's type.\n (this as { cause?: unknown }).cause = init.cause;\n }\n }\n}\n\n/**\n * Client-side logic error — raised before any request is made (e.g. accessing\n * an unknown collection accessor when a typed dictionary is configured).\n *\n * A subclass of {@link RebaseApiError} (with no `status`), so a single\n * `catch (e) { if (e instanceof RebaseApiError) ... }` handles it too.\n *\n * @group Errors\n */\nexport class RebaseClientError extends RebaseApiError {\n /**\n * `init` is the same one {@link RebaseApiError} takes, and it is what makes\n * `code` reachable at all.\n *\n * The constructor used to accept a message and nothing else, so every\n * client-side failure — an undefined filter value, an unknown accessor,\n * `listen()` on a client built with `realtime: false`, a function name with\n * a `/` in it, `refreshSession()` while signed out — arrived with `code ===\n * undefined`. The documented `switch (e.code)` in this file's own example\n * fell to `default: throw e` for all of them, and the only client-side error\n * that *did* carry a code was `OFFLINE`, because that one path minted a\n * `RebaseApiError` instead.\n */\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message, init);\n this.name = \"RebaseClientError\";\n }\n}\n\n/**\n * Brand for a contract method a particular client cannot serve.\n *\n * `Symbol.for` rather than a fresh symbol: two copies of `@rebasepro/types` in\n * one tree — which happens, see `docs/dependency-duplication-traps.md` — must\n * agree about it, and a module-local symbol would not.\n */\nconst UNSUPPORTED_METHOD = Symbol.for(\"rebase.unsupportedMethod\");\n\n/**\n * Build the stub a client installs for a contract method it cannot serve.\n *\n * `listen`, `listenById` and `count` are part of `SDKCollectionClient`, not\n * optional extras — a caller should be able to write\n * `client.data.posts.count()` without asking first, and a transport that cannot\n * serve it should answer with a sentence naming the configuration that would,\n * rather than with `undefined is not a function` at the call site. Where the\n * transport genuinely cannot (a client built with `realtime: false`, a driver\n * with no `listenCollection`), it installs one of these instead of omitting the\n * method.\n *\n * @param message What to tell the caller, naming the fix.\n * @group Errors\n */\nexport function unsupportedMethod<F>(message: string): F {\n const stub = (): never => {\n // The two reasons a method is a stub — `realtime: false`, and a driver\n // with no `listenCollection` — are one thing to a caller: this client\n // cannot do realtime. One code covers both, and the message says which.\n throw new RebaseClientError(message, { code: \"REALTIME_DISABLED\" });\n };\n (stub as unknown as Record<symbol, boolean>)[UNSUPPORTED_METHOD] = true;\n return stub as unknown as F;\n}\n\n/**\n * Can this method actually do anything?\n *\n * `true` for a stub from {@link unsupportedMethod} **and** for a method that is\n * simply not there — a partial client, a hand-built test double, an\n * implementation written against an older shape of the interface. Both mean the\n * same thing to a caller, so both answer the same way, and an adapter that\n * checks this cannot be caught out by either.\n *\n * Ordinary code does not need it: calling the method and letting it throw is\n * the normal path. Adapters do — the admin panel chooses between subscribing\n * and a one-shot `find()` by asking whether the client can listen, and a UI\n * that subscribes into a throw is worse than one that polls. This is the\n * question `if (accessor.listen)` used to be asking, made explicit now that the\n * method is always there to call.\n *\n * @group Errors\n */\nexport function isUnsupported(method: unknown): boolean {\n if (typeof method !== \"function\") return true;\n return (method as unknown as Record<symbol, boolean>)[UNSUPPORTED_METHOD] === true;\n}\n","import type { SearchMatch } from \"./search\";\n/**\n * New or existing status\n * @group Models\n */\nexport type EntityStatus = \"new\" | \"existing\" | \"copy\";\n\n/**\n * Representation of a entity fetched from the driver\n * @group Models\n */\nexport interface Entity<M extends Record<string, unknown> = Record<string, unknown>> {\n\n /**\n * ID of the entity\n */\n id: string | number;\n\n /**\n * A string representing the path of the referenced document (relative\n * to the root of the database).\n */\n path: string;\n\n /**\n * Current values\n */\n values: EntityValues<M>;\n\n /**\n * Why this entity is in a search result: which declared fields matched, and\n * the text around each hit.\n *\n * Present only on rows returned by a search that asked for it. A sibling of\n * `values` rather than a key inside it, because it describes the *query*,\n * not the record — nothing in the collection declares it, no form edits it,\n * and a record fetched by id never has one.\n */\n searchMatches?: SearchMatch[];\n\n /**\n * Which driver this entity belongs to (e.g., 'postgres', 'firestore').\n * If not specified, the default driver is assumed.\n */\n driver?: string;\n\n /**\n * Which database within the driver (e.g., for Firestore multi-database).\n * If not specified, the default database of the driver is used.\n */\n databaseId?: string;\n}\n\n/**\n * This type represents a record of key value pairs as described in an\n * entity collection.\n * @group Models\n */\nexport type EntityValues<M extends Record<string, unknown>> = M;\n\n/**\n * Props for creating a EntityReference\n */\nexport interface EntityReferenceProps {\n /** ID of the entity */\n id: string;\n /** Path of the collection (relative to the root of the database) */\n path: string;\n /** Which driver (e.g., 'postgres', 'firestore'). Defaults to \"(default)\" */\n driver?: string;\n /** Which database within the driver. Defaults to \"(default)\" */\n databaseId?: string;\n}\n\n/**\n * Class used to create a reference to a entity in a different path.\n *\n * @example\n * // Simple reference (most common case - single driver, single db)\n * new EntityReference({ id: \"123\", path: \"users\" })\n *\n * // Reference to a different driver (e.g., Firestore)\n * new EntityReference({ id: \"123\", path: \"analytics\", driver: \"firestore\" })\n *\n * // Reference to a specific database within a driver\n * new EntityReference({ id: \"123\", path: \"orders\", driver: \"postgres\", databaseId: \"orders_db\" })\n */\nexport class EntityReference {\n\n readonly __type = \"reference\";\n /**\n * ID of the entity\n */\n readonly id: string;\n /**\n * A string representing the path of the referenced document (relative\n * to the root of the database).\n */\n readonly path: string;\n\n /**\n * Which driver (e.g., 'postgres', 'firestore').\n * Defaults to \"(default)\" if not specified.\n */\n readonly driver?: string;\n\n /**\n * Which database within the driver.\n * Defaults to \"(default)\" if not specified.\n */\n readonly databaseId?: string;\n\n /**\n * Create a reference to a entity.\n *\n * @example\n * // Simple reference (most common case)\n * new EntityReference({ id: \"123\", path: \"users\" })\n *\n * // With driver\n * new EntityReference({ id: \"123\", path: \"analytics\", driver: \"firestore\" })\n */\n constructor(props: EntityReferenceProps) {\n this.id = props.id;\n this.path = props.path;\n this.driver = props.driver;\n this.databaseId = props.databaseId;\n }\n\n get pathWithId() {\n return `${this.path}/${this.id}`;\n }\n\n /**\n * Get the full path including driver and database prefixes if specified.\n * For the common case (single driver, single db), this just returns pathWithId.\n */\n get fullPath() {\n const parts: string[] = [];\n\n // Add driver prefix if not default\n if (this.driver && this.driver !== \"(default)\") {\n parts.push(this.driver);\n }\n\n // Add database prefix if specified\n if (this.databaseId && this.databaseId !== \"(default)\") {\n parts.push(this.databaseId);\n }\n\n if (parts.length > 0) {\n return `${parts.join(\":\")}:::${this.path}/${this.id}`;\n }\n return this.pathWithId;\n }\n\n isEntityReference() {\n return true;\n }\n}\n\n/**\n * Class used to create a reference to a entity in a different path\n */\nexport class EntityRelation {\n\n readonly __type = \"relation\";\n /**\n * ID of the entity\n */\n readonly id: string | number;\n /**\n * A string representing the path of the referenced document (relative\n * to the root of the database).\n */\n readonly path: string;\n\n /**\n * Pre-fetched data payload to eliminate N+1 queries.\n * When present, clients can use this directly instead of fetching.\n */\n readonly data?: Record<string, unknown>;\n\n constructor(id: string | number, path: string, data?: Record<string, unknown>) {\n this.id = id;\n this.path = path;\n this.data = data;\n }\n\n get pathWithId() {\n return `${this.path}/${this.id}`;\n }\n\n isEntityReference() {\n return false;\n }\n\n isEntityRelation() {\n return true;\n }\n}\n\nexport class GeoPoint {\n\n /**\n * The latitude of this GeoPoint instance.\n */\n readonly latitude: number;\n /**\n * The longitude of this GeoPoint instance.\n */\n readonly longitude: number;\n\n constructor(latitude: number, longitude: number) {\n this.latitude = latitude;\n this.longitude = longitude;\n }\n}\n\nexport class Vector {\n readonly value: number[];\n\n constructor(value: number[]) {\n this.value = value;\n }\n}\n","/**\n * Canonical filter operators and REST wire-format mappings.\n *\n * `WhereFilterOp` is THE operator type used at every layer — from React\n * components through the SDK, server, and down to the database driver.\n *\n * PostgREST short-codes (`eq`, `gt`, `cs`, …) exist **only** at the\n * HTTP wire boundary, handled by `serializeFilter` / `deserializeFilter`\n * in `@rebasepro/common`.\n *\n * ┌──────────────────────┬───────────────┬──────────────────────────────┐\n * │ Canonical │ REST short │ Meaning │\n * ├──────────────────────┼───────────────┼──────────────────────────────┤\n * │ \"==\" │ \"eq\" │ Equal │\n * │ \"!=\" │ \"neq\" │ Not equal │\n * │ \">\" │ \"gt\" │ Greater than │\n * │ \">=\" │ \"gte\" │ Greater than or equal │\n * │ \"<\" │ \"lt\" │ Less than │\n * │ \"<=\" │ \"lte\" │ Less than or equal │\n * │ \"in\" │ \"in\" │ Value in list │\n * │ \"not-in\" │ \"nin\" │ Value not in list │\n * │ \"array-contains\" │ \"cs\" │ Array contains element │\n * │ \"array-contains-any\" │ \"csa\" │ Array contains any of │\n * │ \"like\" │ \"like\" │ SQL LIKE (case-sensitive) │\n * │ \"ilike\" │ \"ilike\" │ SQL ILIKE (case-insensitive) │\n * │ \"not-like\" │ \"nlike\" │ NOT LIKE (case-sensitive) │\n * │ \"not-ilike\" │ \"nilike\" │ NOT ILIKE (case-insensitive) │\n * │ \"is-null\" │ \"isnull\" │ Field IS NULL │\n * │ \"is-not-null\" │ \"notnull\" │ Field IS NOT NULL │\n * └──────────────────────┴───────────────┴──────────────────────────────┘\n *\n * Pattern matching (`like`/`ilike`) uses SQL wildcard syntax: `%` matches any\n * sequence of characters, `_` matches a single character. On MongoDB these are\n * translated to anchored regular expressions; Firestore has no native pattern\n * matching and rejects these operators (use `searchString` instead).\n *\n * @module\n */\n\n/**\n * Where NULLs sort relative to real values on one key.\n *\n * Absent means the convention Postgres itself applies and the driver writes\n * out: `NULLS LAST` ascending, `NULLS FIRST` descending. That convention was\n * hardcoded and unstateable — a \"newest first\" list put every row with no date\n * at the very top, and the only way out was to add a `is-not-null` filter and\n * lose those rows entirely.\n *\n * The keyset comparison honours whatever is chosen here, so a cursor over a\n * nullable key stays correct under either placement.\n *\n * @group Models\n */\nexport type NullsPlacement = \"first\" | \"last\";\n\n/**\n * Canonical sort representation: `[fieldName, direction]`, optionally with a\n * {@link NullsPlacement}.\n *\n * Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.\n * The colon-string form (`\"field:direction\"`, or `\"field:direction:nulls\"`)\n * exists only at the HTTP wire boundary, handled by `serializeOrderBy` /\n * `deserializeOrderBy` in `@rebasepro/common`.\n *\n * The third slot is optional so every `[field, direction]` written before it\n * existed is still exactly this type, and every `const [field, direction] =`\n * destructure still reads what it always read.\n *\n * @group Models\n */\nexport type OrderByTuple<Key extends string = string> = [Key, \"asc\" | \"desc\", NullsPlacement?];\n\n/**\n * One sort key, or several applied in order of significance.\n *\n * ```ts\n * orderBy: [\"created_at\", \"desc\"] // one key\n * orderBy: [[\"roles\", \"asc\"], [\"created_at\", \"desc\"]] // roles, then newest first\n * ```\n *\n * The two forms are told apart by whether the first element is itself an\n * array, so a single tuple never needs wrapping and every existing caller\n * keeps working unchanged. `normalizeOrderBy` in `@rebasepro/common` collapses\n * both to the list form, which is what every layer below the call site speaks.\n *\n * Ties on the last key are broken by the row id, so a multi-key sort is a\n * total order and pages over it neither repeat nor skip rows.\n *\n * @group Models\n */\nexport type OrderBySpec<Key extends string = string> =\n | OrderBySortTuple<Key>\n | OrderBySortTuple<Key>[];\n\n/**\n * A sort key: a field name, or an aggregate over a to-many relation.\n *\n * @group Models\n */\nexport type SortKey<Key extends string = string> = Key | RelationAggregateSort;\n\n/**\n * `[sortKey, direction]` — the authoring form of {@link OrderByTuple}, which\n * additionally accepts a {@link RelationAggregateSort} object.\n *\n * The object never reaches a driver: `normalizeOrderBy` in `@rebasepro/common`\n * encodes it to its string spelling on the way down, and everything below that\n * point speaks plain `OrderByTuple`. See {@link RelationAggregateSort} for why\n * the wire form is a string.\n *\n * @group Models\n */\nexport type OrderBySortTuple<Key extends string = string> = [SortKey<Key>, \"asc\" | \"desc\", NullsPlacement?];\n\n/**\n * The aggregate functions a relation sort can apply.\n *\n * Five, and no `array_agg`/`string_agg`: an aggregate used as a sort key has to\n * produce something with an order, and these are the ones that do.\n *\n * @group Models\n */\nexport type RelationAggregateFn = \"min\" | \"max\" | \"count\" | \"sum\" | \"avg\";\n\n/**\n * Order rows by an aggregate over the rows a to-many relation reaches —\n * \"candidates, oldest waiting first\", \"clients, busiest first\".\n *\n * ```ts\n * // The date of each candidate's earliest open application.\n * orderBy: [[{ relation: \"applications\", field: \"created_at\", agg: \"min\" }, \"asc\"]]\n *\n * // How many applications each candidate has.\n * orderBy: [[{ relation: \"applications\", agg: \"count\" }, \"desc\"]]\n * ```\n *\n * This is the half of a queue that cannot be worked around client-side. A\n * *filter* over a relation can be approximated by denormalising a flag onto the\n * row; an *ordering* cannot be approximated at all once the result set is\n * paged, because the client only ever holds one page and the page was chosen by\n * the wrong order.\n *\n * Rows the relation reaches nothing from sort last ascending and first\n * descending — the placement Postgres gives a `NULL`, stated rather than\n * inherited, because the keyset comparison behind cursor paging has to agree\n * with it exactly. Ties are broken by the row id, so the order is total and\n * paging over it neither repeats nor skips.\n *\n * Compiled by the driver into a correlated subquery, so it is subject to the\n * reader's own row-level security on the target table: a related row the reader\n * cannot see does not contribute to the aggregate. Offered only where\n * {@link DataSourceCapabilities.relationAggregateSorts} says the driver can\n * compile it.\n *\n * @group Models\n */\nexport interface RelationAggregateSort {\n /** The to-many relation to aggregate over, by its name on this collection. */\n relation: string;\n\n /** The aggregate to apply. */\n agg: RelationAggregateFn;\n\n /**\n * The column of the *target* to aggregate. Required by every function\n * except `count`, which counts the related rows themselves when it is\n * omitted — and counts the rows whose column is non-null when it is not.\n */\n field?: string;\n}\n\n/** The wire spelling of a {@link RelationAggregateSort}: `min(applications.created_at)`. */\nconst RELATION_AGGREGATE_SORT_PATTERN = /^(min|max|count|sum|avg)\\(([^().]+)(?:\\.([^()]+))?\\)$/;\n\n/**\n * A {@link RelationAggregateSort} as a single string — `min(applications.created_at)`,\n * `count(applications)`.\n *\n * The wire form is a string because every layer below the call site already is\n * one: `OrderByTuple` is `[string, direction]`, the REST parameter is\n * `?orderBy=key:direction`, the driver contract takes `orderBy?: string |\n * OrderByTuple[]`, and a cursor names its keys by string. `_score` established\n * the same pattern — a sort key that is not a column, spelled as one — and this\n * reuses it rather than widening five signatures to carry an object that would\n * be flattened at the end anyway.\n *\n * SQL's own spelling, so the key reads as what it compiles to. Neither `:` nor\n * `,` appears in it, which is what keeps it safe in the colon-delimited wire\n * shorthand.\n *\n * @group Models\n */\nexport function encodeRelationAggregateSort(sort: RelationAggregateSort): string {\n return `${sort.agg}(${sort.relation}${sort.field ? `.${sort.field}` : \"\"})`;\n}\n\n/**\n * Read the string spelling back, or `undefined` if it is not one.\n *\n * `undefined` rather than a throw: this is asked of *every* sort key to find\n * out which kind it is, and an ordinary column name is not an error.\n *\n * @group Models\n */\nexport function parseRelationAggregateSort(key: string): RelationAggregateSort | undefined {\n const match = RELATION_AGGREGATE_SORT_PATTERN.exec(key);\n if (!match) return undefined;\n const [, agg, relation, field] = match;\n // `min()` and friends have nothing to aggregate without a column, and a\n // key that parses to a half-built sort would resolve to no expression and\n // be dropped — leaving the rows unsorted while the caller believes\n // otherwise. `count` is the one function that means something on its own.\n if (!field && agg !== \"count\") return undefined;\n return { agg: agg as RelationAggregateFn, relation, ...(field && { field }) };\n}\n\n/** Is this sort key the object form rather than a field name? */\nexport function isRelationAggregateSort(key: unknown): key is RelationAggregateSort {\n return typeof key === \"object\" && key !== null &&\n typeof (key as RelationAggregateSort).relation === \"string\" &&\n typeof (key as RelationAggregateSort).agg === \"string\";\n}\n\n/** A sort key in the single-string form every layer below the call site speaks. */\nexport function sortKeyToString(key: SortKey): string {\n return isRelationAggregateSort(key) ? encodeRelationAggregateSort(key) : key;\n}\n\n/**\n * Canonical filter operators supported across all database backends.\n * Each DB driver translates these to its native query format.\n *\n * @group Models\n */\nexport type WhereFilterOp =\n | \"<\"\n | \"<=\"\n | \"==\"\n | \"!=\"\n | \">=\"\n | \">\"\n | \"array-contains\"\n | \"in\"\n | \"not-in\"\n | \"array-contains-any\"\n | \"like\"\n | \"ilike\"\n | \"not-like\"\n | \"not-ilike\"\n | \"is-null\"\n | \"is-not-null\";\n\n/**\n * Used to define filters applied in collections.\n *\n * A single condition is a tuple `[operator, value]`.\n * Multiple conditions on the same field use an array of tuples.\n *\n * @example\n * // Single condition per field\n * { status: [\"==\", \"active\"], price: [\">=\", 9.99] }\n *\n * // Multiple conditions on one field\n * { age: [[\">=\", 18], [\"<\", 65]] }\n *\n * // Array operators\n * { role: [\"in\", [\"admin\", \"editor\"]] }\n * { tags: [\"array-contains\", \"featured\"] }\n *\n * // Pattern matching (SQL wildcards: % and _)\n * { name: [\"ilike\", \"%john%\"] }\n * { slug: [\"like\", \"post-%\"] }\n *\n * // Null checks (the value is ignored; `null` is conventional)\n * { deleted_at: [\"is-null\", null] }\n * { published_at: [\"is-not-null\", null] }\n *\n * @group Models\n */\nexport type FilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;\n\n/**\n * The field names a query may address on a row type: every column, plus a\n * dotted path reaching inside one — or *through a relation* to a column of the\n * related row.\n *\n * A dotted path is not checked at all, in either direction. That is a\n * deliberate loosening, and it is worth being exact about what it costs. The\n * root used to be checked: `\"meta.tag\"` required a `meta` column. It cannot\n * stay checked, because the other thing a dotted path now means is\n * `\"applications.status\"` — and `applications` is a *relation*, which comes\n * from the collection's `relations` and is not a column of `M` at all. There is\n * nothing in a generated row type that could validate one. `FindParams.include`\n * is `string[]` for exactly this reason and says so.\n *\n * So the guarantee moves rather than disappears: an unresolvable path is a 400\n * from the driver, not a silently dropped condition. See\n * `UnknownFilterFieldsMode` in `@rebasepro/server-postgres` — dropping a filter\n * key *widens* the read to every row, which is why that resolution fails\n * closed. A typo'd relation path is refused at runtime with the target\n * collection's real column list in the message.\n *\n * A JSON path — `metadata->>tier` — is admitted on the same terms and for the\n * same reason. It addresses a key *inside* a `json`/`jsonb` column, so nothing\n * in a generated row type describes it either; the driver resolves it and\n * refuses what it cannot. It has no dot, so the dotted branch above never\n * covered it, and every documented `?metadata->>tier=eq.gold` filter was a\n * compile error on a typed client while working perfectly over HTTP.\n *\n * Undotted keys are unaffected and still checked against `keyof M`.\n *\n * When `M` is left at its default `Record<string, unknown>`, `keyof M` is\n * `string` and this collapses to `string`, so every query stays permissive.\n * That is what keeps an untyped `createRebaseClient()` behaving exactly as it\n * did before the row type was threaded through.\n *\n * @group Models\n */\nexport type FieldPath<M extends Record<string, unknown> = Record<string, unknown>> =\n | Extract<keyof M, string>\n | NonColumnFieldPath;\n\n/**\n * A field key that is not a column: a relation path (`author.name`) or a JSON\n * path (`metadata->>tier`).\n *\n * The fluent builder needs this on its own, where `FindParams` does not. Its\n * `where(column, operator, value)` types the value against `M[column]`, which\n * only means something for a real column — so paths take a second overload\n * whose value is `unknown`. Keying that overload on the *shape* of a path,\n * rather than on \"everything that is not a column\", is what keeps a real column\n * with a wrong value type from falling through to it and being accepted: a\n * mistyped column name has neither a dot nor a `->>`, so it matches neither\n * overload and is still refused.\n *\n * @group Models\n */\nexport type NonColumnFieldPath =\n | `${string}.${string}`\n | `${string}->>${string}`;\n\n/**\n * Relaxed filter type that also accepts pre-serialized PostgREST strings.\n * **Internal only** — used at the wire-format boundary\n * (`serializeFilter` / `deserializeFilter` in `@rebasepro/common`).\n *\n * Application code, UI components, and SDK consumers should use\n * {@link FilterValues} instead.\n *\n * @internal\n */\nexport type WireFilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][] | string>>;\n\n/**\n * A pre-defined filter preset for quick access in the collection toolbar.\n * Users can select a preset to instantly apply a set of filters and\n * optionally a sort order.\n *\n * @group Models\n */\nexport interface FilterPreset<Key extends string = string> {\n /**\n * Display label shown in the preset menu.\n * If omitted, a summary is auto-generated from the filter keys.\n */\n label?: string;\n\n /**\n * The filter values to apply when this preset is selected.\n */\n filterValues: FilterValues<Key>;\n\n /**\n * Optional sort override to apply alongside the filter values.\n * One key, or several in order of significance.\n */\n sort?: OrderBySpec<Key>;\n}\n\n/**\n * PostgREST short-code operators. Wire format only — these never appear\n * in application code. Used by `serializeFilter`/`deserializeFilter`\n * in `@rebasepro/common`.\n */\nexport type RestFilterOp =\n | \"eq\" | \"neq\"\n | \"gt\" | \"gte\"\n | \"lt\" | \"lte\"\n | \"in\" | \"nin\"\n | \"cs\" | \"csa\"\n | \"like\" | \"ilike\"\n | \"nlike\" | \"nilike\"\n | \"isnull\" | \"notnull\";\n\n/** Maps canonical operators to their REST short-code equivalents. */\nexport const CANONICAL_TO_REST: Readonly<Record<WhereFilterOp, RestFilterOp>> = {\n \"==\": \"eq\",\n \"!=\": \"neq\",\n \">\": \"gt\",\n \">=\": \"gte\",\n \"<\": \"lt\",\n \"<=\": \"lte\",\n \"in\": \"in\",\n \"not-in\": \"nin\",\n \"array-contains\": \"cs\",\n \"array-contains-any\": \"csa\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"not-like\": \"nlike\",\n \"not-ilike\": \"nilike\",\n \"is-null\": \"isnull\",\n \"is-not-null\": \"notnull\"\n};\n\n/** Maps REST short-code operators to their canonical equivalents. */\nexport const REST_TO_CANONICAL: Readonly<Record<RestFilterOp, WhereFilterOp>> = {\n \"eq\": \"==\",\n \"neq\": \"!=\",\n \"gt\": \">\",\n \"gte\": \">=\",\n \"lt\": \"<\",\n \"lte\": \"<=\",\n \"in\": \"in\",\n \"nin\": \"not-in\",\n \"cs\": \"array-contains\",\n \"csa\": \"array-contains-any\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"nlike\": \"not-like\",\n \"nilike\": \"not-ilike\",\n \"isnull\": \"is-null\",\n \"notnull\": \"is-not-null\"\n};\n\n/**\n * Operators that test for null/not-null and therefore ignore their value.\n * Codecs normalize the value of these conditions to `null`.\n */\nexport const NULL_OPS: ReadonlySet<WhereFilterOp> = new Set<WhereFilterOp>([\n \"is-null\", \"is-not-null\"\n]);\n\n/**\n * Operators whose operand is a **list** of values rather than one value.\n *\n * On the wire that list is always parenthesised — `in.(draft,review)` — which\n * is what lets the REST codec tell `?status=in.(a,b)` (the operator) from\n * `?status=in.progress` (a value that happens to start with an operator's\n * name). See `deserializeSingle` in `@rebasepro/common`.\n *\n * @group Models\n */\nexport const LIST_OPS: ReadonlySet<WhereFilterOp> = new Set<WhereFilterOp>([\n \"in\", \"not-in\", \"array-contains-any\"\n]);\n\n/**\n * Every canonical operator, in a stable order. Useful for engine capability\n * declarations ({@link DataSourceCapabilities.filterOperators}) and for\n * building operator subsets.\n * @group Models\n */\nexport const ALL_WHERE_FILTER_OPS: readonly WhereFilterOp[] = [\n \"<\", \"<=\", \"==\", \"!=\", \">=\", \">\",\n \"in\", \"not-in\",\n \"array-contains\", \"array-contains-any\",\n \"like\", \"ilike\", \"not-like\", \"not-ilike\",\n \"is-null\", \"is-not-null\"\n];\n\n/** All canonical operator strings for runtime validation. */\nconst CANONICAL_OPS: ReadonlySet<string> = new Set<WhereFilterOp>(ALL_WHERE_FILTER_OPS);\n\n/**\n * The REST table as a `Map`, because the key `toCanonicalOp` is handed comes\n * off the wire.\n *\n * Indexed as a plain object, every `Object.prototype` member answered:\n * `toCanonicalOp(\"valueOf\")` returned the inherited *function* as though it\n * were a `WhereFilterOp`, and every caller here treats a defined result as\n * \"known operator\". Same defect the REST codec's own lookup tables were\n * converted away from in `filter-dialect.ts`; this is the copy that survived\n * one package over, and it now sits under the operator validation the REST\n * parser does, which would otherwise have admitted `[\"constructor\", x]`.\n */\nconst REST_OP_LOOKUP: ReadonlyMap<string, WhereFilterOp> = new Map<string, WhereFilterOp>(\n Object.entries(REST_TO_CANONICAL) as [string, WhereFilterOp][]\n);\n\n/**\n * Resolve any operator string (canonical or REST short-code) to its\n * canonical `WhereFilterOp` form. Returns `undefined` for unknown operators.\n *\n * @example\n * toCanonicalOp(\"==\") // \"==\"\n * toCanonicalOp(\"eq\") // \"==\"\n * toCanonicalOp(\"cs\") // \"array-contains\"\n * toCanonicalOp(\"xyz\") // undefined\n */\nexport function toCanonicalOp(op: string): WhereFilterOp | undefined {\n if (CANONICAL_OPS.has(op)) return op as WhereFilterOp;\n return REST_OP_LOOKUP.get(op);\n}\n","/**\n * The keys of a collection's admin block, as data.\n *\n * There is no *type* for the block in this package any more, and that is the point:\n * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a\n * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by\n * declaration merging, which is why installing it is what makes the admin surface\n * appear.\n *\n * The *list* still has to live here, because three runtime consumers need it and two\n * of them are core — see below.\n */\n\n/**\n * Every key that belongs inside a collection's `admin` block, as data.\n *\n * The type that describes these fields is `AdminCollectionOptions` in\n * `@rebasepro/cms-types`, and it is erased at build time — but three runtime\n * consumers need the list, and two of them are core:\n *\n * - `serializeCollections`, to drop the block from the contract\n * - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection\n * files on disk from the admin panel and has to know where each key goes. A key\n * missing from this list gets written to the *top level* of the file, where the\n * backend ignores it and the panel never finds it again.\n * - the `collections-admin-block` codemod\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real option\n * keys; the count is pinned by a test there.\n *\n * @group Models\n */\nexport const ADMIN_COLLECTION_KEYS = [\n \"Actions\",\n \"additionalFields\",\n \"alwaysApplyDefaultValues\",\n \"browserCallbacks\",\n \"components\",\n \"customViews\",\n \"defaultEntityAction\",\n \"defaultFilter\",\n \"defaultSelectedView\",\n \"defaultSize\",\n \"defaultViewMode\",\n \"disableDefaultActions\",\n \"display\",\n \"enabledViews\",\n \"entityActions\",\n \"entityViews\",\n \"exportable\",\n \"filterPresets\",\n \"fixedFilter\",\n \"form\",\n \"formAutoSave\",\n \"formView\",\n \"group\",\n \"hideFromEntityViews\",\n \"hideFromNavigation\",\n \"hideIdFromCollection\",\n \"hideIdFromForm\",\n \"icon\",\n \"includeJsonView\",\n \"inlineEditing\",\n \"kanban\",\n \"listProperties\",\n \"localChangesBackup\",\n \"openEntityMode\",\n \"orderProperty\",\n \"pagination\",\n \"previewProperties\",\n \"propertiesOrder\",\n \"selectionController\",\n \"selectionEnabled\",\n \"sideDialogWidth\",\n \"sort\"\n] as const;\n\n/** A key of a collection's `admin` block. @group Models */\nexport type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];\n\n/**\n * Every key that belongs inside a *property's* `admin` block, as data.\n *\n * The union of `AdminPropertyOptions` and its per-type extensions\n * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.\n * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the\n * runtime consumers are core packages that the BaaS guard forbids from\n * importing `@rebasepro/cms-types`. Here it is the boot-time collection\n * validator in `@rebasepro/server`, which has to tell \"you left `readOnly` at\n * the top of the property, where nothing reads it\" apart from \"you invented a\n * key we have never heard of\".\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real\n * option keys.\n *\n * @group Models\n */\nexport const ADMIN_PROPERTY_KEYS = [\n \"canAddElements\",\n \"clearable\",\n \"columnWidth\",\n \"customProps\",\n \"disabled\",\n \"expanded\",\n \"Field\",\n \"Filter\",\n \"filterOperators\",\n \"fixedFilter\",\n \"format\",\n \"hideFromCollection\",\n \"includeEntityLink\",\n \"includeId\",\n \"markdown\",\n \"minimalistView\",\n \"multiline\",\n \"Preview\",\n \"previewAsTag\",\n \"previewProperties\",\n \"readOnly\",\n \"renderInForm\",\n \"sortable\",\n \"span\",\n \"spreadChildren\",\n \"urlPreview\",\n \"widget\",\n] as const;\n\n/** A key of a property's `admin` block. @group Models */\nexport type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];\n\n/**\n * Move flattened admin keys back down into the `admin` block.\n *\n * The admin panel works with a *flat* view model — the block merged onto the\n * collection — so what comes back from a form has `icon` and `defaultViewMode`\n * at the top level while `admin` still holds whatever the file was loaded with.\n * This is the way back.\n *\n * **The top-level value wins.** It is the one the form just wrote; the block is\n * the copy the collection was loaded with, and preferring it resolves every edit\n * in favour of the value the user changed away from.\n *\n * This lives here, next to the key lists, because it had two implementations —\n * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in\n * `@rebasepro/server`'s schema editor — that agreed on everything except that\n * precedence, which is the only part that decides whether a save is visible.\n *\n * @group Models\n */\nexport function nestAdminKeysOf(\n source: Record<string, unknown>,\n adminKeys: readonly string[]\n): Record<string, unknown> {\n const keys = new Set<string>(adminKeys);\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if (keys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top;\n}\n\n/**\n * {@link nestAdminKeysOf} for a collection.\n *\n * @group Models\n */\nexport function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {\n return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);\n}\n\n/**\n * {@link nestAdminKeysOf} for a property, applied to its children too.\n *\n * A map property carries `properties`, an array property carries `of`, and both\n * hold properties with `admin` blocks of their own. A flat `readOnly` left on a\n * child is as dead — and as fatal at the next boot — as one left on the parent,\n * so the walk goes all the way down.\n *\n * @group Models\n */\nexport function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);\n\n const children = nested.properties;\n if (children && typeof children === \"object\" && !Array.isArray(children)) {\n nested.properties = Object.fromEntries(\n Object.entries(children as Record<string, unknown>).map(([key, child]) => [\n key,\n child && typeof child === \"object\" && !Array.isArray(child)\n ? nestAdminPropertyKeys(child as Record<string, unknown>)\n : child\n ])\n );\n }\n\n const of = nested.of;\n if (Array.isArray(of)) {\n nested.of = of.map(entry => entry && typeof entry === \"object\" && !Array.isArray(entry)\n ? nestAdminPropertyKeys(entry as Record<string, unknown>)\n : entry);\n } else if (of && typeof of === \"object\") {\n nested.of = nestAdminPropertyKeys(of as Record<string, unknown>);\n }\n\n return nested;\n}\n","import { ALL_WHERE_FILTER_OPS, WhereFilterOp } from \"./filter-operators\";\n\n/**\n * Describes the capabilities and features supported by a data source (driver).\n *\n * Each driver (Postgres, Firebase, MongoDB, etc.) declares which features it\n * supports. The admin uses this descriptor to:\n * - Show/hide editor tabs (e.g. Relations for SQL, Subcollections for Firebase)\n * - Filter the property type picker (e.g. `relation` for SQL, `reference` for Firebase)\n * - Toggle driver-specific form controls (e.g. `columnType` for SQL)\n *\n * @group Models\n */\nexport interface DataSourceCapabilities {\n /** Unique driver key (e.g. \"postgres\", \"firestore\", \"mongodb\") */\n key: string;\n\n /** Human-readable label for the UI (e.g. \"PostgreSQL\", \"Firebase / Firestore\") */\n label: string;\n\n // ── Feature flags ─────────────────────────────────────────────────\n /** Does this source support SQL-style relations (JOINs)? */\n supportsRelations: boolean;\n\n /** Does this source support nested subcollections? */\n supportsSubcollections: boolean;\n\n /** Does this source support Row Level Security policies? */\n supportsRLS: boolean;\n\n /** Does this source support document references (Firebase-style)? */\n supportsReferences: boolean;\n\n /** Does this source support SQL column type annotations? */\n supportsColumnTypes: boolean;\n\n /** Does this source support real-time listeners? */\n supportsRealtime: boolean;\n\n /**\n * Does this source store vectors natively?\n *\n * `VectorProperty` carries a `dimensions` and is pgvector-shaped. It was\n * the one driver-specific property kind with no flag to gate it, so unlike\n * every other field in this descriptor there was not even a runtime answer\n * to appeal to — a Firestore collection could declare an embedding column\n * and no driver would do anything with it.\n */\n supportsVectors: boolean;\n\n /**\n * Canonical filter operators this engine can execute.\n *\n * The admin UI intersects this set with the property-type defaults and\n * any per-property narrowing (`property.ui.filterOperators`) to decide\n * which operators to offer in filter fields — so an engine that cannot\n * run `ilike` (e.g. Firestore) never shows a \"Contains\" filter that\n * would throw at query time.\n */\n filterOperators: readonly WhereFilterOp[];\n\n /**\n * Relation kinds this engine's driver can compile into a filter.\n *\n * Only `belongsTo` puts a column on the row being filtered; the others are\n * answered with a correlated subquery over the junction or the target\n * table, which not every driver can build. An engine with no relations at\n * all declares none.\n *\n * The admin uses this to decide whether a relation column offers a filter\n * control. Offering one an engine cannot answer is not cosmetic: a driver\n * that drops the key it cannot resolve *widens* the read to every row, and\n * one that fails closed answers a control the admin itself put on screen\n * with a 400.\n *\n * Optional, so a third-party driver registered before this existed still\n * compiles. Omitted means {@link DEFAULT_FILTERABLE_RELATION_KINDS} — the\n * one kind that is a plain column comparison, which every relational\n * driver can do. The subquery kinds are a real capability and have to be\n * claimed rather than assumed: assuming them wrongly is the widening.\n */\n filterableRelationKinds?: readonly string[];\n\n /**\n * Can a filter address a *column of the related row* — `applications.status`\n * — rather than only the related row's id?\n *\n * A separate capability from {@link filterableRelationKinds} because it is\n * a separate subquery: the id filter stops at the junction, one of these\n * reaches the target table and compares one of its columns. A driver can\n * do the first and not the second.\n *\n * Optional and defaulting to **false**, for the reason the relation kinds\n * default narrow: an unclaimed capability that the admin assumes is there\n * produces a control whose query the driver answers by dropping the key —\n * and a dropped filter key widens the read to every row.\n *\n * Meaningless without {@link supportsRelations}; a driver with no relations\n * has nothing to reach through.\n */\n supportsRelationFieldFilters?: boolean;\n\n /**\n * Can a sort key be an aggregate over a to-many relation — \"oldest waiting\n * first\", \"busiest first\"?\n *\n * Compiled as a correlated scalar subquery in `ORDER BY`, which a document\n * store cannot express at all. Optional and defaulting to **false**.\n *\n * A wrongly claimed sort capability fails differently from a wrongly\n * claimed filter one, and worse in one respect: a driver that cannot\n * resolve the key drops the `ORDER BY` and answers 200 with rows in\n * whatever order the database pleased, which reads as a sorted list. Paging\n * over that repeats and skips rows.\n */\n relationAggregateSorts?: boolean;\n\n // ── Admin capability flags ───────────────────────────────────────\n /** Does this source support SQL admin operations (SQL editor, EXPLAIN, etc.)? */\n supportsSQLAdmin: boolean;\n\n /** Does this source support document admin operations (aggregation, stats)? */\n supportsDocumentAdmin: boolean;\n\n /** Does this source support schema admin (unmapped tables, table metadata)? */\n supportsSchemaAdmin: boolean;\n}\n\n/**\n * Subset of DataSourceCapabilities containing only feature flags.\n * Useful when you only need to check capabilities without UI metadata.\n * @group Models\n */\nexport type DataSourceFeatures = Omit<DataSourceCapabilities, \"key\" | \"label\">;\n\n/**\n * The default data-source key, used when a collection does not name a\n * `dataSource`. Shared by the frontend router and the backend driver\n * registry so both agree on \"the default database\".\n * @group Models\n */\nexport const DEFAULT_DATA_SOURCE_KEY = \"(default)\";\n\n/**\n * How the *frontend* reaches a data source.\n *\n * - `\"server\"` — through the Rebase backend (the `RebaseClient`). The backend\n * holds the actual database adapter and routes by data-source key. This is\n * the default and covers Postgres, MongoDB, and any other server-mediated\n * engine.\n * - `\"direct\"` — straight from the client to the external backend via its own\n * SDK driver (e.g. Firestore). The Rebase backend is not in the data path.\n * - `\"custom\"` — a developer-supplied {@link DataDriver}, transport unspecified.\n *\n * @group Models\n */\nexport type DataSourceTransport = \"server\" | \"direct\" | \"custom\";\n\n/**\n * Declarative definition of a data source — a named place data lives.\n *\n * Declared once and shared front and back: the frontend uses it to decide\n * transport (client vs direct driver), the backend uses the same `key` to\n * resolve a database adapter, and the editor derives capabilities from\n * `engine`. Collections reference a definition by its `key` via\n * `collection.dataSource`.\n *\n * @group Models\n */\nexport interface DataSourceDefinition {\n /**\n * Unique identifier for this data source. Collections point at it via\n * `dataSource`. Defaults to {@link DEFAULT_DATA_SOURCE_KEY}.\n */\n key: string;\n\n /**\n * The engine backing this data source (e.g. `\"postgres\"`, `\"mongodb\"`,\n * `\"firestore\"`, or a custom id). Determines the\n * {@link DataSourceCapabilities} surfaced in the editor.\n */\n engine: string;\n\n /**\n * How the frontend reaches this source. Optional — when omitted it is\n * inferred: `\"direct\"` if the definition carries a client-side driver,\n * `\"server\"` otherwise.\n */\n transport?: DataSourceTransport;\n\n /**\n * The physical database/schema/Firestore-database within the engine.\n * Threaded to drivers/adapters as the existing `databaseId` runtime\n * parameter. Defaults to the engine's own default.\n */\n databaseId?: string;\n\n /** Human-readable label for the UI. */\n label?: string;\n}\n\n/**\n * The resolved data source for a collection: the single source of truth that\n * the frontend router, backend registry, and editor all derive from.\n * Produced by `resolveDataSource(collection, registry)`.\n *\n * @group Models\n */\nexport interface ResolvedDataSource {\n /** Data-source key (routing key, shared front + back). */\n key: string;\n /** Engine backing the source (drives capabilities). */\n engine: string;\n /** Frontend transport. */\n transport: DataSourceTransport;\n /** Within-engine instance, if any (the `databaseId` runtime param). */\n databaseId?: string;\n /** Capabilities derived from {@link engine}. */\n capabilities: DataSourceCapabilities;\n}\n\n/**\n * Relation kinds assumed filterable when a driver does not say.\n *\n * `belongsTo` alone: its filter is a comparison on a column of the row being\n * filtered, the one shape that needs no query construction a driver might not\n * have. Everything else is a correlated subquery over another table.\n *\n * @group Models\n */\nexport const DEFAULT_FILTERABLE_RELATION_KINDS: readonly string[] = [\"belongsTo\"];\n\n// ── Built-in driver capabilities ─────────────────────────────────────\n\n/** @group Models */\nexport const POSTGRES_CAPABILITIES: DataSourceCapabilities = {\n key: \"postgres\",\n label: \"PostgreSQL\",\n supportsRelations: true,\n supportsSubcollections: false,\n supportsRLS: true,\n supportsReferences: false,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // `via` is absent: its join path is authored source → target with no\n // stated inverse, so the driver has nothing to reverse into a filter.\n filterableRelationKinds: [\"belongsTo\", \"manyToMany\", \"hasMany\", \"hasOne\"],\n supportsRelationFieldFilters: true,\n relationAggregateSorts: true,\n supportsSQLAdmin: true,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: true\n};\n\n/** @group Models */\nexport const FIREBASE_CAPABILITIES: DataSourceCapabilities = {\n key: \"firestore\",\n label: \"Firebase / Firestore\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: true,\n supportsVectors: false,\n // Firestore has no SQL pattern matching — the driver throws on the LIKE\n // family, so the UI must never offer it.\n filterOperators: ALL_WHERE_FILTER_OPS.filter(op =>\n op !== \"like\" && op !== \"ilike\" && op !== \"not-like\" && op !== \"not-ilike\"),\n // No relations at all — a document store links by reference. Nothing to\n // reach through, so neither of the two relation-reaching features either.\n filterableRelationKinds: [],\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: false,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: false\n};\n\n/** @group Models */\nexport const MONGODB_CAPABILITIES: DataSourceCapabilities = {\n key: \"mongodb\",\n label: \"MongoDB\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: false,\n supportsVectors: false,\n filterOperators: ALL_WHERE_FILTER_OPS,\n filterableRelationKinds: [],\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: false,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\n/**\n * Fallback capabilities when the driver is unknown.\n * Enables everything so nothing is hidden unexpectedly.\n * @group Models\n */\nexport const DEFAULT_CAPABILITIES: DataSourceCapabilities = {\n key: \"(default)\",\n label: \"Default\",\n supportsRelations: true,\n supportsSubcollections: true,\n supportsRLS: true,\n supportsReferences: true,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // The exception to this descriptor's \"enable everything\" rule. The other\n // flags hide a tab or a picker when they are wrong; this one decides\n // whether a query is sent that an unknown driver may answer by dropping\n // the condition — which returns every row rather than none.\n filterableRelationKinds: DEFAULT_FILTERABLE_RELATION_KINDS,\n // Narrow for the same reason, and more sharply. An unknown driver that is\n // assumed to compile these answers by dropping the key: the filter widens\n // the read to every row, and the sort comes back unordered while looking\n // sorted. Both have to be claimed.\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: true,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\nconst CAPABILITIES_REGISTRY: Record<string, DataSourceCapabilities> = {\n postgres: POSTGRES_CAPABILITIES,\n firestore: FIREBASE_CAPABILITIES,\n mongodb: MONGODB_CAPABILITIES,\n \"(default)\": DEFAULT_CAPABILITIES\n};\n\n/**\n * Look up capabilities for a given engine key.\n * If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.\n * @group Models\n */\nexport function getDataSourceCapabilities(engine?: string): DataSourceCapabilities {\n if (!engine) return POSTGRES_CAPABILITIES; // postgres is the default engine\n return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;\n}\n\n/**\n * Register custom capabilities for a third-party driver.\n * @group Models\n */\nexport function registerDataSourceCapabilities(capabilities: DataSourceCapabilities): void {\n CAPABILITIES_REGISTRY[capabilities.key] = capabilities;\n}\n","import type { CollectionCallbacks } from \"./entity_callbacks\";\n\nimport type { EnumValues, Properties, PostgresProperties, FirebaseProperties, MongoProperties } from \"./properties\";\n\nimport type { User } from \"../users\";\nimport type { EmailSendResult } from \"../controllers/email\";\nimport type { Relation } from \"./relations\";\nimport type { SecurityRule } from \"./security_rules\";\nimport { getDataSourceCapabilities } from \"./data_source\";\nimport type { WhereFilterOp, FilterValues, FilterPreset } from \"./filter-operators\";\nimport type { SearchConfig } from \"./search\";\nimport type { CollectionIndex } from \"./indexes\";\nimport type { CollectionTenantConfig } from \"./tenancy\";\n\n/**\n * Base interface containing all driver-agnostic collection properties.\n * Use {@link PostgresCollectionConfig} or {@link FirebaseCollectionConfig} for\n * driver-specific type safety, or {@link CollectionConfig} when you\n * need to handle any collection regardless of backend.\n *\n * @group Models\n */\nexport interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {\n\n /**\n * The collection's identity. Required, and the value nearly everything else\n * keys on:\n *\n * - the REST path — `/api/data/<slug>`\n * - the SDK accessor — `client.data.<slug>` / `client.data.collection(\"<slug>\")`\n * - the admin panel's URL\n * - the target of a `reference` or `relation` property\n *\n * Conventionally kebab-case and plural (`blog-posts`). It is independent of\n * {@link table}: the slug is what callers say, the table is where the rows\n * live, and renaming one does not rename the other.\n *\n * Treat it as frozen once anything has shipped against it — changing a slug\n * changes every URL and every generated accessor at once.\n *\n * @example\n * defineCollection({\n * slug: \"blog-posts\", // /api/data/blog-posts, client.data.blogPosts\n * table: \"posts\",\n * properties: { … }\n * })\n */\n slug: string;\n\n /**\n * Name of the collection, typically plural.\n * E.g. `Products`, `Blog`\n */\n name: string;\n\n /**\n * Singular name of an entry in this collection\n * E.g. `Product`, `Blog entry`\n */\n singularName?: string;\n\n /**\n * Optional description of this view. You can use Markdown.\n */\n description?: string;\n\n /**\n * Child collections nested under entities of this collection.\n * Populated automatically during normalization from driver-specific fields\n * (e.g. Firebase `subcollections`, Postgres `relations` with many-cardinality).\n *\n * Custom drivers can set this directly to expose child collections to the UI.\n */\n childCollections?: () => CollectionConfig<Record<string, unknown>>[];\n\n\n /**\n * The data source this collection belongs to — the routing key shared by\n * the frontend router and the backend driver registry. It points at a\n * {@link DataSourceDefinition} registered on `<Rebase dataSources>` (front)\n * and `initializeRebaseBackend({ dataSources })` (back).\n *\n * If not specified, the default data source `\"(default)\"` is used, which\n * for a standard Rebase app is the server-mediated Postgres backend.\n *\n * @example\n * // Default data source (server-mediated Postgres)\n * { slug: \"products\" }\n *\n * // A direct-transport Firestore data source registered as \"analytics\"\n * { slug: \"events\", dataSource: \"analytics\" }\n *\n * // The same, spelled once — `defineCollection` accepts the handle\n * // `database(\"analytics\")` returned and records its key here.\n * import { analytics } from \"../resources\";\n * defineCollection({ slug: \"events\", dataSource: analytics, … })\n *\n * A string on the recorded collection, because a collection is data past\n * `defineCollection`: it serialises, it compares with `===`, and it reaches\n * the admin UI over the wire, none of which a handle survives.\n */\n dataSource?: string;\n\n /**\n * The database engine backing this collection (`\"postgres\"`, `\"firestore\"`,\n * `\"mongodb\"`, or a custom id).\n *\n * On concrete collection types ({@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, {@link MongoDBCollectionConfig}) this is a literal\n * discriminant. On the base type it is optional and gets stamped\n * automatically during collection normalization from the registered\n * {@link DataSourceDefinition}.\n *\n * Prefer setting {@link dataSource} and letting the engine be resolved.\n */\n engine?: string;\n\n /**\n * Which database within the engine.\n * - For Firestore: The Firestore database ID (e.g., for multi-database projects)\n * - For PostgreSQL: Schema or database name\n * - For MongoDB: Database name\n *\n * If not specified, the default database of the engine is used. Resolved\n * from the collection's {@link DataSourceDefinition} when omitted here.\n */\n databaseId?: string;\n\n /**\n * Set of properties that compose a entity\n */\n properties: Properties;\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * Mark this collection as an authentication collection.\n * When true, this collection is used for user management, login, password hashing, and invitation flows.\n */\n auth?: boolean | AuthCollectionConfig;\n\n\n\n\n\n\n\n /**\n * Row-level authorization rules for this collection.\n *\n * Driver-agnostic on purpose, unlike `disableDefaultPolicies`, `table` and\n * `relations`, which are declared on {@link PostgresCollectionConfig} only.\n * The rules are a *contract* — who may read or write which rows — and each\n * engine enforces it its own way:\n *\n * - **Postgres** compiles them to real `CREATE POLICY` statements and lets\n * the database enforce them (see {@link PostgresCollectionConfig.securityRules},\n * which narrows this with the raw-SQL details).\n * - **MongoDB** translates them into a query filter it AND-s into every\n * read and write, honouring `access`, `ownerField`, `roles`, `mode` and\n * the `operation`/`operations` selectors, and making a best effort at raw\n * `using`/`withCheck` SQL.\n * - **Firestore** does not implement them at all; its own rules language is\n * evaluated by Google, not from here. `supportsRLS` on\n * {@link DataSourceCapabilities} reports which engines generate policies,\n * which is not the same question as whether an engine honours a rule.\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * This interface defines all the callbacks that can be used when a entity\n * is being created, updated or deleted.\n * Useful for adding your own logic or blocking the execution of the operation.\n */\n readonly callbacks?: CollectionCallbacks<M, USER>;\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * User id of the owner of this collection. This is used only by plugins, or if you\n * are writing custom code\n *\n * **Admin form only — not enforced by the API or the database.** The\n * collection editor stamps it on a collection it creates and shows it\n * beside the name; nothing on the request path consults it. It is not an\n * ownership check, and a collection with somebody else's id here is served\n * to exactly the same callers as one with none.\n */\n ownerId?: string;\n\n /**\n * Arbitrary key-value metadata for external consumers.\n * Not interpreted by Rebase — passed through serialization unchanged.\n * Used by domain apps to store custom per-collection config.\n */\n metadata?: Record<string, unknown>;\n\n\n\n\n /**\n * If set to true, changes to the entity will be saved in a subcollection.\n * This prop has no effect if the history plugin is not enabled\n */\n history?: boolean;\n\n /**\n * Whether a write naming a field this collection does not declare is\n * rejected with a 400. Defaults to `true`.\n *\n * Set to `false` where a column really does exist that the config never\n * declared — populated by a trigger, or introspected rather than declared —\n * and callers need to write it. The column still has to exist: the driver\n * checks the key against the table's own columns whatever this is set to,\n * because a key with no column behind it is not passed to the database and\n * refused, it is dropped from the statement and answered 201.\n *\n * It does not let a typo through to Postgres for Postgres to judge. That is\n * what this flag was documented as doing, and no such judgment ever\n * happened.\n */\n strictWrites?: boolean;\n\n\n\n\n\n\n\n\n}\n\n// ── Driver-specific collection types ──────────────────────────────────\n\n/**\n * A collection backed by PostgreSQL (or any SQL database).\n * Adds support for SQL-style relations (JOINs) and Row Level Security.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only SQL-relevant fields appear.\n *\n * @group Models\n */\nexport interface PostgresCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n properties: PostgresProperties;\n\n /**\n * The database engine for this collection. For Postgres collections this\n * can be omitted (Postgres is the default) or set to `\"postgres\"`.\n */\n engine?: \"postgres\" | undefined;\n\n /**\n * The PostgreSQL table name for this collection.\n *\n * Optional: it defaults to `toSnakeCase(slug)`, which is what\n * `getTableName()` has always returned when it was absent. The type simply\n * demanded what the runtime already derived, so the smallest collection\n * anyone could write named its table twice —\n * `{ slug: \"todos\", table: \"todos\", … }` — and \"why do I write it twice\"\n * is the first question every evaluator asked.\n *\n * Set it only when the table name differs from the slug: an existing\n * database whose table is `blog_posts` while the URL should stay `posts`.\n *\n * Note that a **derived** name is still a real name, and nothing yet warns\n * when one moves. Foreign-key and junction column defaults are derived from\n * the *slug* rather than from this field, so renaming a slug re-derives\n * them on the next `db push` even where `table` is pinned.\n */\n table?: string;\n\n /**\n * The PostgreSQL schema name for this table.\n * E.g. \"public\", \"rebase\", \"auth\".\n * If not specified, \"public\" is used (or the default search path).\n */\n schema?: string;\n\n /**\n * For SQL databases, you can define the relations between collections here.\n * Relations describe JOINs, foreign keys, and junction tables.\n */\n relations?: Relation[];\n\n /**\n * Security rules for this collection (PostgreSQL Row Level Security).\n * When defined, the schema generator will enable RLS on the table and\n * create the corresponding PostgreSQL policies.\n *\n * Supports three levels of expressiveness:\n * 1. **Convenience shortcuts** — `ownerField`, `access`, `roles`\n * 2. **Raw SQL** — `using` and `withCheck` for full PostgreSQL power\n * 3. **Combined** — mix shortcuts with `roles` for common patterns\n *\n * The authenticated user context is available in raw SQL via:\n * - `rebase.uid()` — the current user's ID\n * - `rebase.roles()` — comma-separated app role IDs\n * - `rebase.jwt()` — full JWT claims as JSONB\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * Opt out of the framework's default Row Level Security policies.\n *\n * The schema generator automatically injects, for every collection, a\n * baseline SELECT policy granting the trusted server context and the\n * `admin` role read access (reads run under a restricted role, so RLS\n * default-denies without it). For auth collections it additionally injects\n * a self-read policy (`id = rebase.uid()`) and an admin-only write gate\n * (INSERT/UPDATE/DELETE require the `admin` role or the trusted server\n * context), making privileged columns such as `roles` safe by default.\n *\n * Author-defined `securityRules` are permissive and broaden access on top\n * of these defaults. Set this flag to `true` to remove the defaults\n * entirely and take full responsibility for the collection's RLS.\n *\n * @default false\n */\n disableDefaultPolicies?: boolean;\n\n /**\n * Opt in to Postgres full-text search for this collection.\n *\n * Omit it and `.search()` keeps its existing behaviour exactly — an\n * `ILIKE '%term%'` across top-level string properties. Declare it and the\n * collection gains one generated `tsvector` column and a GIN index, and\n * `.search()` compiles to a ranked `@@ websearch_to_tsquery` against them.\n *\n * Postgres-only, like {@link VectorProperty}: the block is rejected at boot\n * on other engines rather than silently ignored.\n *\n * @see SearchConfig\n */\n search?: SearchConfig;\n\n /**\n * Ordinary indexes on this collection's table.\n *\n * Collection-level, not per-property, because an index over two columns\n * has no single property to hang on and a partial index has none at all —\n * and because a second declaration site for the single-column case would\n * put the same object in two places. An index's identity is a column list\n * in an order; the single-column case is a degenerate one, not a special\n * one.\n *\n * `VectorProperty.index` stays where it is: an ANN structure is a property\n * of the column's type, not of a query.\n *\n * Postgres-only, like {@link SearchConfig}: refused on another engine\n * rather than silently ignored.\n */\n indexes?: readonly CollectionIndex<Extract<keyof M, string>>[];\n\n /**\n * Turn `delete` into \"stamp a timestamp\", and hide stamped rows from reads.\n *\n * With this on, a delete — single, bulk or through a nested path — sets the\n * field to `now()` instead of issuing a `DELETE`, and every read filters\n * `<field> IS NULL` by default: `find`, `findById`, `count`, aggregates, the\n * realtime refetch, and the loading of this collection through a relation.\n * A restore is an ordinary update setting the field back to `null`. A real\n * `DELETE` is still available as `delete(…, { hard: true })` / `?hard=true`,\n * and needs exactly the same permission an ordinary delete does — it is the\n * same operation, and gating it separately would be a second access-control\n * surface for one verb.\n *\n * `true` uses `deletedAt` (column `deleted_at`). The object form renames the\n * field. **Either way the collection must declare that property itself**, as\n * a `date` — this flag says what a column *means*, it does not conjure the\n * column into existence. A config that turns it on without the property is\n * refused at boot rather than at the first delete, because the failure would\n * otherwise land on a caller trying to remove a row.\n *\n * The hooks do not change: `beforeDelete` can still veto and `afterDelete`\n * still fires. From the application's point of view the row was deleted;\n * how the table records that is this flag's business.\n *\n * Postgres-only, like {@link SearchConfig}.\n */\n softDelete?: boolean | {\n /**\n * The `date` property that records the deletion. Defaults to\n * `deletedAt`.\n */\n field?: string;\n };\n\n /**\n * Scope every row of this collection to a tenant.\n *\n * One declaration replaces the four hand-written pieces a tenant-scoped\n * table used to need — the `NOT NULL` column, the RLS rule, the value\n * stamped on insert, and the index — and keeps them in agreement, because\n * they are all derived from this.\n *\n * ```ts\n * tenant: { field: \"orgId\", from: { claim: \"org_id\" } }\n * ```\n *\n * The property must already be declared: this says what a column *means*,\n * it does not create one. Postgres-only, like {@link SearchConfig} — RLS is\n * what enforces the boundary.\n *\n * @see CollectionTenantConfig\n */\n tenant?: CollectionTenantConfig<M>;\n}\n\n/**\n * A collection backed by Firebase / Firestore.\n * Adds support for subcollections (nested document collections).\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only Firestore-relevant fields appear.\n *\n * @group Models\n */\nexport interface FirebaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n /**\n * The database engine for this collection. Must be set to `\"firestore\"`.\n */\n engine: \"firestore\";\n\n /**\n * Set of properties that compose a entity.\n * Firestore collections support `reference` properties but not `relation`.\n */\n properties: FirebaseProperties;\n\n /**\n * The Firestore collection path to query. Defaults to `slug` if not set.\n * Use this when the Firestore path differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const fsCustomer: FirebaseCollectionConfig = {\n * slug: \"fs_customer\", // URL: /c/fs_customer\n * path: \"customer\", // Firestore path: customer\n * name: \"Customers (Firestore)\",\n * engine: \"firestore\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n\n /**\n * You can add subcollections to your entity in the same way you define the root\n * collections. The collections added here will be displayed when opening\n * the side dialog of a entity.\n */\n subcollections?: () => CollectionConfig<Record<string, unknown>>[];\n}\n\n/**\n * A collection backed by MongoDB.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only MongoDB-relevant fields appear.\n *\n * @group Models\n */\nexport interface MongoDBCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n\n /**\n * The database engine for this collection. Must be set to `\"mongodb\"`.\n */\n engine: \"mongodb\";\n\n /**\n * Set of properties that compose a entity.\n * MongoDB collections support `reference` properties but not `relation`.\n */\n properties: MongoProperties;\n\n /**\n * The MongoDB collection name to use. Defaults to `slug` if not set.\n * Use this when the MongoDB collection name differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const mongoCustomer: MongoDBCollectionConfig = {\n * slug: \"mongo_customer\", // URL: /c/mongo_customer\n * path: \"customer\", // MongoDB collection: customer\n * name: \"Customers (MongoDB)\",\n * engine: \"mongodb\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n}\n\n/**\n * A collection backed by any data source.\n * This is a discriminated union — use {@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, or {@link MongoDBCollectionConfig} for\n * driver-specific type safety.\n *\n * @group Models\n */\nexport type CollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> =\n | PostgresCollectionConfig<M, USER>\n | FirebaseCollectionConfig<M, USER>\n | MongoDBCollectionConfig<M, USER>;\n\n/**\n * A collection of *any* row type.\n *\n * `CollectionConfig` is **invariant** in `M`: `callbacks` both consumes `M`\n * (`AfterReadProps<M>`) and produces it, so neither direction of assignment\n * holds. `CollectionConfig<SomeRow>` is therefore not assignable to a bare\n * `CollectionConfig`, whose `M` defaults to `Record<string, unknown>`.\n *\n * That matters wherever a collection is merely *referred to* rather than read\n * from. `defineCollection` returns a config whose `M` is inferred from the\n * properties — the whole point of it — so a field typed `() => CollectionConfig`\n * rejects every collection the builder produces, and `target: () => otherCollection`\n * (the documented way to point a relation at its other end) does not compile in\n * any project that uses the builder.\n *\n * `any` is deliberate and is what it is for here: these positions never read the\n * target's rows, they only identify which collection is meant, so there is no\n * type safety to preserve and invariance is pure obstruction.\n *\n * @group Models\n */\nexport type AnyCollectionConfig = CollectionConfig<any, any>;\n\n/**\n * Type guard for PostgreSQL collections.\n * Returns true if the collection uses the Postgres engine (or the default engine).\n *\n * Generic over the *input* type, and narrows by intersection rather than\n * replacement. Narrowing to a bare `PostgresCollectionConfig` discarded whatever\n * the caller actually had — most visibly the admin panel's view model, whose\n * flattened presentation fields vanished the moment a collection passed through\n * one of these guards.\n *\n * @group Models\n */\nexport function isPostgresCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return !collection.engine || collection.engine === \"postgres\";\n}\n\n/**\n * Narrows to the SQL collection fields — `table`, `relations`,\n * `disableDefaultPolicies` — by asking the engine's declared capabilities\n * rather than by naming Postgres.\n *\n * The two halves of this already existed and were never joined. The engine\n * split (`PostgresCollectionConfig` / `FirebaseCollectionConfig` /\n * `MongoDBCollectionConfig`) said which fields belong to which engine at the\n * type level; {@link DataSourceCapabilities} said the same thing at runtime,\n * down to a `supportsRelations` flag. So call sites guarded on the capability\n * and then read a field the base type had to declare for them — which is why\n * those fields were on the base, and why a MongoDB collection could be written\n * with a `table`.\n *\n * Prefer this over {@link isPostgresCollectionConfig} wherever the question is\n * \"does this collection live in a SQL table\", so a custom SQL engine\n * registered through `registerDataSourceCapabilities` is included.\n *\n * @group Models\n */\nexport function isRelationalCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return getDataSourceCapabilities(collection.engine).supportsRelations;\n}\n\n/**\n * Type guard for Firebase / Firestore collections.\n * @group Models\n */\nexport function isFirebaseCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & FirebaseCollectionConfig<any, any> {\n return collection.engine === \"firestore\";\n}\n\n/**\n * Type guard for MongoDB collections.\n * @group Models\n */\nexport function isMongoDBCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & MongoDBCollectionConfig<any, any> {\n return collection.engine === \"mongodb\";\n}\n\n/**\n * Returns the data path for a collection.\n * For Firestore or MongoDB collections with a `path`, returns that value;\n * otherwise falls back to `slug`.\n */\nexport function getCollectionDataPath<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): string {\n if (isFirebaseCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n if (isMongoDBCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n return collection.slug;\n}\n\n/**\n * Reads a collection's driver-declared subcollections thunk (the `subcollections`\n * field) independent of engine identity, so engine-agnostic code doesn't have to\n * type-guard against a specific driver. Returns `undefined` when the collection\n * declares none.\n *\n * Pair with `getDataSourceCapabilities(engine).supportsSubcollections` to decide\n * whether the engine honours subcollections at all before reading them.\n * @group Models\n */\nexport function getDeclaredSubcollections<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): (() => CollectionConfig<Record<string, unknown>>[]) | undefined {\n return (collection as FirebaseCollectionConfig<M, USER>).subcollections;\n}\n\n/**\n * Where the rows in an {@link EntityChildView} come from.\n *\n * The two are not the same thing, and conflating them is what made a Postgres\n * relation borrow Firestore's addressing:\n *\n * - `subcollection` is **containment**. The rows live under the parent; the\n * path is their identity, and they cannot exist without it. This is what\n * Firestore has natively.\n * - `relation` is a **link**. The rows are an ordinary collection, narrowed to\n * those the parent reaches. `owned` means the child carries the parent's\n * foreign key and belongs to it alone; `linked` means the row is shared\n * through a junction, so what the parent controls is the link, not the row.\n *\n * @group Models\n */\nexport type ChildViewSource =\n | { kind: \"subcollection\" }\n | {\n kind: \"relation\";\n relationKey: string;\n mode: \"owned\" | \"linked\";\n /**\n * Slug of the collection the rows actually live in.\n *\n * Distinct from the view's `key`, which is the relation. A `linked` view\n * needs both: the key addresses the parent's set, and this addresses the\n * whole collection to pick an existing row out of.\n */\n targetSlug: string;\n };\n\n/**\n * A list of rows rendered inside an entity view — the tab under a record.\n *\n * This is a *presentation* descriptor, which is the whole point: rendering a\n * related list as a tab used to require minting a child `CollectionConfig` with\n * its own slug, which dragged a URL grammar, a path resolver and a second\n * read/write pipeline along with it. A tab needs a key, a collection to list,\n * and to know where its rows come from.\n *\n * @group Models\n */\nexport interface EntityChildView<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Stable identifier for this view: the tab id and the path segment.\n *\n * For a relation this is the **relation key** — the name the backend\n * resolves a nested path segment by — not the target collection's slug.\n * Those differ whenever a relation is named, which is every inline relation\n * property, and the mismatch is why such a tab used to open onto an error.\n */\n key: string;\n\n /** The collection whose rows this view lists, with any overrides applied. */\n collection: CollectionConfig<M>;\n\n source: ChildViewSource;\n}\n\n\nexport type { WhereFilterOp, FilterValues, WireFilterValues, FilterPreset } from \"./filter-operators\";\n\n\nexport type InferCollectionConfigType<S extends CollectionConfig> = S extends CollectionConfig<infer M> ? M : never;\n\n/**\n * Configuration for authentication collections.\n *\n * Controls what happens when admins create users, reset passwords,\n * and which entity actions are auto-injected.\n *\n * Use `auth: true` as sugar for `{ enabled: true }` with all defaults.\n *\n * @example Override user creation\n * ```ts\n * auth: {\n * enabled: true,\n * onCreateUser: async (values, ctx) => {\n * const hash = await ctx.hashPassword(\"welcome123\");\n * return {\n * values: { ...values, passwordHash: hash, emailVerified: true },\n * temporaryPassword: \"welcome123\",\n * };\n * },\n * }\n * ```\n *\n * @example Disable the reset-password entity action\n * ```ts\n * auth: {\n * enabled: true,\n * actions: { resetPassword: false },\n * }\n * ```\n *\n * @group Models\n */\nexport interface AuthCollectionConfig {\n /** Set to true to mark this collection as the authentication collection. */\n enabled: boolean;\n\n /**\n * Called when an admin creates a user via the collection REST API.\n *\n * Default: generate password → hash → normalize email → save →\n * send invitation email (or return temp password if no email configured).\n *\n * Override to implement custom invitation flows, LDAP sync, etc.\n */\n onCreateUser?: (\n values: Record<string, unknown>,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionCreateResult>;\n\n /**\n * Called when an admin resets a user's password via the admin panel.\n *\n * Default: generate reset token → send email (or generate + return temp password).\n * Override for custom reset flows.\n */\n onResetPassword?: (\n uid: string,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionResetResult>;\n\n /**\n * Control which auth-specific entity actions are auto-injected.\n *\n * Default: `{ resetPassword: true }` — the framework auto-injects\n * the built-in `resetPasswordAction` into the collection's entity actions.\n *\n * Set to `false` to disable, or pass a custom `EntityAction` to replace the UI.\n *\n * The object form is an `EntityAction` from `@rebasepro/cms-types`, typed\n * here as `object` because it is a React component with admin controllers in\n * its props and nothing on the server reads it — only whether the built-in\n * action is injected, which is the boolean.\n */\n actions?: {\n resetPassword?: boolean | object;\n };\n}\n\n/**\n * Context provided to collection-level auth hooks.\n *\n * This is a simplified facade over the server internals —\n * it exposes only what's needed for custom auth flows without\n * coupling collection config to internal interfaces.\n *\n * @group Models\n */\nexport interface AuthCollectionContext {\n /** Hash a password using the configured algorithm (scrypt by default). */\n hashPassword: (password: string) => Promise<string>;\n /**\n * Send an email. Only available when email service is configured.\n *\n * Resolves with what the provider reported — the assigned Message-ID, most\n * usefully — so a hook that sends a message can store the id and later\n * thread a reply back to it. Callers that do not care may ignore it.\n */\n sendEmail?: (options: { to: string; subject: string; html: string; text?: string }) => Promise<EmailSendResult>;\n /** Whether the email service is configured and available. */\n emailConfigured: boolean;\n /** The app name from email config (for templates). */\n appName: string;\n /** The base URL for password reset links. */\n resetPasswordUrl: string;\n}\n\n/**\n * Result of a collection-level `onCreateUser` hook.\n * @group Models\n */\nexport interface AuthCollectionCreateResult {\n /** Processed values to persist (must include passwordHash, NOT raw password). */\n values: Record<string, unknown>;\n /** If set, shown to the admin in the creation result dialog. */\n temporaryPassword?: string;\n /** Whether an invitation email was sent. */\n invitationSent?: boolean;\n}\n\n/**\n * Result of a collection-level `onResetPassword` hook.\n * @group Models\n */\nexport interface AuthCollectionResetResult {\n /** If set, shown to the admin. */\n temporaryPassword?: string;\n /** Whether a reset email was sent. */\n invitationSent?: boolean;\n}\n","/**\n * Opt-in full-text search configuration.\n *\n * ## Why this is opt-in\n *\n * Without a `search` block, `.search()` behaves exactly as it always has: an\n * `ILIKE '%term%'` OR-ed across the collection's top-level, non-enum `string`\n * properties. That default is unchanged and will stay unchanged — declaring\n * this block is the only way to get anything else.\n *\n * The default has three limits that no amount of tuning inside it can fix:\n * it cannot reach inside `map` (JSONB) or `array` properties, it has no notion\n * of relevance, and a leading `%` means it can never use an index. Collections\n * that outgrow those limits declare what they want searched; collections that\n * have not are left completely alone.\n *\n * ## What declaring it does\n *\n * One `tsvector` column, `GENERATED ALWAYS AS … STORED`, plus one GIN index on\n * it. Postgres recomputes the column on every write of a source field, so it\n * cannot drift from the row, and refuses any attempt to write it directly.\n * `.search()` then compiles to `@@ websearch_to_tsquery(…)` against that\n * column, which stems, drops stopwords, AND-es the terms, and ranks.\n *\n * These are stated consequences, not hidden ones: the column and the index\n * appear in generated DDL, in `schema.generated.ts`, and in `rebase db push`\n * output like any other declared object.\n *\n * @example\n * ```ts\n * const talents: PostgresCollectionConfig = {\n * slug: \"talents\",\n * table: \"talents\",\n * properties: { … },\n * search: {\n * language: \"spanish\",\n * unaccent: true,\n * fields: [\n * { path: \"full_name\", weight: \"A\" },\n * \"location\",\n * \"questionnaire.certifications\" // into the JSONB\n * ]\n * }\n * };\n * ```\n *\n * @group Search\n */\nexport interface SearchConfig {\n /**\n * The fields to index, in the author's own words. Nothing is inferred: a\n * field is searched if and only if it is named here.\n *\n * A bare string is shorthand for `{ path, weight: \"B\" }`.\n *\n * A path may address:\n * - a top-level `string` property — `\"full_name\"`\n * - a `string[]` property — `\"tags\"` (every element is indexed)\n * - a path into a `map` property — `\"questionnaire.certifications\"`,\n * which indexes every string found at or below that point, including\n * nested objects and arrays of strings. JSON *keys* are never indexed,\n * only values.\n *\n * A path that does not resolve to one of those is a boot-time error, not\n * a silent omission — a search field you believe is live and is not is the\n * failure this whole block exists to prevent.\n */\n fields: readonly (string | SearchField)[];\n\n /**\n * The Postgres text search configuration, which decides stemming and\n * stopwords. `\"spanish\"` stems `auditores` to `auditor` and drops `de`;\n * `\"simple\"` does neither.\n *\n * Defaults to `\"simple\"`, which is the only choice that is never wrong:\n * a stemmer applied to the wrong language silently mangles lexemes. Set it\n * to your content's language to get stemming.\n *\n * @default \"simple\"\n */\n language?: string;\n\n /**\n * Fold accents before indexing, so `auditoria` matches `auditoría`.\n *\n * This is not cosmetic in accented languages. Postgres stems the two\n * spellings to *different* lexemes — `to_tsvector('spanish', 'auditoría')`\n * yields `auditor` while `'auditoria'` yields `auditori` — so without this\n * a query typed without accents misses the rows that carry them, which is\n * most queries most users type.\n *\n * Requires the `unaccent` extension. Boot fails with an explicit message if\n * it is not installed and cannot be created, rather than quietly indexing\n * accented text as-is.\n *\n * @default false\n */\n unaccent?: boolean;\n\n /**\n * Name of the generated column holding the `tsvector`.\n *\n * Only change this if `search_vector` collides with a column you already\n * have. It is part of your schema once created: renaming it later is a\n * column drop and recreate, which rewrites the table.\n *\n * @default \"search_vector\"\n */\n column?: string;\n\n /**\n * Also match on trigram similarity, so near-misses and typos still rank —\n * `iso14000` reaching `ISO 14001`, which no amount of stemming will do\n * because they are simply different lexemes.\n *\n * Adds a second generated `text` column and a GIN trigram index alongside\n * the `tsvector`, and requires the `pg_trgm` extension. Costs write time\n * and disk; buys the single most common class of failed search.\n *\n * Also changes what `_score` means: the trigram similarity is added to\n * `ts_rank`. It has to be. A typo matches nothing on the exact path, so\n * every row this finds has a `ts_rank` of zero — ranking by that alone\n * would order the results arbitrarily, which is the failure `fuzzy` exists\n * to fix.\n *\n * @default false\n */\n fuzzy?: boolean;\n\n /**\n * Similarity floor for {@link SearchConfig.fuzzy}, between 0 and 1. A row\n * whose trigram similarity to the query falls below this never matches on\n * the fuzzy path (it can still match on the exact one).\n *\n * Lower admits more typos and more noise. Ignored unless `fuzzy` is set.\n *\n * @default 0.3\n */\n fuzzyThreshold?: number;\n}\n\n/**\n * One indexed field, with the weight it carries in the ranking.\n *\n * @group Search\n */\nexport interface SearchField {\n /**\n * Property name, or dotted path into a `map` property.\n * @see SearchConfig.fields\n */\n path: string;\n\n /**\n * Postgres weight class. `ts_rank` scores an `A` hit far above a `D` hit,\n * which is how a name outranks a passing mention in a long description.\n *\n * The four classes are Postgres's own and there are exactly four.\n *\n * @default \"B\"\n */\n weight?: SearchWeight;\n}\n\n/**\n * Postgres tsvector weight classes, strongest to weakest.\n *\n * @group Search\n */\nexport type SearchWeight = \"A\" | \"B\" | \"C\" | \"D\";\n\n/** The column name used when {@link SearchConfig.column} is not given. */\nexport const DEFAULT_SEARCH_COLUMN = \"search_vector\";\n\n/** The text search configuration used when {@link SearchConfig.language} is not given. */\nexport const DEFAULT_SEARCH_LANGUAGE = \"simple\";\n\n/** The weight a field carries when it does not name one. */\nexport const DEFAULT_SEARCH_WEIGHT: SearchWeight = \"B\";\n\n/** The similarity floor used when {@link SearchConfig.fuzzyThreshold} is not given. */\nexport const DEFAULT_FUZZY_THRESHOLD = 0.3;\n\n/**\n * Sort keys a query computes rather than reads from a column.\n *\n * `orderBy` is otherwise typed against the row — `keyof M` — which is exactly\n * right for a column and exactly wrong for relevance: `_score` is produced by\n * the query, so it appears in no generated row type and a project with a\n * generated SDK could not name it. The runtime accepted it, the docs told\n * people to use it, and the types rejected it.\n *\n * Kept as a named union rather than a loose `string` so the other half of the\n * guarantee survives: a typo'd column is still a compile error, and remains a\n * 400 at runtime rather than a silently unsorted list.\n *\n * `_distance` is deliberately not here. A vector search orders by distance on\n * its own and overrides `orderBy` outright, so naming it would imply a choice\n * the caller does not have.\n *\n * @group Search\n */\nexport type ComputedSortField = typeof RELEVANCE_SORT_FIELD;\n\n/**\n * The relevance sort key. Valid only on a collection that declares a\n * {@link SearchConfig} *and* on a query that carries a search string; anywhere\n * else it is an unknown field and the request is refused.\n */\nexport const RELEVANCE_SORT_FIELD = \"_score\";\n\n/**\n * One field that matched, and the text around the hit.\n *\n * Returned per row as `_matches` when a query asks for it — see the `explain`\n * option on `.search()`. Answers the question a ranked list otherwise leaves\n * open: *why is this row here?* A candidate surfacing for \"iso 14001\" because\n * of a certification is a different result from one surfacing because the\n * string appears in a paragraph about something else, and the score alone\n * cannot tell them apart.\n *\n * @group Search\n */\nexport interface SearchMatch {\n /**\n * The declared field path that matched, exactly as written in\n * {@link SearchConfig.fields} — e.g. `\"questionnaire.certifications\"`.\n * Map it to a label for display; the path is stable, a label is yours.\n */\n field: string;\n\n /**\n * The matching text, with each hit wrapped in `<mark>…</mark>`.\n *\n * Built by Postgres's `ts_headline` over the same normalized text that was\n * indexed. With {@link SearchConfig.unaccent} on that means the snippet\n * reads with accents folded — `Auditoria` rather than `Auditoría`. That is\n * deliberate: `ts_headline` over the *original* text cannot find a hit the\n * unaccented query produced, so it returns the text with nothing marked at\n * all. A readable snippet that highlights beats a prettier one that\n * silently does not.\n *\n * Contains markup by construction. Render it as HTML or strip the tags —\n * do not display it raw, and do not trust it as plain text.\n */\n snippet: string;\n}\n","import type { AnyCollectionConfig } from \"./collections\";\nimport type { Properties } from \"./properties\";\n\n/**\n * @group Models\n */\nexport type OnAction = \"cascade\" | \"restrict\" | \"no action\" | \"set null\" | \"set default\";\n\n/**\n * The key a junction row's own columns are carried under, in both directions.\n *\n * A read that includes a `manyToMany` relation serves each related row with its\n * link's columns nested here — `{ id: 5, name: \"ts\", _pivot: { role: \"owner\" } }`\n * — and a membership write may name the same key on an element to state what\n * the link should hold. One constant because the two have to be the same word:\n * a wire name that differs between the read and the write it round-trips\n * through is a shape no client can echo back.\n *\n * Leading underscore, like `_matches`: it reads as metadata about the row\n * rather than as one of its columns. A payload property may not be named\n * `_pivot` either — `checkJunctionPayload` refuses it — so the key means one\n * thing wherever it appears.\n *\n * @group Models\n */\nexport const JUNCTION_PIVOT_KEY = \"_pivot\";\n\n/**\n * What kind of link a relation is.\n *\n * The discriminant. Every other field a relation carries belongs to exactly one\n * of these, which is the point: a relation used to be a single open interface\n * where `cardinality`, `direction`, `localKey`, `foreignKeyOnTarget`, `through`\n * and `joinPath` were all optional and any combination typechecked. Which link\n * you meant then had to be *inferred* from which fields you happened to set,\n * and the inference was ~200 lines that guessed, fell back on naming\n * conventions, and swallowed its own failures.\n *\n * Most of that guessing produced bugs rather than convenience. A `many`\n * relation carrying a `localKey` — a combination the old type permitted — made\n * the write path stamp the parent's own foreign key onto the child row. Under\n * these kinds that state cannot be written down.\n *\n * @group Models\n */\nexport type RelationKind = \"belongsTo\" | \"hasOne\" | \"hasMany\" | \"manyToMany\" | \"via\";\n\n/** Fields every relation carries, whatever its kind. @group Models */\nexport interface RelationBase {\n /**\n * The name this link is addressed by: the key in `include`, the tab in the\n * admin panel, and the path segment of a nested URL.\n *\n * Defaults to the declaring property's key, or to the target's slug for an\n * entry in `relations`.\n */\n relationName?: string;\n\n /** The collection on the other end. */\n target: () => AnyCollectionConfig;\n\n /**\n * What the database does to this side's foreign key when the target row's\n * key changes. Emitted as the constraint's `ON UPDATE`.\n *\n * Unset means no clause, which Postgres reads as `NO ACTION`. Set\n * `\"cascade\"` when the target's key is a natural key that can be edited —\n * a slug, a SKU — so the pointers follow it.\n *\n * Only a `belongsTo` puts the key on this table, so this is the only kind\n * where the clause is written here; on the other kinds it describes the\n * constraint the target's own column carries.\n */\n onUpdate?: OnAction;\n /**\n * What the database does to this side's rows when the target row is\n * deleted. Emitted as the constraint's `ON DELETE`.\n *\n * Defaults, when unset, to `\"set null\"` for an optional relation and\n * **`\"restrict\"`** for a required one. `NOT NULL` says a child cannot exist\n * without a parent; it does not say deleting the parent should delete the\n * child. Ask for `\"cascade\"` when that is what you mean — it is the one\n * value that destroys rows you did not name.\n *\n * A `manyToMany` is the exception: its junction rows default to\n * `\"cascade\"`, because the row deleted there is the link and not the target.\n */\n onDelete?: OnAction;\n\n /**\n * Presentation overrides applied when this relation is rendered as a tab.\n *\n * Whether the link is *required* is not here: it is\n * `validation: { required: true }` on the declaring property, the same key\n * every other field uses. A relation carried its own copy until 0.18, and\n * the two disagreed by construction — the DDL generator read the property\n * (so the column was `NOT NULL`) while codegen read the relation (so the\n * generated `Insert` type made it optional), and a `create()` that\n * typechecked failed at the database.\n */\n overrides?: Partial<AnyCollectionConfig>;\n}\n\n/**\n * This collection holds the foreign key. One target row per source row.\n *\n * ```ts\n * author: { kind: \"belongsTo\", target: () => authors, localKey: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface BelongsToRelation extends RelationBase {\n kind: \"belongsTo\";\n /**\n * Column on **this** collection's table holding the target's key.\n * Defaults to `<relationName>_id`.\n */\n localKey?: string;\n}\n\n/**\n * The target holds the foreign key, and at most one target row points back.\n *\n * ```ts\n * profile: { kind: \"hasOne\", target: () => profiles, foreignKeyOnTarget: \"user_id\" }\n * ```\n * @group Models\n */\nexport interface HasOneRelation extends RelationBase {\n kind: \"hasOne\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * Set it when the two sides are joined on a natural key rather than on the\n * row id — an external identity id, a SKU, a tenant slug. See\n * {@link HasManyRelation.sourceKey}, which this mirrors.\n */\n sourceKey?: string;\n}\n\n/**\n * The target holds the foreign key, and many target rows point back. The\n * children belong to this parent alone — deleting one deletes a row.\n *\n * ```ts\n * posts: { kind: \"hasMany\", target: () => posts, foreignKeyOnTarget: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface HasManyRelation extends RelationBase {\n kind: \"hasMany\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * The mirror of `localKey` on {@link BelongsToRelation}: that one names the\n * column this side reads from, this one names the column the other side\n * points at. Without it the pair can only be joined on the row id, which\n * makes a natural-key link — `auth_user_id ↔ auth_user_id`, a SKU, a tenant\n * slug — inexpressible as `hasMany`, and it has to drop to the read-only\n * `via`.\n *\n * The column must be unique: the link addresses one source row per value,\n * and Postgres will not accept a foreign key against a non-unique column.\n *\n * ```ts\n * applications: {\n * kind: \"hasMany\",\n * target: () => talentApplications,\n * sourceKey: \"auth_user_id\",\n * foreignKeyOnTarget: \"auth_user_id\"\n * }\n * ```\n */\n sourceKey?: string;\n}\n\n/**\n * Both sides hold many, through a junction table. The target rows are shared,\n * so this collection owns the *link* and not the row: removing one removes a\n * junction row and leaves the target alone.\n *\n * Declared the same way from either side — there is no owning and inverse\n * version. Swap `sourceColumn` and `targetColumn` to describe the other\n * direction.\n *\n * ```ts\n * tags: { kind: \"manyToMany\", target: () => tags }\n * ```\n * @group Models\n */\nexport interface ManyToManyRelation extends RelationBase {\n kind: \"manyToMany\";\n /**\n * The junction table and its two key columns. Every part defaults: the\n * table to both table names sorted and joined, the columns to\n * `<collection>_id` and `<relationName>_id`.\n */\n through?: {\n table?: string;\n /** Junction column holding **this** collection's key. */\n sourceColumn?: string;\n /** Junction column holding the **target's** key. */\n targetColumn?: string;\n /**\n * Extra columns the junction row carries, declared exactly like a\n * collection's properties.\n *\n * A membership is often not only a membership. \"This user is in that\n * organisation\" is really \"…as an `owner`, since March\"; \"this tag is\n * on that post\" is really \"…in third place\". Until this existed the\n * junction was two key columns and nothing else, so the role and the\n * position had to become a collection of their own — which is a\n * different data model, a different set of policies and a different\n * URL, for what is still one link.\n *\n * The properties are read by the same planner that reads a\n * collection's, so a payload column gets the type, `NOT NULL`,\n * `DEFAULT`, `UNIQUE` and enum type it would get on a table. What it\n * does **not** get is `indexes` (declared per collection, and no\n * collection declares a junction), `search`, `vector`, or anything a\n * relation would put on it — a payload property may not be a\n * `relation`, a `reference` or a `vector`, and config validation\n * refuses one that is.\n *\n * On the wire the values travel under {@link JUNCTION_PIVOT_KEY}: a\n * read serves `{ …target, _pivot: { role } }`, and a membership write\n * accepts `{ id, _pivot: { role } }` beside the bare ids.\n *\n * ```ts\n * members: {\n * kind: \"manyToMany\",\n * target: () => users,\n * through: {\n * table: \"org_members\",\n * properties: {\n * role: { type: \"string\", enum: [\"owner\", \"admin\", \"member\"],\n * defaultValue: \"member\", validation: { required: true } },\n * joinedAt: { type: \"date\", autoValue: \"on_create\" }\n * }\n * }\n * }\n * ```\n *\n * Both sides of the same junction may declare it, and both must agree:\n * `resolveJunctionSpecs` refuses two declarations of the same payload\n * key that do not describe the same column, because only one of them\n * could ever be created.\n */\n properties?: Properties;\n };\n}\n\n/**\n * An explicit chain of joins, for links the four shapes above cannot express:\n * multi-hop paths, composite keys, or a join whose condition is not a plain\n * foreign key.\n *\n * Read-only. Rebase will not infer how to write through an arbitrary join\n * chain, and guessing is what this type exists to stop.\n *\n * ```ts\n * permissions: {\n * kind: \"via\",\n * target: () => permissions,\n * cardinality: \"many\",\n * joinPath: [\n * { table: \"user_roles\", on: { from: \"id\", to: \"user_id\" } },\n * { table: \"role_permissions\", on: { from: \"role_id\", to: \"role_id\" } },\n * { table: \"permissions\", on: { from: \"permission_id\", to: \"id\" } }\n * ]\n * }\n * ```\n * @group Models\n */\nexport interface ViaRelation extends RelationBase {\n kind: \"via\";\n /** Whether the chain yields one row or many. Cannot be derived from a join chain. */\n cardinality: \"one\" | \"many\";\n /**\n * The joins, in order, from this collection's table to the target's.\n *\n * Each step names a table and the columns to join it on; the last step's\n * table is the target. Read-only, because Rebase will not work out how to\n * write through an arbitrary chain, and guessing is what this kind exists to\n * stop.\n */\n joinPath: JoinStep[];\n}\n\n/**\n * A link from one collection to another, as authored.\n *\n * A closed union: pick the kind that describes the link and the type offers\n * exactly the fields that kind needs. See {@link ResolvedRelation} for the form\n * the runtime works with, which has every default filled in.\n *\n * @group Models\n */\nexport type Relation =\n | BelongsToRelation\n | HasOneRelation\n | HasManyRelation\n | ManyToManyRelation\n | ViaRelation;\n\n/**\n * A relation with every default filled in — the form the runtime works with.\n *\n * The authored {@link Relation} and this are deliberately different types.\n * They used to be one, which meant no reader could tell which fields had been\n * supplied and which had been guessed, and so every consumer re-derived what it\n * needed with its own chain of `if (through) … else if (localKey) …` fallbacks.\n * Those chains disagreed with each other; that disagreement is what produced\n * silently wrong reads and corrupt writes.\n *\n * Here each variant carries exactly its own fields, all required. A consumer\n * switches on `kind` and gets what it needs without a fallback, and the cases\n * it forgot are a compile error rather than a wrong answer at runtime.\n *\n * @group Models\n */\nexport type ResolvedRelation =\n | ResolvedBelongsTo\n | ResolvedHasOne\n | ResolvedHasMany\n | ResolvedManyToMany\n | ResolvedVia;\n\n/** Fields present on every resolved relation. @group Models */\nexport interface ResolvedRelationBase {\n /** Always set: defaulted during resolution if the author omitted it. */\n relationName: string;\n /**\n * The collection on the other end.\n *\n * Still a thunk — a relation between two collections that import each other\n * has to be — but normalised: resolution unwraps the module namespace the\n * author's `() => import(…)` may hand back, so every consumer gets the\n * config and not a `{ default: … }` wrapper.\n */\n target: () => AnyCollectionConfig;\n /** The target's slug, resolved once so consumers need not call `target()`. */\n targetSlug: string;\n /** As authored — see {@link RelationBase.onUpdate}. Defaults are not filled in. */\n onUpdate?: OnAction;\n /**\n * As authored — see {@link RelationBase.onDelete}. `undefined` here means\n * the author said nothing, and the DDL generator picks the default; it does\n * **not** mean \"no action\".\n */\n onDelete?: OnAction;\n /** Presentation overrides applied when this relation is rendered as a tab. */\n overrides?: Partial<AnyCollectionConfig>;\n /**\n * Whether one row or many come back. Derived from `kind` — kept because it\n * is what most consumers actually branch on, and because `via` is the one\n * kind where it is authored rather than implied.\n */\n cardinality: \"one\" | \"many\";\n /**\n * Whether Rebase knows how to write through this link. False only for\n * {@link ResolvedVia}, whose join chain it will not invent a write for.\n */\n writable: boolean;\n /**\n * Whether the target rows are shared with other parents. True for\n * many-to-many and for multi-hop `via`: what the parent owns is the link,\n * so removing one must not delete the row.\n */\n shared: boolean;\n}\n\n/** @group Models */\nexport interface ResolvedBelongsTo extends ResolvedRelationBase {\n kind: \"belongsTo\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on this collection's table. */\n localKey: string;\n}\n\n/** @group Models */\nexport interface ResolvedHasOne extends ResolvedRelationBase {\n kind: \"hasOne\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /** @see ResolvedHasMany.sourceKey */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedHasMany extends ResolvedRelationBase {\n kind: \"hasMany\";\n cardinality: \"many\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /**\n * Column on the source's table that `foreignKeyOnTarget` points at, or\n * `undefined` for the source's primary key.\n *\n * The one optional field on a resolved relation, and deliberately so. Every\n * other default is filled in here because it can be: a table name and a\n * column name are derivable from the relation and its two endpoints alone.\n * The primary key is not — this driver resolves it from `isId`, then the\n * Drizzle schema, then a column named `id`, and the middle tier does not\n * exist at resolution time.\n *\n * So `undefined` is a sentinel with exactly one meaning, not a field a\n * consumer is invited to guess at. Read it through `sourceKeyField()`,\n * which is the only place that turns it into a column name.\n */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedManyToMany extends ResolvedRelationBase {\n kind: \"manyToMany\";\n cardinality: \"many\";\n writable: true;\n shared: true;\n /**\n * The junction table and its two key columns, with every default filled in:\n * the table from both table names sorted and joined, the columns from each\n * endpoint's slug.\n */\n through: {\n table: string;\n sourceColumn: string;\n targetColumn: string;\n /**\n * The payload columns as authored, or `{}` when there are none —\n * never `undefined`, so a consumer reads one shape.\n * See {@link ManyToManyRelation.through}.\n */\n properties: Properties;\n };\n}\n\n/** @group Models */\nexport interface ResolvedVia extends ResolvedRelationBase {\n kind: \"via\";\n writable: false;\n /** The chain as authored — see {@link ViaRelation.joinPath}. Nothing to default. */\n joinPath: JoinStep[];\n}\n\n// ── Narrowing helpers ────────────────────────────────────────────────\n//\n// Consumers that only care about one axis — \"does this list many rows\",\n// \"is there a column on the target\" — should ask that question rather\n// than enumerate kinds, so adding a kind later does not silently skip them.\n\n/** Relations whose target row carries this collection's key. @group Models */\nexport type ResolvedForeignKeyOnTarget = ResolvedHasOne | ResolvedHasMany;\n\n/** @group Models */\nexport function hasForeignKeyOnTarget(relation: ResolvedRelation): relation is ResolvedForeignKeyOnTarget {\n return relation.kind === \"hasOne\" || relation.kind === \"hasMany\";\n}\n\n/** @group Models */\nexport function isManyToMany(relation: ResolvedRelation): relation is ResolvedManyToMany {\n return relation.kind === \"manyToMany\";\n}\n\n/** @group Models */\nexport function isToMany(relation: ResolvedRelation): boolean {\n return relation.cardinality === \"many\";\n}\n\n/**\n * Defines a single, explicit step in a multi-join path.\n *\n * Each step represents one JOIN operation in the sequence. The `from` columns\n * refer to the previous table in the chain (or the source table for the first step),\n * and the `to` columns refer to the current table being joined.\n *\n * @example Single column join:\n * ```typescript\n * {\n * table: \"authors\",\n * on: {\n * from: \"author_id\", // Column from previous table (e.g., posts.author_id)\n * to: \"id\" // Column from current table (authors.id)\n * }\n * }\n * ```\n *\n * @example Multi-column composite key join:\n * ```typescript\n * {\n * table: \"order_items\",\n * on: {\n * from: [\"order_id\", \"store_id\"], // Multiple columns from previous table\n * to: [\"order_id\", \"store_id\"] // Corresponding columns in current table\n * }\n * }\n * ```\n */\nexport interface JoinStep {\n /**\n * The database table name to join TO in this step.\n * This is the table you're joining into, not the table you're joining from.\n *\n * @example \"authors\", \"user_roles\", \"product_categories\"\n */\n table: string;\n\n /**\n * The join condition for this step. Defines how the previous table\n * connects to the current table.\n *\n * - `from`: Column name(s) on the PREVIOUS table in the join chain\n * - `to`: Column name(s) on the CURRENT table (specified in `table`)\n *\n * For the first step, `from` refers to the source collection's table.\n * For subsequent steps, `from` refers to the table from the previous step.\n *\n * Both `from` and `to` support:\n * - Single column: `\"user_id\"`\n * - Multiple columns: `[\"company_id\", \"region_id\"]` for composite keys\n *\n * When using arrays, both `from` and `to` must have the same length,\n * and columns are matched by position (index 0 with index 0, etc.).\n */\n on: {\n from: string | string[];\n to: string | string[];\n };\n}\n","/**\n * Structured, engine-agnostic policy expressions.\n *\n * A {@link PolicyExpression} is the single source of truth for a row-level\n * security condition. It is compiled to Postgres `USING`/`WITH CHECK` SQL\n * (authoritative enforcement) and independently evaluated in JavaScript (to\n * drive the admin UI, and — in future — to enforce on engines without native\n * RLS such as MongoDB). Because both the SQL and the JS decision derive from\n * the *same* expression, the UI matches database enforcement by construction —\n * no drift between two hand-written implementations.\n *\n * The only escape hatch that cannot be evaluated client-side is the\n * {@link RawPolicyExpression} node (`{ kind: \"raw\" }`): it preserves full\n * PostgreSQL power but, being arbitrary SQL, is treated as *unknown* by the\n * JavaScript evaluator (never silently allowed) and reflected exactly in the UI\n * via server-computed capability flags.\n *\n * @group Models\n */\nexport type PolicyExpression =\n | TruePolicyExpression\n | FalsePolicyExpression\n | AndPolicyExpression\n | OrPolicyExpression\n | NotPolicyExpression\n | ComparePolicyExpression\n | RolesOverlapPolicyExpression\n | RolesContainPolicyExpression\n | AuthenticatedPolicyExpression\n | RegisteredPolicyExpression\n | ServerContextPolicyExpression\n | ExistsInPolicyExpression\n | RawPolicyExpression;\n\n/**\n * The id a request without a logged-in user reports as `rebase.uid()`.\n *\n * A user-context request always sets `app.uid`: blank would read back as\n * `NULL`, and `NULL` is how the trusted server context is recognised, so an\n * anonymous visitor would be promoted to server privileges. The driver\n * therefore substitutes this sentinel at the single chokepoint where the GUC\n * is set.\n *\n * The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a\n * tautology on the user path** — it is true for anonymous visitors too. Use\n * {@link policy.authenticated} to mean \"signed in\", and\n * {@link policy.serverContext} to mean \"the trusted server context\". Do not\n * hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one\n * literal is not enough.\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_ID = \"anonymous\";\n\n/**\n * Every uid that has ever meant \"nobody is signed in\" — newest first.\n *\n * There are two because there were two. The types, the policy compiler, the\n * JavaScript evaluator and the linter were all built on\n * {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated\n * callers as `'anon'` — so `policy.authenticated()`, which compiled to\n * `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The\n * sanctioned way to write \"signed in\" granted to everyone, and the linter\n * flagged the spelling that actually worked as a foreign convention.\n *\n * The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here\n * because policies outlive the server that generated them: a database still\n * holding policies from before the fix, or a project whose server has not been\n * upgraded yet, must not become a grant in either direction. Compile against\n * this list, not against a single literal.\n *\n * No real user id is ever one of these, so a match is always \"not signed in\".\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_IDS: readonly string[] = [ANONYMOUS_USER_ID, \"anon\"];\n\n/**\n * Whether a uid stands for \"no one is signed in\", in any spelling rebase has\n * used. `null`/`undefined` is the trusted server context, not an anonymous\n * caller, and is therefore **not** anonymous — see {@link ANONYMOUS_USER_ID}.\n *\n * @group Models\n */\nexport function isAnonymousUid(uid: string | null | undefined): boolean {\n return typeof uid === \"string\" && ANONYMOUS_USER_IDS.includes(uid);\n}\n\n/** Always allows. Compiles to `true`. @group Models */\nexport interface TruePolicyExpression {\n kind: \"true\";\n}\n\n/** Always denies. Compiles to `false`. @group Models */\nexport interface FalsePolicyExpression {\n kind: \"false\";\n}\n\n/** Logical AND — every operand must pass. @group Models */\nexport interface AndPolicyExpression {\n kind: \"and\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical OR — at least one operand must pass. @group Models */\nexport interface OrPolicyExpression {\n kind: \"or\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical negation. @group Models */\nexport interface NotPolicyExpression {\n kind: \"not\";\n operand: PolicyExpression;\n}\n\n/** Comparison operators available to {@link ComparePolicyExpression}. @group Models */\nexport type PolicyCompareOperator = \"eq\" | \"neq\" | \"lt\" | \"lte\" | \"gt\" | \"gte\";\n\n/**\n * Compares two operands, e.g. `owner_id = rebase.uid()`.\n * @group Models\n */\nexport interface ComparePolicyExpression {\n kind: \"compare\";\n op: PolicyCompareOperator;\n left: PolicyOperand;\n right: PolicyOperand;\n}\n\n/**\n * True when the user holds *at least one* of the given application roles.\n * Compiles to `string_to_array(rebase.roles(), ',') && ARRAY[...]`.\n * @group Models\n */\nexport interface RolesOverlapPolicyExpression {\n kind: \"rolesOverlap\";\n roles: readonly string[];\n}\n\n/**\n * True when the user holds *all* of the given application roles.\n * Compiles to `string_to_array(rebase.roles(), ',') @> ARRAY[...]`.\n * @group Models\n */\nexport interface RolesContainPolicyExpression {\n kind: \"rolesContain\";\n roles: readonly string[];\n}\n\n/**\n * True when a signed-in user is making the request. Compiles to\n * `rebase.uid() IS NOT NULL AND rebase.uid() <> 'anonymous'`.\n *\n * Both halves are load-bearing. `IS NOT NULL` excludes the server context;\n * the {@link ANONYMOUS_USER_ID} comparison excludes anonymous visitors, who\n * *do* carry a non-null `rebase.uid()`. Checking only `IS NOT NULL` grants to\n * everyone — see {@link ANONYMOUS_USER_ID}.\n *\n * `policy.not(policy.authenticated())` therefore means \"anonymous visitor or\n * the server context\". To single out the server context, use\n * {@link ServerContextPolicyExpression}.\n * @group Models\n */\nexport interface AuthenticatedPolicyExpression {\n kind: \"authenticated\";\n}\n\n/**\n * True for a signed-in caller who has an ACCOUNT — not a guest.\n *\n * The distinction {@link AuthenticatedPolicyExpression} cannot make. Anonymous\n * SIGN-IN (`POST /auth/anonymous`) mints a real user row with a real uid and a\n * real session, so such a caller is \"authenticated\" by every test that looks at\n * `rebase.uid()`: same shape, same default role, indistinguishable inside a\n * policy. On a deployment with anonymous sign-in enabled, every rule meaning\n * \"a signed-in person\" therefore also meant \"anybody at all\", since pressing\n * Continue as guest needs no email, no password and no agreement to anything.\n *\n * Note the two senses of \"anonymous\", which is the reason this was easy to\n * miss. {@link ANONYMOUS_USER_ID} is the sentinel for a request carrying NO\n * session, and `authenticated()` already excludes it. A guest is the other\n * thing: a session with nobody behind it. This node excludes both.\n *\n * Compiles to `authenticated() AND NOT rebase.is_anonymous()`.\n *\n * Use it wherever a rule is about a person who could be held responsible for\n * something — writing a review, joining an organization, spending money. Use\n * `authenticated()` where a guest is genuinely welcome, which is what\n * anonymous sign-in is for: a cart before checkout, a draft before signup.\n * @group Models\n */\nexport interface RegisteredPolicyExpression {\n kind: \"registered\";\n}\n\n/**\n * True only in the trusted **server context** — the built-in flows that run\n * without a user (signup, migrations, `dataAsAdmin`) set no user GUC, so\n * `rebase.uid()` is `NULL` for them and only for them. Compiles to\n * `rebase.uid() IS NULL`.\n *\n * This is what lets the owner connection satisfy a policy even under FORCE RLS.\n * It is deliberately a primitive rather than `not(authenticated())`: the two\n * meant the same thing while `authenticated` ignored {@link ANONYMOUS_USER_ID},\n * and conflating them is what turns a server-only grant into an anonymous one.\n *\n * The JavaScript evaluator always returns `false` for this node — a client is\n * never the server context.\n * @group Models\n */\nexport interface ServerContextPolicyExpression {\n kind: \"serverContext\";\n}\n\n/**\n * Membership / relational access: true when at least one row exists in another\n * collection (a join/membership table) matching `where`. This is what lets you\n * scope reads to \"rows whose team the caller belongs to\" without an N+1\n * per-row lookup — it compiles to a single correlated `EXISTS` subquery.\n *\n * Inside `where`, {@link FieldPolicyOperand} (`policy.field`) references a column\n * of the joined collection, while {@link OuterFieldPolicyOperand}\n * (`policy.outerField`) references a column of the row being checked (the outer\n * table under RLS). Combine with {@link AuthUidPolicyOperand} to correlate to\n * the caller.\n *\n * @example\n * ```ts\n * // documents visible only to members of the document's team:\n * policy.existsIn({\n * collection: \"team_members\",\n * where: policy.and(\n * policy.compare(policy.field(\"team_id\"), \"eq\", policy.outerField(\"team_id\")),\n * policy.compare(policy.field(\"user_id\"), \"eq\", policy.authUid()),\n * ),\n * })\n * // → EXISTS (SELECT 1 FROM team_members _ex0\n * // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id = rebase.uid())\n * ```\n *\n * Postgres-authoritative: like {@link RawPolicyExpression}, the JavaScript\n * evaluator treats it as *unknown* (it cannot run a subquery client-side), so\n * enforcement is always the database's.\n * @group Models\n */\nexport interface ExistsInPolicyExpression {\n kind: \"existsIn\";\n /** Slug of the collection to search (the join / membership table). */\n collection: string;\n /** Condition evaluated against the joined collection's rows. */\n where: PolicyExpression;\n}\n\n/**\n * A raw PostgreSQL boolean expression — the full-power escape hatch.\n *\n * Columns can be referenced as `{column_name}`. This is Postgres-only and\n * **server-authoritative**: the JavaScript evaluator cannot evaluate arbitrary\n * SQL, so it treats this node as *unknown* rather than guessing.\n * @group Models\n */\nexport interface RawPolicyExpression {\n kind: \"raw\";\n sql: string;\n}\n\n/**\n * An operand referenced by a {@link ComparePolicyExpression}.\n * @group Models\n */\nexport type PolicyOperand =\n | FieldPolicyOperand\n | OuterFieldPolicyOperand\n | LiteralPolicyOperand\n | AuthUidPolicyOperand\n | AuthRolesPolicyOperand\n | AuthClaimPolicyOperand;\n\n/** A column value on the row being evaluated. @group Models */\nexport interface FieldPolicyOperand {\n kind: \"field\";\n /** The property/column name (resolved to its DB column when compiled). */\n name: string;\n}\n\n/**\n * A column value on the *outer* row when used inside {@link ExistsInPolicyExpression}\n * — i.e. the row the RLS policy is being evaluated for, referenced from within the\n * subquery. Outside an `existsIn` it is equivalent to {@link FieldPolicyOperand}.\n * @group Models\n */\nexport interface OuterFieldPolicyOperand {\n kind: \"outerField\";\n /** The property/column name on the outer collection. */\n name: string;\n}\n\n/** A constant value. @group Models */\nexport interface LiteralPolicyOperand {\n kind: \"literal\";\n value: string | number | boolean | null;\n}\n\n/** The current user's id — compiles to `rebase.uid()`. @group Models */\nexport interface AuthUidPolicyOperand {\n kind: \"authUid\";\n}\n\n/**\n * The current user's roles as an array — compiles to\n * `string_to_array(rebase.roles(), ',')`.\n * @group Models\n */\nexport interface AuthRolesPolicyOperand {\n kind: \"authRoles\";\n}\n\n/**\n * A named claim on the caller's session token — compiles to\n * `NULLIF(rebase.jwt() ->> '<name>', '')`.\n *\n * The operand multi-tenancy is built on, and the reason it is an operand rather\n * than a {@link RawPolicyExpression}: a claim arrives as **text**, and the\n * column it is compared against usually is not. `org_id = rebase.jwt() ->>\n * 'org_id'` on a `uuid` column is not a policy that denies — it is\n * `CREATE POLICY` failing with \"operator does not exist: uuid = text\", and a\n * table left with RLS enabled and no policy denies every row. Casting the\n * *column* to text instead compiles, but takes the index off the one predicate\n * that is ANDed into every read of the table.\n *\n * As an operand the compiler can see both sides: it casts the claim to the\n * column's type, guarded so a malformed claim denies rather than raising\n * `invalid input syntax` on every query, and the column keeps its index.\n *\n * An absent claim, and a claim set to the empty string, are both NULL — and a\n * comparison against NULL is never true, so a caller carrying no claim sees no\n * rows rather than all of them.\n *\n * Only *custom* claims are reachable. `uid`, `roles`, `aal` and `isAnonymous`\n * are identity claims written after the custom ones when a token is minted,\n * precisely so a claims hook cannot assert them; they have their own operands\n * ({@link AuthUidPolicyOperand}, {@link AuthRolesPolicyOperand}) and naming one\n * here is refused.\n *\n * Postgres-authoritative: the JavaScript evaluator reports *unknown* rather\n * than reproducing Postgres's cast semantics (uuid case folding, numeric\n * widening) a second time and getting them subtly wrong.\n * @group Models\n */\nexport interface AuthClaimPolicyOperand {\n kind: \"authClaim\";\n /** The claim's name on the token, e.g. `\"org_id\"`. */\n name: string;\n}\n\n// ── Constructor helpers ──────────────────────────────────────────────\n// Small, dependency-free builders so callers (and the desugaring in\n// `@rebasepro/common`) can assemble expressions without object-literal noise.\n\n/** @group Models */\nexport const policy = {\n true: (): TruePolicyExpression => ({ kind: \"true\" }),\n false: (): FalsePolicyExpression => ({ kind: \"false\" }),\n and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: \"and\",\noperands: operands as PolicyExpression[] }),\n or: (...operands: readonly PolicyExpression[]): OrPolicyExpression => ({ kind: \"or\",\noperands: operands as PolicyExpression[] }),\n not: (operand: PolicyExpression): NotPolicyExpression => ({ kind: \"not\",\noperand }),\n compare: (left: PolicyOperand, op: PolicyCompareOperator, right: PolicyOperand): ComparePolicyExpression =>\n ({ kind: \"compare\",\nop,\nleft,\nright }),\n rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: \"rolesOverlap\",\nroles: roles as string[] }),\n rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: \"rolesContain\",\nroles: roles as string[] }),\n authenticated: (): AuthenticatedPolicyExpression => ({ kind: \"authenticated\" }),\n registered: (): RegisteredPolicyExpression => ({ kind: \"registered\" }),\n serverContext: (): ServerContextPolicyExpression => ({ kind: \"serverContext\" }),\n existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>\n ({ kind: \"existsIn\",\ncollection: args.collection,\nwhere: args.where }),\n raw: (sql: string): RawPolicyExpression => ({ kind: \"raw\",\nsql }),\n field: (name: string): FieldPolicyOperand => ({ kind: \"field\",\nname }),\n outerField: (name: string): OuterFieldPolicyOperand => ({ kind: \"outerField\",\nname }),\n literal: (value: string | number | boolean | null): LiteralPolicyOperand => ({ kind: \"literal\",\nvalue }),\n authUid: (): AuthUidPolicyOperand => ({ kind: \"authUid\" }),\n authRoles: (): AuthRolesPolicyOperand => ({ kind: \"authRoles\" }),\n authClaim: (name: string): AuthClaimPolicyOperand => ({ kind: \"authClaim\",\nname })\n};\n","/**\n * The SQL helper functions RLS policies call, and the schema they live in.\n *\n * ## One schema, and it is ours\n *\n * Rebase creates exactly one schema in a project's database: `rebase`. These\n * three functions live in it alongside the framework's own tables, and that is\n * the whole contract — a reader can look at a database and know precisely which\n * namespace belongs to the framework and that nothing else was touched.\n *\n * It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called\n * `auth`, which is Supabase's name, chosen so that a developer who had written\n * Supabase RLS would recognise `auth.uid()`. The familiarity was real but the\n * name was not Rebase's to take, and taking it had a concrete cost: pointing\n * Rebase at a database that already had a Supabase `auth` schema meant\n * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's\n * `RETURNS uuid`, which Postgres rejects outright —\n *\n * ERROR: cannot change return type of existing function\n * HINT: Use DROP FUNCTION auth.uid() first.\n *\n * — and the failure landed inside a catch-all that logged a warning and carried\n * on, leaving a database with auth tables, no helper functions, and policies\n * calling functions that did not exist. Under `rebase db migrate` the same\n * statements aborted the migration instead.\n *\n * `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`\n * schema untouched and gains a `rebase` one, which is what a gradual migration\n * needs.\n *\n * ## Why functions at all, rather than inlining `current_setting`\n *\n * Because the indirection has already been spent once. `uid()` resolves\n * `app.uid` and falls back to the pre-rename `app.user_id`, so that during a\n * rolling deploy — old and new pods serving one database — both eras resolve\n * the principal. That was a single `CREATE OR REPLACE`. Inlined into policy\n * bodies it would have been a rewrite of every policy on every table.\n *\n * ## Why the name is not configurable\n *\n * A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so\n * these strings are written into every policy in every database Rebase has\n * provisioned. Everything that reads policies back — the SQL-to-policy parser\n * behind the admin UI, the drift checker, `rls-check` — would have to know the\n * configured value to recognise its own output. One frozen name is the feature.\n */\n\n/** The schema Rebase owns. The only schema Rebase creates. */\nexport const REBASE_SCHEMA = \"rebase\";\n\n/**\n * The principal of the current request, as text, or NULL in the server context.\n *\n * Never NULL for a user request — an anonymous one carries\n * {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for\n * the trusted server plane and `IS NOT NULL` a tautology.\n */\nexport const RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;\n\n/** The request's roles as a comma-separated string, for `string_to_array`. */\nexport const RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;\n\n/**\n * Whether the caller is a GUEST — signed in through anonymous sign-in rather\n * than with an account.\n *\n * A different question from {@link ANONYMOUS_USER_ID}, and the two are easy to\n * confuse: that sentinel means \"no session at all\", while this means \"a session\n * with nobody behind it\". Anonymous sign-in mints a real user row with a real\n * uid, so before this reached the database the two kinds of caller were one\n * principal inside every policy.\n */\nexport const RLS_IS_ANONYMOUS_SQL = `${REBASE_SCHEMA}.is_anonymous()`;\n\n/** The request's JWT claims as `jsonb`, or `{}`. */\nexport const RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;\n\n/**\n * The pre-1.0 spellings, for recognising policies and hand-written SQL that\n * predate the move.\n *\n * Kept because policies outlive the server that wrote them: a database migrated\n * by an older release still holds `auth.uid()` in its policy bodies until the\n * next push or boot recompiles them, and anything that reads policies back has\n * to recognise both eras or report the framework's own output as foreign drift.\n * Also used to give a project whose `securityRules` contain raw `auth.uid()` a\n * message naming the replacement, instead of a parse failure.\n */\nexport const LEGACY_RLS_SCHEMA = \"auth\";\nexport const LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;\nexport const LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;\nexport const LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;\n\n/**\n * Rewrites the pre-1.0 function calls in a fragment of policy SQL.\n *\n * Deliberately anchored on a word boundary and the schema qualifier, so a column\n * called `auth_uid` or a table named `auth` is left alone.\n */\nexport function rewriteLegacyRlsFunctions(sql: string): string {\n return sql.replace(\n /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/gi,\n (_match, fn: string) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`\n );\n}\n\n/** Whether a fragment of SQL still calls the pre-1.0 functions. */\nexport function usesLegacyRlsFunctions(sql: string): boolean {\n return /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/i.test(sql);\n}\n","/**\n * First-class multi-tenancy: one declaration, every layer.\n *\n * A tenant-scoped collection was expert work. It took four separate,\n * hand-written pieces that nothing checked against each other — a column, an\n * `existsIn` or raw RLS rule, a value stamped on every insert by a callback,\n * and an index somebody had to remember. Miss the index and the table scans;\n * miss the stamp and the row is invisible the moment it is written; miss the\n * rule and every tenant reads every other tenant's rows, which is the failure\n * nothing surfaces until it is a disclosure.\n *\n * {@link CollectionTenantConfig} is the one place that says \"this collection\n * belongs to a tenant\", and the four pieces are derived from it:\n *\n * - the column is `NOT NULL` and gets a btree index (`planSchema`);\n * - a **restrictive** RLS policy is injected for every operation, so it\n * composes with (rather than replaces) whatever `securityRules` the\n * collection declares — tenancy narrows, it never grants;\n * - the write path stamps the caller's tenant on create, refuses a write that\n * names another tenant, and refuses an update that moves a row between\n * tenants;\n * - the OpenAPI document marks the field so a generated client can see it.\n *\n * @see CollectionTenantConfig\n * @group Models\n */\n\n/**\n * The caller's tenant comes from a claim on their session token.\n *\n * The single-tenant-per-user shape: an identity provider (or Rebase's own\n * custom-claims hook) puts the organization on the token, and every request\n * carries it. Compiles to a comparison against `rebase.jwt() ->> '<claim>'`,\n * which is the same value a hand-written rule would read — so the generated\n * policy and anything an author writes beside it agree by construction.\n *\n * @group Models\n */\nexport interface TenantClaimSource {\n /**\n * The claim's name on the access token, e.g. `\"org_id\"`.\n *\n * Custom claims survive verification and reach RLS as `rebase.jwt()`; the\n * identity claims (`uid`, `roles`, `aal`, `isAnonymous`) are written after\n * them when a token is minted and cannot be shadowed, so naming one of\n * those here is refused rather than quietly reading the identity.\n */\n claim: string;\n}\n\n/**\n * The caller's tenants come from rows of a membership collection.\n *\n * The many-tenants-per-user shape — a `memberships` table with a user column\n * and a tenant column, which is how a person belongs to three organizations at\n * once. Compiles to a correlated `EXISTS` over that table (`policy.existsIn`),\n * so the database answers \"is the caller a member of this row's tenant?\" in the\n * same query rather than in an N+1 of lookups.\n *\n * Nothing is put on the token, so nothing has to be re-minted when somebody\n * joins or leaves a tenant — the next statement already sees the new row.\n *\n * @group Models\n */\nexport interface TenantMembershipSource {\n membership: {\n /** Slug of the collection holding the memberships. */\n collection: string;\n /** The property on it that holds the user id (compared to `rebase.uid()`). */\n userField: string;\n /** The property on it that holds the tenant id. */\n tenantField: string;\n };\n}\n\n/** Where the caller's tenant comes from. @group Models */\nexport type TenantSource = TenantClaimSource | TenantMembershipSource;\n\n/** Narrow a {@link TenantSource} to its claim form. @group Models */\nexport function isTenantClaimSource(source: TenantSource): source is TenantClaimSource {\n return typeof (source as TenantClaimSource).claim === \"string\";\n}\n\n/** Narrow a {@link TenantSource} to its membership form. @group Models */\nexport function isTenantMembershipSource(source: TenantSource): source is TenantMembershipSource {\n return typeof (source as TenantMembershipSource).membership === \"object\"\n && (source as TenantMembershipSource).membership !== null;\n}\n\n/**\n * The roles tenancy does not apply to, when the collection names none.\n *\n * `admin`, mirroring the security baseline every collection already carries\n * (`<table>_default_admin_read` / `_write`): the Studio, `dataAsAdmin` and a\n * support operator all run with it, and a tenancy rule that locked them out\n * would make the admin panel show an empty table on a collection full of rows.\n *\n * @group Models\n */\nexport const DEFAULT_TENANT_BYPASS_ROLES: readonly string[] = [\"admin\"];\n\n/**\n * Declare a collection tenant-scoped.\n *\n * ```ts\n * export const posts = buildCollection({\n * slug: \"posts\",\n * properties: {\n * orgId: { type: \"string\", validation: { required: true } },\n * title: { type: \"string\" }\n * },\n * tenant: { field: \"orgId\", from: { claim: \"org_id\" } }\n * });\n * ```\n *\n * The property has to exist — this says what a column *means*, it does not\n * conjure one into existence, exactly like `softDelete`. A config naming a\n * property the collection does not declare is refused at boot rather than at\n * the first read.\n *\n * ## What it composes with\n *\n * The injected policy is **restrictive**, so it is ANDed with every permissive\n * policy on the table: `securityRules`, `ownerField`, the injected admin\n * baseline. That is the only composition that is safe by construction — a\n * permissive tenancy policy would OR with the author's rules and a single\n * `access: \"public\"` rule would take the whole tenancy boundary off.\n *\n * Postgres-only. RLS is what enforces it, and an engine without row-level\n * security cannot be given this guarantee by an application-layer filter that\n * a raw query goes around.\n *\n * @group Models\n */\nexport interface CollectionTenantConfig<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * The property holding the tenant id.\n *\n * A `string` or `number` property, or a `reference` / `belongsTo` relation\n * to the tenants collection — in which case the foreign key the relation\n * already declares is the column, and no second one is created.\n *\n * The column is made `NOT NULL` and indexed: a nullable tenant column is a\n * row that belongs to nobody and is therefore invisible to everybody, and\n * an unindexed one turns every RLS-filtered read into a sequential scan.\n */\n field: Extract<keyof M, string> | string;\n\n /** Where the caller's tenant comes from. */\n from: TenantSource;\n\n /**\n * Roles that see and write across every tenant.\n *\n * Defaults to {@link DEFAULT_TENANT_BYPASS_ROLES}. An empty array means\n * \"nobody bypasses\" — the trusted server context still does, because it is\n * what runs migrations and the auth flows, and a policy that excluded it\n * would break the boot rather than protect a tenant.\n */\n bypassRoles?: readonly string[];\n}\n","import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { OrderByTuple } from \"./filter-operators\";\nimport type { LogicalCondition } from \"../controllers/data\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\nimport type { SchemaEditingAdmin } from \"./schema_editing\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /**\n * An `or(...)`/`and(...)` group, alongside `filter`.\n *\n * Counted as well as fetched, or `total` describes a different set of rows\n * from the one that was served — the same reason `filter` is here.\n */\n logical?: LogicalCondition;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * Declared here because a subscription is a query, and every field a query\n * has this one needs too. It was missing, so the type-checked boundary\n * dropped it: the client sent the group, nothing rejected it, and the\n * subscription re-fetched with the group gone — pushing every row the\n * caller's policies allowed rather than the ones they asked for. The same\n * defect `FetchCollectionProps.logical` documents, one layer up.\n */\n logical?: LogicalCondition;\n /**\n * Where the subscription's page starts. Missing for the same reason, with\n * a quieter symptom: a subscriber watching page two was pushed page one,\n * and a `collection_update` frame carries no window for it to notice with.\n */\n offset?: number;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched. */\n searchExplain?: boolean;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void\n ): void;\n\n /**\n * Subscribe to single entity changes\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `rebase.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>\n & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;\n\n/**\n * Type guard: can this admin plan a live schema change?\n *\n * Planning is engine-specific — it renders DDL, a Drizzle schema and the\n * declarative SQL artifacts — so the implementation lives in the driver\n * package. The server detects the capability structurally, exactly as it does\n * for SQL, rather than importing an engine it is supposed to know nothing\n * about.\n *\n * @group Admin\n */\nexport function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {\n return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === \"function\";\n}\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Ask the database whether it is there, before anything else touches it.\n *\n * Boot's first database call is not `initializeDriver` — it is the schema\n * provisioning that runs ahead of it, and a driver's connection diagnosis\n * therefore never got the chance to run. A stopped database produced\n * `Failed query: [redacted]` and a stack through drizzle internals: no host,\n * no port, no `ECONNREFUSED`, and no hint about starting the thing.\n *\n * Implementations MUST issue the cheapest round trip they have (`SELECT 1`),\n * MUST throw an error whose message names the host, the port and the\n * driver's own reason, and MAY log a fuller diagnosis first. They MUST NOT\n * throw for a reachable database that merely answered something unexpected —\n * the caller treats a throw as fatal.\n *\n * `driverResult` is optional for the same reason as\n * {@link ensureCollectionSchema}: this runs before `initializeDriver`, so an\n * adapter that was constructed with its own connection has to fall back to\n * it.\n */\n verifyConnection?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n *\n * `driverResult` is optional: this runs before `initializeDriver`, and only\n * the bundle path has a pre-init stand-in to pass. An adapter built by an\n * application already holds its own connection and MUST use it when this is\n * `undefined` — dereferencing it unconditionally works for managed tenants\n * and breaks every app that builds its own adapter.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Apply the collections' row-level-security policies, additively and\n * idempotently — the companion to {@link ensureCollectionSchema}.\n *\n * That method creates the tables; a table with RLS disabled and no policies\n * is not servable, because authenticated requests run as a restricted role:\n * a read with no `SELECT` policy returns nothing (a public collection\n * answers 401) and a write with no `INSERT`/`UPDATE` policy is denied. The\n * `db push` CLI applies these from the same collections, but it cannot reach\n * a managed tenant's in-cluster database — the runtime, already connected,\n * is the only thing that can.\n *\n * MUST be idempotent (re-run on every boot) and MUST NOT be destructive.\n * Runs after auth initialization, because the generated policies call the\n * `auth.*` helper functions and `CREATE POLICY` validates they exist.\n */\n ensureCollectionPolicies?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Create the RLS helper functions on this source's database. See\n * `DatabaseAdapter.ensureRlsRuntime`; needed on every source that is not\n * the default, whose helpers arrive with the auth tables.\n */\n ensureRlsRuntime?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Re-check, after the schema exists, that requests will actually be\n * constrained by the database's own authorization.\n *\n * A driver that isolates user requests by switching to a restricted role has\n * to decide at connect time whether the switch is needed — and on a fresh\n * database that question is asked before there is anything to answer with.\n * The process then creates the schema, becomes its owner, and an owner is\n * exempt from the policies on what it owns. So the answer that was true when\n * the driver initialized can be false by the time it serves a request.\n *\n * This is where a driver asks again. It runs once, after collection tables,\n * auth tables and policies are all in place, and it MUST fail rather than\n * serve when the answer changed and cannot be acted on: booting anyway\n * produces exactly the unenforced server this exists to prevent.\n *\n * Optional, because it is only meaningful for drivers whose isolation\n * depends on state the schema affects. A driver with nothing to re-check\n * omits it.\n */\n finalizeSecurityPosture?(driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Read the collections schema version this database was last provisioned\n * from, or `null` when nothing has ever stamped it.\n *\n * The companion to {@link stampCollectionsSchemaVersion}: one process writes\n * what it applied, every other process compares itself to it. This is what\n * lets a split deployment — several processes over one database, only one of\n * which provisions — notice that a unit is serving against a schema it was\n * not built for. That failure is otherwise silent in both directions: a\n * column that does not exist is a SQL error on one route, and a policy that\n * was never applied is a 200 with no rows.\n *\n * `null` is not an error and MUST NOT be treated as one — every database\n * provisioned before the stamp existed reads this way, and so does every\n * fresh one until its first provisioning boot finishes.\n */\n readCollectionsSchemaVersion?(\n driverResult?: InitializedDriver\n ): Promise<string | null>;\n\n /**\n * Record the collections schema version this process just applied.\n *\n * Called only by the process that provisions, and only after both\n * {@link ensureCollectionSchema} and {@link ensureCollectionPolicies} have\n * run — a stamp written before the policies would claim a schema that is\n * only half in place, and the half that is missing is the one that fails\n * without an error.\n */\n stampCollectionsSchemaVersion?(\n version: string,\n driverResult?: InitializedDriver\n ): Promise<void>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Role management service (optional, roles are now simple strings). */\n roleService?: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n","/**\n * The vocabulary a live schema change is described in.\n *\n * Declared here, and nowhere else, because two packages that must not import\n * each other both need it: `@rebasepro/server-postgres` decides what a change\n * means and renders the files it needs, while `@rebasepro/server` commits those\n * files and serves the routes. Neither can reach the other — the server is\n * engine-agnostic by design — so the shared kernel holds the shapes and the\n * driver is detected structurally through {@link SchemaEditingAdmin}.\n *\n * Nothing here executes anything. These are the nouns.\n */\n\n/**\n * What a change will do to a live database.\n *\n * - `safe` — the boot-time ensure path expresses it, and the result matches the\n * configuration.\n * - `diverges` — the ensure path applies *something*, but the database will not\n * match what the configuration declares, and nothing reports it. This is the\n * category worth having: adding a required property to a populated table\n * yields a nullable column, and adding a value to an existing enum yields\n * nothing at all. Both read as success.\n * - `needs-migration` — the ensure path cannot express it. Dropping anything,\n * changing a type, moving a primary key.\n */\nexport type SchemaChangeVerdict = \"safe\" | \"diverges\" | \"needs-migration\";\n\nexport type SchemaChangeKind =\n | \"add-collection\"\n | \"remove-collection\"\n | \"add-property\"\n | \"remove-property\"\n | \"change-property-type\"\n | \"rename-column\"\n | \"add-enum-value\"\n | \"remove-enum-value\"\n | \"change-required\"\n | \"change-primary-key\";\n\nexport interface SchemaChange {\n kind: SchemaChangeKind;\n verdict: SchemaChangeVerdict;\n /** Collection slug. */\n collection: string;\n /** Property name, where the change is to one. */\n property?: string;\n /** One line, specific: what changed and what it will do. */\n detail: string;\n /** What to do instead, when the verdict is not `safe`. */\n remedy?: string;\n}\n\nexport interface ClassifiedSchemaChanges {\n changes: SchemaChange[];\n /** The worst verdict present, or `safe` for an empty diff. */\n verdict: SchemaChangeVerdict;\n /** True only when every change is `safe` — the one case an editor may apply. */\n applicable: boolean;\n}\n\n/**\n * Where a project's generated schema artifacts live, relative to the **project**\n * root — which is the repository root only when the project is the whole\n * repository.\n *\n * Here rather than in the Postgres package because it is a contract, not an\n * engine detail: `@rebasepro/server` has to derive these for a project in a\n * subdirectory, and it cannot import a driver to do it.\n */\nexport interface SchemaCommitPaths {\n /** Drizzle schema, imported by the backend. */\n schemaFile: string;\n /** Declarative DDL, what `db push` applies and Atlas diffs against. */\n ddlFile: string;\n policiesFile: string;\n searchFile: string;\n /** Vector columns and ANN indexes — like search, applied by Rebase not Atlas. */\n vectorFile: string;\n /**\n * `autoValue: \"on_update\"` triggers and the function they share. Atlas's\n * free tier will not parse a desired state containing a function, so this\n * is applied by Rebase like search and vector.\n */\n triggersFile: string;\n}\n\nexport const DEFAULT_COMMIT_PATHS: SchemaCommitPaths = {\n schemaFile: \"backend/src/schema.generated.ts\",\n ddlFile: \"drizzle/schema.sql\",\n policiesFile: \"drizzle/policies.sql\",\n searchFile: \"drizzle/search.sql\",\n vectorFile: \"drizzle/vector.sql\",\n triggersFile: \"drizzle/triggers.sql\"\n};\n\n/** One file the commit writes, as content rather than as a path on a disk. */\nexport interface SchemaChangeFile {\n path: string;\n contents: string;\n}\n\n/**\n * Everything a change needs written and run.\n *\n * Computed without touching a disk or a network. The database is *read* — what\n * a change means depends on what is already there, and a plan that guessed\n * would be guessing about whether the statements it returns will be accepted.\n */\nexport interface SchemaChangePlan {\n /** Every file the commit writes — collection source and generated artifacts. */\n files: SchemaChangeFile[];\n /** The additive DDL this change adds, in dependency order. */\n statements: string[];\n classified: ClassifiedSchemaChanges;\n /** A commit message describing the change rather than announcing one. */\n message: string;\n /**\n * Constraints the configuration asks for that these statements do not\n * carry, and why.\n *\n * Almost always empty. When it is not, it is the part the person confirming\n * needs to read: the change will apply, and the database will still not\n * enforce something the configuration says — a required property over a\n * table that already holds rows with no value for it. Optional so a plan\n * from an engine that does not distinguish these cases stays valid.\n */\n withheldConstraints?: WithheldSchemaConstraint[];\n}\n\n/** A constraint a plan asks for and does not apply. */\nexport interface WithheldSchemaConstraint {\n /** `schema.table.column`. */\n target: string;\n kind: \"not-null\";\n /** What is in the way, naming the obstacle rather than the rule. */\n reason: string;\n /** What would make it applicable. */\n remedy: string;\n}\n\n/**\n * An admin that can plan a schema change.\n *\n * Planning only. Applying is `executeSql`, which every SQL admin already has,\n * and committing belongs to whatever holds the repository — keeping those three\n * apart is what lets the same plan be committed locally on a developer's machine\n * and through a GitHub App from a cloud tenant.\n *\n * @group Admin\n */\nexport interface SchemaEditingAdmin {\n /**\n * Decide what the change means and render everything it needs.\n *\n * Rejects when the change is not applicable, carrying the classification so\n * a caller can say which change was the problem.\n */\n planSchemaChange(\n before: unknown[],\n after: unknown[],\n options?: { paths?: Partial<SchemaCommitPaths> }\n ): Promise<SchemaChangePlan>;\n}\n","/**\n * The cross-instance transport for channel broadcast and presence, and the\n * contract anyone implementing one has to meet.\n *\n * These types live in `@rebasepro/types` rather than in the Postgres adapter on\n * purpose: a transport package should depend on the contract, not on the\n * database driver that happens to ship the default implementation. A\n * `@rebasepro/channel-bus-<something>` package needs this file and nothing else.\n *\n * Why a transport exists at all: entity/collection realtime already spans\n * instances (CDC, or per-mutation LISTEN/NOTIFY). Channel broadcast and presence\n * did not — they fanned out from per-process maps, so two clients served by\n * different replicas could not see each other, and nothing errored. The bus is\n * the missing hop, and deliberately *only* that hop: which local clients receive\n * a frame stays in the realtime service, so a transport never has to know what a\n * subscription, a WebSocket or a presence roster is.\n */\n\n/**\n * A frame in flight between instances.\n *\n * `sid` identifies the publishing instance. The realtime service drops frames\n * carrying its own `sid` on arrival — local fan-out already happened before the\n * publish — so a transport that echoes a publisher's own messages back to it is\n * still correct, merely wasteful.\n *\n * Keys are spelled out rather than abbreviated. The one shipped transport with a\n * size limit has a pointer path for anything that would approach it, so shaving\n * bytes off key names buys nothing worth the opacity.\n */\nexport type ChannelBusFrame =\n /** A broadcast carrying its payload. */\n | {\n kind: \"broadcast\";\n sid: string;\n channel: string;\n event: string;\n /** Originating client, echoed so receivers can skip it if it is theirs. */\n from?: string;\n /** Sequence number, present only on retained channels. */\n seq?: number;\n payload: unknown;\n }\n /**\n * A broadcast too large for the transport to carry inline: the body is\n * already durable in `rebase.channel_messages`, so the frame carries only\n * its address and each receiver reads it back. Only ever emitted for\n * retained channels, and only by a transport with a finite\n * {@link ChannelBus.maxFrameBytes}.\n */\n | {\n kind: \"broadcast_ref\";\n sid: string;\n channel: string;\n from?: string;\n seq: number;\n }\n /** A presence join/leave/update, small by construction. */\n | {\n kind: \"presence_diff\";\n sid: string;\n channel: string;\n joins: Record<string, Record<string, unknown>>;\n leaves: Record<string, Record<string, unknown>>;\n };\n\n/** Receives frames published by *other* instances. */\nexport type ChannelBusHandler = (frame: ChannelBusFrame) => void | Promise<void>;\n\n/**\n * A cross-instance transport.\n *\n * ## What an implementation must guarantee\n *\n * - **`start()` rejects if the transport is unusable.** The caller falls back to\n * in-process delivery when it does. Resolving while disconnected produces a\n * cluster that believes it is connected and silently is not, which is the\n * exact failure this whole mechanism exists to remove.\n * - **`publish()` reaches every *other* instance, or rejects.** Delivery back to\n * the publisher is permitted but pointless (see {@link ChannelBusFrame.sid}).\n * - **`stop()` is idempotent** and releases everything, including anything\n * holding the event loop open.\n * - **A malformed message never throws out of the transport.** Parsing happens\n * inside the implementation; drop and log what you cannot understand, so one\n * bad frame cannot take the listener down.\n *\n * ## What it does *not* have to guarantee\n *\n * - **Ordering.** Retained channels carry `seq`, and the client SDK orders by\n * it. Unsequenced broadcasts are cursor-grade traffic where order is not\n * meaningful.\n * - **Durability.** A frame lost in transit is a missed live update; retained\n * channels repair themselves through the client's `channel_history` replay.\n * - **Exactly-once.** Duplicates are tolerated — retained frames are deduped by\n * `seq`, and presence diffs are idempotent by construction.\n */\nexport interface ChannelBus {\n /**\n * Identifies the transport in logs and in `getChannelBusKind()`. Use your\n * own name; the framework only compares against `\"memory\"` to decide\n * whether publishing is worth attempting at all.\n */\n readonly kind: string;\n\n /**\n * Largest frame this transport will carry, in bytes of encoded JSON, or\n * `Infinity` when there is no meaningful ceiling.\n *\n * A broadcast that exceeds it is published as a `broadcast_ref` pointer when\n * the channel is retained, and refused with an error to the sender when it\n * is not. Implementations with no limit should return `Infinity` rather than\n * a large number, so the pointer path is never taken needlessly.\n */\n readonly maxFrameBytes: number;\n\n /** Connect and begin delivering remote frames to `handler`. */\n start(handler: ChannelBusHandler): Promise<void>;\n\n /** Publish a frame to the other instances. */\n publish(frame: ChannelBusFrame): Promise<void>;\n\n /** Disconnect and release resources. Idempotent. */\n stop(): Promise<void>;\n}\n\n/**\n * Which transport to use, for the two that ship with the Postgres adapter.\n *\n * To use one that does not ship here — a Redis package, or your own class —\n * pass the {@link ChannelBus} instance itself instead of a config object.\n *\n * There are deliberately only two built in, and neither adds a service to a\n * deployment. Rebase deploys as Postgres + backend + frontend; a bus that\n * required a message broker would put a second stateful service into every\n * `docker-compose.yml` the CLI scaffolds, for a feature most applications never\n * use. Measured across two backend instances against one Postgres container,\n * the Postgres bus carried ~10k cross-instance messages/second with no losses,\n * and stayed flat out to eight instances — comfortably past what live-cursor\n * collaboration generates. The extension point below is the answer for anyone\n * who does outgrow it.\n */\nexport type ChannelBusConfig =\n /**\n * In-process only — the historical behaviour. Broadcast and presence reach\n * the clients connected to *this* instance and no further.\n */\n | { type: \"memory\" }\n /**\n * Postgres LISTEN/NOTIFY, reusing infrastructure the deployment already has.\n *\n * `pg_notify` caps a payload at 8000 bytes, so a broadcast larger than that\n * is delivered cross-instance only on a *retained* channel, where the\n * notification carries a pointer (`seq`) instead of the message and each\n * receiver reads the body back from `rebase.channel_messages`. An oversized\n * broadcast on an ephemeral channel is refused rather than silently\n * delivered to half the cluster.\n *\n * NOTE: `LISTEN` needs a session-mode connection. Behind PgBouncer in\n * transaction mode this must point at the database directly\n * (`DATABASE_DIRECT_URL`), not at the pooler.\n */\n | {\n type: \"postgres\";\n /** Direct connection for the LISTEN client. Defaults to `DATABASE_DIRECT_URL`. */\n connectionString?: string;\n /**\n * How long to coalesce outgoing frames into a single notification, in\n * milliseconds. Defaults to 10.\n *\n * A notify is a query on your primary database, so under load this is\n * the difference between one query per message and one per window. The\n * window is leading-edge: a frame arriving when none is open goes out\n * immediately, so an idle channel pays no added latency and only a\n * sustained stream is batched.\n *\n * Set to 0 to disable coalescing and send every frame on its own.\n */\n batchWindowMs?: number;\n };\n\n/**\n * What `realtime.bus` accepts: a built-in transport by name, or any\n * {@link ChannelBus} instance.\n *\n * ```typescript\n * realtime: { bus: { type: \"postgres\" } } // shipped\n * realtime: { bus: new MyRedisChannelBus(url) } // a separate package, or your own\n * ```\n */\nexport type ChannelBusSetting = ChannelBusConfig | ChannelBus;\n\n/**\n * Whether `setting` is an already-constructed transport rather than a request\n * for a built-in one.\n *\n * Structural rather than nominal so that an instance from a *different copy* of\n * `@rebasepro/types` — an entirely normal outcome of a separately versioned\n * transport package — is still recognised.\n */\nexport function isChannelBusInstance(setting: ChannelBusSetting | undefined): setting is ChannelBus {\n return typeof (setting as ChannelBus | undefined)?.publish === \"function\";\n}\n","/**\n * The resource graph: one declaration site for every named thing a project needs.\n *\n * ## The rule\n *\n * **Every named resource is declared with a constructor in config code.** A\n * database, a bucket, a topic and whatever kind comes next are all spelled the\n * same way, so \"where do I declare my second one\" has one answer instead of one\n * answer per kind.\n *\n * ```ts\n * export const main = database(\"main\");\n * export const media = bucket(\"media\", { transport: \"direct\" });\n * export const signups = topic<SignupEvent>(\"signups\");\n * ```\n *\n * ## Declaration is not binding\n *\n * A declaration says a resource *exists* and what shape it has. It never says\n * how to reach it — that is a property of the environment, not of the project,\n * and it differs between a laptop, a self-hosted box and a tenant in the cloud.\n * Binding lives in `@rebasepro/server`'s boot path, where each kind registers\n * the resolver that reads its environment variables, keyed off the logical\n * name declared here.\n *\n * This split is the whole point. Before it, storage topology was hand-written\n * into `rebase.json` while database topology lived in TypeScript, and the\n * boundary between them was a fact about what the control plane could read\n * before a build — a platform implementation detail that a developer had no way\n * to derive. Worse, storage could be declared in *both* places, and the merge\n * silently kept the JSON's engine and discarded the code's.\n *\n * ## Why a registry rather than a fixed union\n *\n * Kinds register themselves. Adding pub/sub, a cache or a search index must not\n * require editing a manifest schema, a validator and three switch statements —\n * that cost is exactly why the last two kinds ended up in different homes.\n */\n\n/** How a client reaches a resource. */\nexport type ResourceTransport =\n /** Through the backend. The default, and the only one that needs no client SDK. */\n | \"server\"\n /** A provider SDK talks to the resource directly; the backend is not in the path. */\n | \"direct\";\n\n/**\n * A resource kind, as registered.\n *\n * `engines` is an allowlist rather than documentation. An unrecognised engine\n * used to be a free string that passed every check and failed later, further\n * from the typo that caused it — `\"s2\"` for `\"s3\"` reached the runtime. Anything\n * genuinely outside the list is spelled `custom:<id>`, which says so at the call\n * site instead of looking like a typo.\n */\nexport interface ResourceKindSpec {\n /** The kind's name, as it appears in a declaration and in the graph. */\n kind: string;\n /**\n * Which definition of this kind this is. Bump it whenever anything else in\n * the spec changes.\n *\n * Two copies of this package can meet in one process — a published driver\n * inlines it into its dist, and the runtime image ships its own — and the\n * registry is shared between them on purpose. Without a revision the only\n * thing the registry can do with two specs that differ is refuse, and a\n * refusal at driver load is a pod that never boots: every bundle built with\n * a driver older than the change dies on the first image that carries it.\n * With one, the higher revision wins whichever copy loads first, and the\n * older copy is told so. Missing means 0, which is what every copy shipped\n * before revisions existed reports.\n *\n * Only copies that know about revisions honour them. A copy published\n * BEFORE they existed still compares the whole literal and throws, so a\n * kind that has shipped in such a copy cannot change its literal at all —\n * not even to add this field. Correct those kinds with `amendResourceKind`.\n */\n revision?: number;\n /** Engines this kind ships with. `custom:<id>` is always additionally valid. */\n engines: readonly string[];\n /** Used when a declaration names none. */\n defaultEngine: string;\n /**\n * Environment variable base names this kind binds from, in the order a\n * binder should try them. A resource keyed `analytics` reads\n * `<BASE>__ANALYTICS`; the default-keyed resource reads `<BASE>` unsuffixed,\n * so a single-resource project configured the obvious way declares nothing.\n */\n envBases: readonly string[];\n /**\n * The subset of `envBases` that matters for a given engine.\n *\n * The binder reads every base and takes whichever is set — harmless, and it\n * keeps binding tolerant. A GENERATOR cannot be that relaxed: `rebase eject\n * infra` writing S3_BUCKET, GCS_BUCKET, STORAGE_BUCKET and\n * STORAGE_PUBLIC_URL for a `local` bucket hands somebody four variables of\n * which three are noise, and a config file full of irrelevant keys is one\n * nobody reads carefully.\n *\n * Keyed by engine; an engine with no entry falls back to all of them, which\n * is the honest answer for one this package has never heard of.\n */\n envBasesByEngine?: Readonly<Record<string, readonly string[]>>;\n /** Option keys this kind accepts beyond the common ones, for validation. */\n optionKeys?: readonly string[];\n /**\n * Whether a project implicitly has one of these even when it declares\n * nothing. True for databases — a backend without one is not a backend —\n * and false for topics, where zero is the normal number.\n */\n implicitDefault?: boolean;\n}\n\n/** The key a resource takes when a project declares only one of its kind. */\nexport const DEFAULT_RESOURCE_KEY = \"(default)\";\n\n/** A declared resource, as it appears in the graph. */\nexport interface ResourceDeclaration {\n kind: string;\n /** Unique within its kind. What a binder looks up and what an env suffix is built from. */\n key: string;\n engine: string;\n transport: ResourceTransport;\n label?: string;\n /** Kind-specific options, validated against the kind's `optionKeys`. */\n options: Readonly<Record<string, unknown>>;\n /**\n * What in the project reaches this resource, as `<what>:<name>` — a\n * `collection:posts` routed to a database, a `property:posts.cover` stored\n * in a bucket, a `function:report` importing a handle.\n *\n * Recorded by the derive step, never by a constructor: a declaration says\n * a resource exists, and only a reader that has evaluated the rest of the\n * project can say who uses it. It is the map a host needs to split a\n * monolith into units later, and the map a console needs to answer \"what\n * breaks if I remove this\". Absent when nothing was recorded, which is\n * different from an empty list.\n */\n usedBy?: readonly string[];\n}\n\n/**\n * The value a constructor returns.\n *\n * Carries its own declaration so config code can hold it and pass it around,\n * and stringifies to its key so it drops into the places that still take one.\n * Collections name a data source by string today; a handle works there without\n * the collection API having to change, which keeps this a config redesign\n * rather than a rewrite of the data layer.\n */\nexport interface ResourceHandle extends ResourceDeclaration {\n toString(): string;\n}\n\nconst BRAND = Symbol.for(\"@rebasepro/types.resource\");\n\n/** Whether a value is a resource handle rather than a plain string key. */\nexport function isResourceHandle(value: unknown): value is ResourceHandle {\n return typeof value === \"object\" && value !== null && BRAND in value;\n}\n\n/**\n * A reference to a resource where a key is expected: the handle a constructor\n * returned, or the key spelled as a string.\n *\n * The handle is the point. `dataSource: analytics` is the same name spelled\n * once — rename the export and every use follows, jump-to-definition lands on\n * the declaration, and the derive step can record who uses what. The string\n * form stays because a key has to survive serialisation: the runtime and the\n * admin UI read collections as plain data, where a handle cannot travel.\n */\nexport type ResourceRef = string | ResourceHandle;\n\n/** The key a resource reference names, whether it is a handle or already a key. */\nexport function resourceKeyOf(ref: ResourceRef): string {\n return isResourceHandle(ref) ? ref.key : ref;\n}\n\n/**\n * Replace every resource handle inside a value with its key, deeply.\n *\n * Applied where authored config becomes data: `defineCollection`, the\n * collection loaders, the derive step. Past that point a collection is plain\n * data that serialises, compares with `===` and reaches the admin UI over the\n * wire, so a handle must not survive into it. Plain objects and arrays are\n * walked; anything else — a function, a Date, a class instance — is a leaf and\n * is returned as it is, which is what keeps callbacks and validators intact.\n */\nexport function resolveResourceRefs<T>(value: T): T {\n if (isResourceHandle(value)) return value.key as unknown as T;\n // Identity-preserving: a value with no handle inside comes back as the\n // same object, not a copy. Collections point at each other through\n // `target: () => authors`, and a loader that cloned every collection would\n // leave those closures returning the originals while everything else\n // held the copies. A collection that `defineCollection` already\n // normalised passes through here untouched.\n if (Array.isArray(value)) {\n let changed = false;\n const out = value.map(item => {\n const next = resolveResourceRefs(item);\n if (next !== item) changed = true;\n return next;\n });\n return (changed ? out : value) as T;\n }\n if (value !== null && typeof value === \"object\") {\n const proto = Object.getPrototypeOf(value);\n if (proto === Object.prototype || proto === null) {\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n const next = resolveResourceRefs(v);\n if (next !== v) changed = true;\n out[k] = next;\n }\n return (changed ? out : value) as T;\n }\n }\n return value;\n}\n\n/**\n * The process-wide registry.\n *\n * Keyed off `globalThis` through a shared symbol rather than held in a module\n * local, because a module local is per *copy* of this package. A project that\n * ends up with two copies of `@rebasepro/types` — which a partially-linked\n * `node_modules` produces, and which has already caused a phantom\n * \"JWT secret not configured\" bug in this repo — would otherwise register into\n * one registry and read from the other, and see an empty graph with nothing\n * anywhere to explain it.\n *\n * `declarations` is that shared map, and stays shared: it is what a project\n * writes and what every copy has to be able to read.\n *\n * `kinds` is NOT, and the distinction is the whole point of `KINDS_KEY` below.\n */\ninterface Registry {\n /** Kinds this copy and its peers agree on — the versioned map. */\n kinds: Map<string, ResourceKindSpec>;\n /** Kinds written by a copy that predates the versioned map. Read-only here. */\n legacyKinds: Map<string, ResourceKindSpec>;\n declarations: Map<string, ResourceDeclaration>;\n}\n\nconst GLOBAL_KEY = Symbol.for(\"@rebasepro/types.resourceRegistry\");\n\n/**\n * Where kinds live, versioned — and why the version is in the symbol.\n *\n * Sharing one kinds map across copies means the copy that registers SECOND is\n * the one that runs the comparison. That copy is whatever the bundle happens to\n * carry, which for a driver is a build of this package frozen at its release —\n * so the rule enforced is the rule that shipped THEN, not the one written here.\n *\n * `revision` (390bb03cd, applied to `database` in 346df48e2) was supposed to\n * settle a disagreement between two copies, and it settles it only when the\n * copy doing the arithmetic knows what `revision` is. 0.17.0–0.17.3 do not:\n * they deep-equal the spec and throw. The runtime registers at import and a\n * driver is imported after it, so the old copy is always second, always the\n * judge, and always throws — verified by the bundle corpus on 2026-09-07,\n * which reported v0.17.3's message verbatim (\"Two packages cannot define the\n * same kind.\", no revision clause) while the runtime it ran on was 0.18.\n *\n * So copies that understand `revision` keep their kinds here, under a symbol\n * no released copy looks at, and the legacy map is left to whoever still wants\n * it. An old copy then registers into a map nobody contests, finds no existing\n * entry, and cannot throw — in any load order, which is what the previous fix\n * only claimed. Bumping this suffix again is how a future change to the\n * REGISTRATION PROTOCOL is made; a change to a kind's own definition is still\n * `revision`, among peers that share this map.\n */\nconst KINDS_KEY = Symbol.for(\"@rebasepro/types.resourceKinds.v2\");\n\nfunction registry(): Registry {\n const g = globalThis as unknown as Record<symbol, unknown>;\n let shared = g[GLOBAL_KEY] as { kinds: Map<string, ResourceKindSpec>; declarations: Map<string, ResourceDeclaration> } | undefined;\n if (!shared) {\n // `kinds` is created but never written by this copy: an older copy's\n // own `registry()` returns this object as-is once it exists, and would\n // throw on `undefined.get` if the property were absent.\n shared = { kinds: new Map(), declarations: new Map() };\n g[GLOBAL_KEY] = shared;\n }\n let kinds = g[KINDS_KEY] as Map<string, ResourceKindSpec> | undefined;\n if (!kinds) {\n kinds = new Map();\n g[KINDS_KEY] = kinds;\n }\n return { kinds, legacyKinds: shared.kinds, declarations: shared.declarations };\n}\n\n/**\n * Every kind visible to this copy: the versioned map, plus anything only a\n * legacy copy registered.\n *\n * The fallback is not for Rebase's own kinds — this copy defines all of those —\n * but for a third-party driver built against an older `@rebasepro/types` that\n * registers a kind of its own. Dropping it would make that kind invisible and\n * turn `declareResource` into \"unknown resource kind\" for something genuinely\n * registered.\n */\nfunction visibleKinds(): Map<string, ResourceKindSpec> {\n const { kinds, legacyKinds } = registry();\n if (legacyKinds.size === 0) return kinds;\n const merged = new Map(legacyKinds);\n for (const [k, v] of kinds) merged.set(k, v);\n return merged;\n}\n\n/**\n * Corrections this copy applies on top of a registered kind.\n *\n * Deliberately a module local — per COPY of this package — where the registry\n * above is deliberately shared. A published driver inlines this package into\n * its dist, and the copy it carries compares the shared registry's entry for a\n * kind against its own literal by `JSON.stringify` and throws if they differ\n * (see `registerResourceKind` before revisions existed). That code is in the\n * field and cannot be changed, so the registered literal of any kind that has\n * ever shipped is frozen: change one enumerable key and every bundle built with\n * an older driver dies at driver load on the next image. What a kind actually\n * binds can still be corrected — here, read through `resourceKind()` and\n * everything built on it, invisible to the older copy, which keeps binding the\n * way it did when it was published.\n */\ntype KindAmendment = Partial<Pick<ResourceKindSpec, \"envBases\" | \"envBasesByEngine\" | \"optionKeys\">>;\nconst amendments = new Map<string, KindAmendment>();\n\n/**\n * Correct a registered kind without touching its registered literal.\n *\n * Use this, never an edit to the literal, for a kind that has shipped in a\n * published package. The amendment applies to reads through this copy only.\n */\nexport function amendResourceKind(kind: string, amendment: KindAmendment): void {\n amendments.set(kind, { ...amendments.get(kind), ...amendment });\n}\n\n/** A registered kind as this copy sees it: the shared literal plus this copy's amendments. */\nfunction effectiveKind(spec: ResourceKindSpec): ResourceKindSpec {\n const amendment = amendments.get(spec.kind);\n return amendment ? { ...spec, ...amendment } : spec;\n}\n\n/** `kind:key`, the graph's primary key. */\nfunction declarationId(kind: string, key: string): string {\n return `${kind}:${key}`;\n}\n\n/**\n * Register a resource kind.\n *\n * Idempotent for an identical spec. When a spec for the same kind is already\n * registered and differs, the `revision` decides: the higher one is kept and\n * the other copy is warned about, in either load order. Two different specs at\n * the SAME revision are a genuine conflict — two packages defining one kind, or\n * a change that forgot to bump — and still throw.\n *\n * Both copies in that comparison are peers on `KINDS_KEY`, which is what makes\n * the rule enforceable: a copy old enough not to know `revision` writes to the\n * legacy map instead and never reaches this function's arithmetic. Registering\n * a kind an older copy already put in the legacy map is therefore not a\n * conflict — it is the ordinary case, and `visibleKinds` prefers this one.\n */\nexport function registerResourceKind(spec: ResourceKindSpec): void {\n const kinds = registry().kinds;\n const existing = kinds.get(spec.kind);\n if (!existing) {\n kinds.set(spec.kind, spec);\n return;\n }\n if (JSON.stringify(existing) === JSON.stringify(spec)) return;\n\n const have = existing.revision ?? 0;\n const incoming = spec.revision ?? 0;\n if (have === incoming) {\n throw new Error(\n `Resource kind \"${spec.kind}\" is already registered with a different definition at revision ${have}. ` +\n \"Two packages cannot define the same kind; a newer definition of the same kind must carry a higher `revision`.\"\n );\n }\n const [kept, dropped] = incoming > have ? [spec, existing] : [existing, spec];\n if (kept === spec) kinds.set(spec.kind, spec);\n // No logger below @rebasepro/server, and this runs in browsers too.\n console.warn(\n `[resources] Resource kind \"${spec.kind}\" is registered twice, at revisions ${dropped.revision ?? 0} and ` +\n `${kept.revision ?? 0}; keeping revision ${kept.revision ?? 0}. The older copy is usually @rebasepro/types ` +\n \"inlined in a driver built before the kind changed — rebuild the project with a current driver to remove it.\"\n );\n}\n\n/** Every registered kind, for validators and for `rebase doctor`. */\nexport function resourceKinds(): ResourceKindSpec[] {\n return [...visibleKinds().values()].map(effectiveKind);\n}\n\n/** One registered kind, or undefined. */\nexport function resourceKind(kind: string): ResourceKindSpec | undefined {\n const spec = visibleKinds().get(kind);\n return spec && effectiveKind(spec);\n}\n\n/** Options every kind accepts. */\nexport interface DeclareOptions {\n engine?: string;\n transport?: ResourceTransport;\n label?: string;\n [option: string]: unknown;\n}\n\nconst COMMON_OPTION_KEYS = [\"engine\", \"transport\", \"label\"] as const;\n\n/** Whether an engine is one the kind knows, or an explicit `custom:` opt-out. */\nexport function isValidEngine(spec: ResourceKindSpec, engine: string): boolean {\n return engine.startsWith(\"custom:\") || spec.engines.includes(engine);\n}\n\n/**\n * Declare a resource. The primitive every kind's constructor is built from.\n *\n * Redeclaring the same `kind:key` with a *different* shape throws rather than\n * merging. Merging is what the old storage path did, and it silently discarded\n * one of the two engines — a declaration accepted and then ignored, which is\n * the failure this whole model exists to remove. Redeclaring it identically is\n * fine: a config module evaluated twice must not be an error.\n */\nexport function declareResource(\n kind: string,\n key: string = DEFAULT_RESOURCE_KEY,\n options: DeclareOptions = {}\n): ResourceHandle {\n const spec = resourceKind(kind);\n if (!spec) {\n const known = [...visibleKinds().keys()].sort().join(\", \") || \"none\";\n throw new Error(\n `Unknown resource kind \"${kind}\". Registered kinds: ${known}. ` +\n \"Call registerResourceKind() before declaring one.\"\n );\n }\n\n if (!key || typeof key !== \"string\" || key.trim() === \"\") {\n throw new Error(`A ${kind} needs a non-empty key.`);\n }\n\n const engine = options.engine ?? spec.defaultEngine;\n if (!isValidEngine(spec, engine)) {\n throw new Error(\n `Unknown ${kind} engine \"${engine}\" for \"${key}\". ` +\n `Known engines: ${spec.engines.join(\", \")}. ` +\n `An engine this build does not ship is spelled \"custom:${engine}\", ` +\n \"which says so at the call site rather than failing later.\"\n );\n }\n\n const allowed = new Set<string>([...COMMON_OPTION_KEYS, ...(spec.optionKeys ?? [])]);\n const unknown = Object.keys(options).filter(k => !allowed.has(k));\n if (unknown.length > 0) {\n throw new Error(\n `Unknown option(s) on ${kind} \"${key}\": ${unknown.join(\", \")}. ` +\n `A ${kind} accepts: ${[...allowed].sort().join(\", \")}.`\n );\n }\n\n const extra: Record<string, unknown> = {};\n for (const k of spec.optionKeys ?? []) {\n if (options[k] !== undefined) extra[k] = options[k];\n }\n\n const declaration: ResourceDeclaration = {\n kind,\n key,\n engine,\n transport: options.transport ?? \"server\",\n ...(options.label !== undefined ? { label: options.label } : {}),\n options: Object.freeze(extra)\n };\n\n const id = declarationId(kind, key);\n const previous = registry().declarations.get(id);\n if (previous) {\n if (JSON.stringify(previous) !== JSON.stringify(declaration)) {\n throw new Error(\n `${kind} \"${key}\" is declared twice with different configuration. ` +\n \"Declare it once and export it — two declarations of one resource is \" +\n \"the ambiguity this model exists to remove, so it is refused rather \" +\n \"than merged.\"\n );\n }\n } else {\n registry().declarations.set(id, declaration);\n }\n\n const handle = {\n ...declaration,\n toString() { return key; },\n [BRAND]: true as const\n };\n return handle as ResourceHandle;\n}\n\n/** Every declared resource, in declaration order, optionally filtered by kind. */\nexport function declaredResources(kind?: string): ResourceDeclaration[] {\n const all = [...registry().declarations.values()];\n return kind ? all.filter(r => r.kind === kind) : all;\n}\n\n/**\n * Forget every declaration, keeping registered kinds.\n *\n * For tests and for a CLI that evaluates more than one project in a process.\n * Kinds survive because they are registered by module import, which will not\n * happen a second time.\n */\nexport function resetDeclaredResources(): void {\n registry().declarations.clear();\n}\n\n/**\n * The env-var suffix a resource's bindings use: `__ANALYTICS` for `analytics`,\n * and nothing at all for the default-keyed one.\n *\n * The default takes no suffix so that a project with one database configured\n * through plain `DATABASE_URL` keeps working having declared nothing — the\n * overwhelmingly common project must not have to say so.\n */\nexport function resourceEnvSuffix(key: string): string {\n if (key === DEFAULT_RESOURCE_KEY) return \"\";\n return `__${key.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\")}`;\n}\n\n/**\n * Two resources of a kind whose keys differ but whose env suffixes do not.\n *\n * `media-files` and `media_files` both become `__MEDIA_FILES`, so one would\n * silently read the other's configuration. Returned rather than thrown so the\n * caller can report it with the rest of a validation pass.\n */\nexport function findEnvSuffixCollision(keys: readonly string[]): { a: string; b: string; suffix: string } | null {\n const seen = new Map<string, string>();\n for (const key of keys) {\n const suffix = resourceEnvSuffix(key);\n const previous = seen.get(suffix);\n if (previous !== undefined && previous !== key) return { a: previous, b: key, suffix };\n seen.set(suffix, key);\n }\n return null;\n}\n\n/**\n * The whole graph, as recorded in a manifest and read by a host.\n *\n * `version` is the graph format, not the project's. A host reading a graph it\n * does not understand must say so rather than provision half of it.\n */\nexport interface ResourceGraph {\n version: 1;\n resources: ResourceDeclaration[];\n}\n\n/** The current graph format version. */\nexport const RESOURCE_GRAPH_VERSION = 1 as const;\n\n/**\n * Build a graph from the current declarations, sorted for a stable diff.\n *\n * `usedBy` maps a `kind:key` id to the things that reach it. The derive step\n * supplies it after evaluating collections; a runtime building the graph at\n * boot passes nothing and gets declarations alone, which is all it binds from.\n */\nexport function buildResourceGraph(usedBy?: ReadonlyMap<string, readonly string[]>): ResourceGraph {\n const resources = declaredResources().slice().sort(\n (a, b) => a.kind.localeCompare(b.kind) || a.key.localeCompare(b.key)\n ).map(r => {\n const users = usedBy?.get(declarationId(r.kind, r.key));\n return users && users.length > 0 ? { ...r, usedBy: [...users].sort() } : r;\n });\n return { version: RESOURCE_GRAPH_VERSION, resources };\n}\n\n/** `kind:key`, the id `usedBy` maps are keyed by. Exported for the derive step. */\nexport function resourceId(kind: string, key: string): string {\n return declarationId(kind, key);\n}\n\n/**\n * The environment variables worth writing for a resource, given its engine.\n *\n * Falls back to every base the kind reads when the engine is unknown — a\n * `custom:` engine gets the full list rather than an empty one, because\n * guessing narrow would silently omit the variable it actually needs.\n */\nexport function envBasesForResource(declaration: ResourceDeclaration): readonly string[] {\n const spec = resourceKind(declaration.kind);\n if (!spec) return [];\n return spec.envBasesByEngine?.[declaration.engine] ?? spec.envBases;\n}\n","/**\n * Describes a named storage backend — a place files live.\n *\n * Declared once and shared front + back: the frontend uses it to decide\n * transport (HTTP proxy vs direct SDK), the backend uses the same `key`\n * to resolve a StorageController, and collection properties reference\n * a definition by its `key` via `StorageConfig.storageSource`.\n *\n * This mirrors the {@link DataSourceDefinition} pattern used for databases.\n *\n * @group Models\n */\n\n/**\n * The default storage source key, used when a property does not specify\n * a `storageSource`. Shared by the frontend and backend registries so\n * both agree on \"the default storage backend\".\n * @group Models\n */\nexport const DEFAULT_STORAGE_SOURCE_KEY = \"(default)\";\n\n/**\n * How the *frontend* reaches a storage backend.\n *\n * - `\"server\"` — through the Rebase backend REST API (`/api/storage`).\n * The backend holds the actual `StorageController` and routes by\n * storage-source key. This is the default and covers Local, S3, GCS,\n * and any other server-mediated engine.\n * - `\"direct\"` — straight from the client to the external backend via\n * its own SDK (e.g. Firebase Storage via `@firebase/storage`).\n * The Rebase backend is **not** in the upload/download path.\n *\n * @group Models\n */\nexport type StorageSourceTransport = \"server\" | \"direct\";\n\n/**\n * Declarative definition of a storage source — a named place files live.\n *\n * Declared once and shared front and back: the frontend uses it to decide\n * transport (client HTTP proxy vs direct provider SDK), the backend uses\n * the same `key` to resolve a `StorageController`, and collection\n * properties reference a definition by its `key` via\n * `StorageConfig.storageSource`.\n *\n * @group Models\n */\nexport interface StorageSourceDefinition {\n /**\n * Unique identifier for this storage source. Collection properties\n * point at it via `StorageConfig.storageSource`.\n * Defaults to {@link DEFAULT_STORAGE_SOURCE_KEY}.\n */\n key: string;\n\n /**\n * The engine backing this storage source (e.g. `\"local\"`, `\"s3\"`,\n * `\"gcs\"`, `\"firebase\"`, `\"azure\"`, or a custom id).\n */\n engine: string;\n\n /**\n * The credential set this source signs with, when several sources share one.\n *\n * ## What it is for\n *\n * Every binding a bucket needs is read per key — `S3_BUCKET__MEDIA`,\n * `S3_ACCESS_KEY_ID__MEDIA`, and so on. That is right for the bucket *name*,\n * which is different for every source by definition, and wrong for the\n * credentials, which usually are not: fifteen buckets on one MinIO install\n * meant fifteen copies of the same endpoint, access key and secret — ninety\n * variables where eighteen would do, and one key rotation became fifteen\n * paired edits where a single missed one fails at upload time with an opaque\n * signing error.\n *\n * Naming an account here lets the *account-scoped* bindings fall back to\n * `<BASE>__<ACCOUNT>` when no per-key value is set. The bucket name never\n * falls back: it is what distinguishes one source from another.\n *\n * ## Why it does not fall back to the bare variable\n *\n * A source with no `account` reads only its own suffixed names, exactly as\n * before — so every project that predates this is wire-identical. The\n * unsuffixed `S3_ACCESS_KEY_ID` belongs to the *default* source, and letting\n * a named bucket inherit it would mean a typo'd key silently signs with\n * another source's credentials. Two forms, both explicit, opt-in.\n */\n account?: string;\n\n /**\n * How the frontend reaches this storage. Defaults to `\"server\"`.\n *\n * When `\"direct\"`, the client uses a provider-specific SDK\n * (e.g. `@firebase/storage`) and the backend does not proxy\n * upload/download traffic for this source.\n */\n transport: StorageSourceTransport;\n\n /**\n * Serve unqualified uploads — a storage property naming no `storageSource`\n * — from this source.\n *\n * Declared, never inferred. A project with named buckets and no default\n * used to have one chosen for it by declaration order, with a warning, and\n * the choice differed between development and production because the\n * synthesized local default is dropped in production and the promotion was\n * not. Where the files land is the author's decision; boot now fails\n * without one.\n */\n default?: boolean;\n\n /** Human-readable label for the UI (e.g. \"Firebase Storage\", \"S3 Media\"). */\n label?: string;\n}\n\n/**\n * A resolved storage source: the single source of truth that the frontend\n * router and backend registry both derive from.\n *\n * @group Models\n */\nexport interface ResolvedStorageSource {\n /** Storage source key (routing key, shared front + back). */\n key: string;\n /** Engine backing the source. */\n engine: string;\n /** Frontend transport. */\n transport: StorageSourceTransport;\n /** Human-readable label. */\n label?: string;\n}\n\n/**\n * The environment-variable suffix for a storage or data source key.\n *\n * `\"\"` for the default source — so a single-bucket project keeps configuring\n * plain `S3_BUCKET` — and `__<KEY>` for every named one, uppercased with\n * non-alphanumerics collapsed to underscores: `media-cdn` → `S3_BUCKET__MEDIA_CDN`.\n *\n * The rule derives the variable name from the declared key rather than\n * discovering keys by scanning the environment. Scanning would have to guess how\n * `S3_BUCKET__MEDIA_CDN` splits into a key; deriving cannot be ambiguous, and a\n * typo surfaces as a missing source at boot instead of a silently ignored\n * variable.\n *\n * It lives in this package, with no dependencies, because four things must agree\n * on it exactly: the CLI (validating a build), the runtime (reading its own\n * environment), the control plane (writing a tenant's Secret), and the docs. A\n * second implementation of a naming convention is a second chance to disagree.\n *\n * @group Models\n */\nexport function storageEnvSuffix(key: string, defaultKey: string = DEFAULT_STORAGE_SOURCE_KEY): string {\n if (!key || key === defaultKey) return \"\";\n const normalized = key\n .replace(/[^A-Za-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .toUpperCase();\n if (!normalized) {\n throw new Error(\n `Source key \"${key}\" cannot be turned into an environment variable name. ` +\n \"Use a key containing at least one letter or digit.\"\n );\n }\n return `__${normalized}`;\n}\n\n/**\n * Two distinct keys that collapse onto the same variable name, or `null`.\n *\n * `media-cdn` and `media_cdn` are different source keys but the same suffix, so\n * without this one of them silently reads the other's configuration. Returns the\n * offending pair rather than throwing, so each caller can raise it in its own\n * idiom — a `BundleError` at boot, a build failure in the CLI, a rejected deploy\n * in a control plane.\n *\n * @group Models\n */\nexport function findStorageSuffixCollision(\n keys: string[],\n defaultKey: string = DEFAULT_STORAGE_SOURCE_KEY\n): { a: string; b: string; suffix: string } | null {\n const seen = new Map<string, string>();\n for (const key of keys) {\n const suffix = storageEnvSuffix(key, defaultKey);\n const existing = seen.get(suffix);\n if (existing !== undefined && existing !== key) {\n return { a: existing, b: key, suffix };\n }\n seen.set(suffix, key);\n }\n return null;\n}\n","/**\n * The kinds Rebase ships, and the constructors a project declares them with.\n *\n * Each kind is registered rather than hardcoded, so a fourth one arrives\n * without editing a manifest schema, a validator and a switch statement. That\n * cost is precisely why databases and buckets ended up declared in different\n * files with different rules — the cheapest thing to do was always to bolt the\n * new kind onto whichever home was nearest.\n *\n * A kind owns its engine list. `custom:<id>` is always accepted, so a build\n * that ships an engine this package has never heard of says so at the call site\n * instead of looking like a typo of one that exists.\n */\nimport {\n DEFAULT_RESOURCE_KEY,\n declareResource,\n declaredResources,\n amendResourceKind,\n registerResourceKind,\n type DeclareOptions,\n type ResourceDeclaration,\n type ResourceHandle\n} from \"./resources\";\nimport { DEFAULT_DATA_SOURCE_KEY, type DataSourceDefinition } from \"./data_source\";\nimport { DEFAULT_STORAGE_SOURCE_KEY, type StorageSourceDefinition } from \"./storage_source\";\n\n// ── database ─────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n // There is no single frozen literal for this kind, which is why it carries a\n // revision. 0.17.0 and 0.17.1 shipped `optionKeys: [\"databaseId\",\n // \"migrations\"]`; 0.17.2 added \"extensions\" to the literal itself, before the\n // rule against that existed. So two different objects are inlined in drivers\n // that are in the field, and no choice of literal can equal both: a runtime\n // that matched one threw `already registered with a different definition` at\n // the other and refused to boot. Two tenants crash-looped for six and a half\n // days on exactly that.\n //\n // `revision` is what resolves it. An older copy — whichever literal it\n // carries — is at revision 0, loses to this one, and warns instead of\n // throwing. Corrections still go in the amendment below; this number moves\n // only when the literal itself has to, and every published copy predating the\n // move is thereby handled.\n revision: 1,\n kind: \"database\",\n engines: [\"postgres\", \"mongodb\", \"firestore\", \"sqlite\"],\n defaultEngine: \"postgres\",\n envBases: [\"DATABASE_URL\", \"REBASE_DRIVER\", \"REBASE_DB_POOL_MAX\"],\n optionKeys: [\"databaseId\", \"migrations\", \"extensions\"],\n implicitDefault: true\n});\n// What a database actually binds from. The 0.17.3 list named two variables\n// the resolver never read and omitted five it does (25f1a97e3).\namendResourceKind(\"database\", {\n envBases: [\n \"DATABASE_URL\",\n \"DATABASE_READ_URL\",\n \"ADMIN_CONNECTION_STRING\",\n \"REBASE_DRIVER\",\n \"DB_POOL_MAX\",\n \"DB_POOL_IDLE_TIMEOUT\",\n \"DB_POOL_CONNECT_TIMEOUT\"\n ]\n});\n\n/** Options a database accepts beyond the common ones. */\nexport interface DatabaseOptions extends DeclareOptions {\n /**\n * The physical database or schema within the engine, when it differs from\n * the engine's own default. Threaded to drivers as `databaseId`.\n */\n databaseId?: string;\n /** Directory of migration files, relative to the config directory. */\n migrations?: string;\n /**\n * Server extensions Rebase may install on this database.\n *\n * A permission, not a request: naming one grants leave to run\n * `CREATE EXTENSION IF NOT EXISTS <name>`, and Rebase issues it only when\n * something in the schema actually needs it. Naming an extension nothing\n * needs installs nothing.\n *\n * It has to be said out loud because installing an extension is a decision\n * with a deployment behind it — the image has to ship the library, the role\n * has to be allowed to install it, and a managed provider has to have it on\n * an allow-list. Rebase cannot see any of that from inside the connection,\n * so the answer comes from whoever chose the database.\n *\n * Today `vector` is the one that matters: a `{ type: \"vector\" }` property\n * compiles to a `VECTOR(n)` column, which does not exist until pgvector is\n * installed. Without this, Rebase creates the column and lets Postgres\n * refuse, naming the option.\n *\n * ```ts\n * export const main = database({ extensions: [\"vector\"] });\n * ```\n *\n * `pg_trgm` and `unaccent` are not on this list and need no permission: a\n * `search` block installs them unasked, because they are contrib modules\n * present in every Postgres distribution. pgvector is a separate build that\n * a stock `postgres:18` does not carry.\n */\n extensions?: string[];\n}\n\n/** A database handle. Collections point at it via `dataSource`. */\nexport type DatabaseHandle = ResourceHandle;\n\n/**\n * Declare a database.\n *\n * ```ts\n * export const main = database(); // the default one\n * export const analytics = database(\"analytics\"); // reads DATABASE_URL__ANALYTICS\n * export const withPgv = database({ extensions: [\"vector\"] }); // the default one, configured\n * ```\n *\n * The third form exists because the default database has no name to pass, and\n * the alternative was `database(\"(default)\", { … })` — writing out an internal\n * sentinel to reach the options. A key is a string and options are an object,\n * so the two can never be confused for one another.\n */\nexport function database(options?: DatabaseOptions): DatabaseHandle;\nexport function database(key?: string, options?: DatabaseOptions): DatabaseHandle;\nexport function database(\n keyOrOptions: string | DatabaseOptions = DEFAULT_RESOURCE_KEY,\n options: DatabaseOptions = {}\n): DatabaseHandle {\n return typeof keyOrOptions === \"string\"\n ? declareResource(\"database\", keyOrOptions, options)\n : declareResource(\"database\", DEFAULT_RESOURCE_KEY, keyOrOptions);\n}\n\n/**\n * The extensions the project's databases gave Rebase leave to install.\n *\n * A flat union rather than a per-database answer, because the surfaces that ask\n * — `rebase db push` and the boot schema-ensure — drive one connection and\n * generate one `schema.sql` for every collection regardless of `dataSource`.\n * Splitting the permission by data source would be a distinction the rest of\n * that pipeline does not make, and a false precision is worse than none.\n *\n * Empty for a project that declared nothing, which is every project that has\n * not opted in — so this reads as a refusal by default, on purpose.\n */\nexport function declaredDatabaseExtensions(): readonly string[] {\n const names = new Set<string>();\n for (const declaration of declaredResources(\"database\")) {\n const declared = declaration.options.extensions;\n if (!Array.isArray(declared)) continue;\n for (const name of declared) {\n if (typeof name === \"string\" && name.trim()) names.add(name.trim());\n }\n }\n return [...names].sort();\n}\n\n// ── bucket ───────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n // FROZEN at the 0.17.3 literal, for the reason given on `database`.\n kind: \"bucket\",\n engines: [\"local\", \"s3\", \"gcs\", \"azure\", \"firebase\"],\n defaultEngine: \"local\",\n envBases: [\"S3_BUCKET\", \"GCS_BUCKET\", \"STORAGE_BUCKET\", \"STORAGE_PUBLIC_URL\"],\n envBasesByEngine: {\n local: [\"STORAGE_BUCKET\"],\n s3: [\"S3_BUCKET\", \"STORAGE_ENDPOINT\", \"STORAGE_REGION\", \"STORAGE_PUBLIC_URL\"],\n gcs: [\"GCS_BUCKET\", \"STORAGE_PUBLIC_URL\"],\n azure: [\"STORAGE_BUCKET\", \"STORAGE_PUBLIC_URL\"],\n firebase: [\"STORAGE_BUCKET\", \"STORAGE_PUBLIC_URL\"]\n },\n optionKeys: [\"publicRead\", \"prefix\", \"account\"],\n implicitDefault: false\n});\n// What a bucket actually binds from, per engine (25f1a97e3), plus `default`:\n// the registry no longer promotes a lone named bucket, so a project needs a way\n// to say which one serves unqualified uploads. The literal above is frozen, so\n// the new option key arrives here.\namendResourceKind(\"bucket\", {\n optionKeys: [\"publicRead\", \"prefix\", \"account\", \"default\"],\n envBases: [\n \"STORAGE_TYPE\",\n \"STORAGE_PATH\",\n \"S3_BUCKET\",\n \"S3_REGION\",\n \"S3_ACCESS_KEY_ID\",\n \"S3_SECRET_ACCESS_KEY\",\n \"S3_ENDPOINT\",\n \"S3_FORCE_PATH_STYLE\",\n \"GCS_BUCKET\",\n \"GCS_PROJECT_ID\",\n \"GCS_KEY_FILENAME\"\n ],\n envBasesByEngine: {\n local: [\"STORAGE_TYPE\", \"STORAGE_PATH\"],\n s3: [\n \"STORAGE_TYPE\",\n \"S3_BUCKET\",\n \"S3_REGION\",\n \"S3_ACCESS_KEY_ID\",\n \"S3_SECRET_ACCESS_KEY\",\n \"S3_ENDPOINT\",\n \"S3_FORCE_PATH_STYLE\"\n ],\n gcs: [\"STORAGE_TYPE\", \"GCS_BUCKET\", \"GCS_PROJECT_ID\", \"GCS_KEY_FILENAME\"],\n azure: [],\n firebase: []\n }\n});\n\n/** Options a bucket accepts beyond the common ones. */\nexport interface BucketOptions extends DeclareOptions {\n /**\n * Whether objects are world-readable by default.\n *\n * Declared rather than inferred from the engine, because the two have\n * disagreed before: a private object served through a cacheable public URL\n * is a data leak that nothing errors on.\n */\n publicRead?: boolean;\n /** Key prefix within the bucket, for sharing one bucket between sources. */\n prefix?: string;\n /**\n * Serve unqualified uploads — a storage property with no `storageSource` —\n * from this bucket.\n *\n * A project that declares only `bucket(\"media\")` has no default bucket, and\n * the registry used to promote the one it found with a warning. That is a\n * decision about where a user's files land, made by the framework, on the\n * strength of declaration order; it also produced two different\n * destinations either side of a deploy, because the synthesized local\n * default is dropped in production and the promotion is not. So it is now\n * a boot error, and this is one of the two ways to answer it — the other\n * being `bucket()`, which declares the default bucket itself.\n */\n default?: boolean;\n /**\n * The credential set this bucket signs with, when several share one.\n *\n * `bucket(\"media\", { engine: \"s3\", account: \"minio\" })` keeps reading its own\n * `S3_BUCKET__MEDIA` — the bucket name is what distinguishes one source from\n * another and never falls back — while the provider-level variables\n * (`S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_ENDPOINT`, `S3_REGION`,\n * `S3_FORCE_PATH_STYLE`) fall back to `__MINIO` when no per-key value is set.\n *\n * Fifteen buckets on one install go from ninety variables to eighteen, and\n * rotating the key becomes one edit. A per-bucket value still wins, so a\n * single source can move to another provider without breaking the rest off\n * their shared account.\n */\n account?: string;\n}\n\n/** A bucket handle. Storage properties point at it via `storageSource`. */\nexport type BucketHandle = ResourceHandle;\n\n/**\n * Declare a bucket.\n *\n * ```ts\n * export const uploads = bucket({ engine: \"s3\" }); // the default one\n * export const media = bucket(\"media\", { transport: \"direct\" });\n * ```\n *\n * `transport: \"direct\"` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n *\n * The options-only form exists for the same reason `database`'s does: the\n * default bucket has no name to pass, and without it the only way to configure\n * one was `bucket(\"(default)\", { … })` — writing out an internal sentinel to\n * reach the options. Passing options where a key belongs used to throw \"a\n * bucket needs a non-empty key\", which names neither the mistake nor the fix.\n */\nexport function bucket(options?: BucketOptions): BucketHandle;\nexport function bucket(key?: string, options?: BucketOptions): BucketHandle;\nexport function bucket(\n keyOrOptions: string | BucketOptions = DEFAULT_RESOURCE_KEY,\n options: BucketOptions = {}\n): BucketHandle {\n return typeof keyOrOptions === \"string\"\n ? declareResource(\"bucket\", keyOrOptions, options)\n : declareResource(\"bucket\", DEFAULT_RESOURCE_KEY, keyOrOptions);\n}\n\n// ── topic ────────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n // FROZEN at the 0.17.3 literal, for the reason given on `database`.\n kind: \"topic\",\n // `jobs` is the durable local implementation: a topic fans out to one job\n // row per subscription, so each subscriber retries on its own schedule and\n // a failure is a row somebody can look at rather than a lost message.\n engines: [\"jobs\"],\n defaultEngine: \"jobs\",\n envBases: [\"REBASE_TOPIC_URL\"],\n optionKeys: [\"delivery\", \"maxAttempts\"],\n implicitDefault: false\n});\n// Nothing. A topic on the `jobs` engine is rows in the project's own database\n// and binds from no variable of its own. The literal above says\n// `REBASE_TOPIC_URL` — a name nothing in either repository read, which\n// `rebase status` then printed as a variable somebody could set — and it has to\n// keep saying it, because a driver ≤ 0.17.3 compares that object and throws.\n// The gate in `resource-env-bases.test.ts` covers every registered kind through\n// the amended view, so a phantom name fails a build rather than reaching a\n// developer.\namendResourceKind(\"topic\", { envBases: [] });\n\n/**\n * How hard the runtime tries to deliver.\n *\n * Only `at-least-once` is implemented, and it is the honest name for what a\n * retrying queue does: a handler must tolerate seeing the same event twice.\n * `at-most-once` is listed so a future transport can offer it without the\n * option changing shape, and is refused today rather than silently upgraded.\n */\nexport type TopicDelivery = \"at-least-once\" | \"at-most-once\";\n\n/** Options a topic accepts beyond the common ones. */\nexport interface TopicOptions extends DeclareOptions {\n delivery?: TopicDelivery;\n /** Attempts per subscription before a message is left failed. Default 5. */\n maxAttempts?: number;\n}\n\n/**\n * What a subscription does with an event.\n *\n * `attempt` counts from 1. Worth branching on: the first delivery and the\n * fourth are the same call, but the fourth is where it is worth logging loudly.\n */\nexport type TopicHandler<T> = (event: T, context: { attempt: number; topic: string; subscription: string }) => Promise<void> | void;\n\n/** A declared subscription, as recorded in the graph and wired at boot. */\nexport interface TopicSubscription<T = unknown> {\n topic: string;\n name: string;\n handler: TopicHandler<T>;\n maxAttempts?: number;\n}\n\n/**\n * What a topic publishes through.\n *\n * Installed by `@rebasepro/server` at boot. Absent — in the CLI evaluating\n * config to derive the graph, or in a unit test — publishing throws a message\n * naming the cause, rather than resolving and dropping the event. A publish\n * that silently does nothing is the failure mode a queue exists to prevent.\n */\nexport interface TopicRuntime {\n publish(topic: string, event: unknown): Promise<void>;\n}\n\nconst runtimeHolder: { current: TopicRuntime | null } = { current: null };\n\n/** Install the transport topics publish through. Called by the server at boot. */\nexport function setTopicRuntime(runtime: TopicRuntime | null): void {\n runtimeHolder.current = runtime;\n}\n\nconst subscriptions: TopicSubscription[] = [];\n\n/** Every declared subscription, for the worker to wire and the graph to record. */\nexport function declaredSubscriptions(topic?: string): TopicSubscription[] {\n return topic ? subscriptions.filter(s => s.topic === topic) : subscriptions.slice();\n}\n\n/** Forget declared subscriptions. For tests, alongside `resetDeclaredResources`. */\nexport function resetDeclaredSubscriptions(): void {\n subscriptions.length = 0;\n}\n\n/** A topic handle, carrying its payload type. */\nexport interface TopicHandle<T> extends ResourceHandle {\n /**\n * Publish an event.\n *\n * Resolves once the event is durably recorded for every subscription, not\n * once they have run. Enqueued inside a transaction that rolls back, it was\n * never published.\n */\n publish(event: T): Promise<void>;\n /**\n * Declare a subscription.\n *\n * The name is its identity: it is what the job row records, what a retry\n * counts against, and what a second subscription must not collide with.\n */\n subscription(name: string, handler: TopicHandler<T>, options?: { maxAttempts?: number }): void;\n}\n\n/**\n * Declare a topic.\n *\n * ```ts\n * export const signups = topic<{ userId: string }>(\"signups\");\n * signups.subscription(\"send-welcome\", async (event) => { … });\n * await signups.publish({ userId });\n * ```\n */\nexport function topic<T = unknown>(key: string, options: TopicOptions = {}): TopicHandle<T> {\n if (options.delivery === \"at-most-once\") {\n throw new Error(\n `Topic \"${key}\" asks for at-most-once delivery, which no shipped transport implements. ` +\n \"The durable queue behind topics retries, so it is at-least-once and a handler must \" +\n \"tolerate seeing an event twice. Refused rather than quietly given the other guarantee.\"\n );\n }\n const handle = declareResource(\"topic\", key, options);\n\n return {\n ...handle,\n toString() { return key; },\n async publish(event: T): Promise<void> {\n const runtime = runtimeHolder.current;\n if (!runtime) {\n throw new Error(\n `Cannot publish to topic \"${key}\": no topic runtime is installed. ` +\n \"Publishing works inside a running Rebase backend; this looks like config \" +\n \"being evaluated outside one (a build, a script, or a test without a harness).\"\n );\n }\n await runtime.publish(key, event);\n },\n subscription(name: string, handler: TopicHandler<T>, subOptions: { maxAttempts?: number } = {}): void {\n if (!name || name.trim() === \"\") {\n throw new Error(`A subscription on topic \"${key}\" needs a non-empty name.`);\n }\n if (subscriptions.some(s => s.topic === key && s.name === name)) {\n throw new Error(\n `Topic \"${key}\" already has a subscription named \"${name}\". ` +\n \"The name is what a job row records and what a retry counts against, so two \" +\n \"cannot share one.\"\n );\n }\n subscriptions.push({\n topic: key,\n name,\n handler: handler as TopicHandler<unknown>,\n ...(subOptions.maxAttempts !== undefined ? { maxAttempts: subOptions.maxAttempts } : {})\n });\n }\n } as TopicHandle<T>;\n}\n\n// ── cron ─────────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n kind: \"cron\",\n // The in-process scheduler, claiming each slot in `rebase.cron_claims` so\n // several instances of one deployment run a slot once. It is the only\n // engine because it is the only one that exists; an external scheduler\n // (a platform's cron, a Kubernetes CronJob) would be a second engine that\n // triggers the same handler over HTTP, and it can register itself.\n engines: [\"scheduler\"],\n defaultEngine: \"scheduler\",\n // Code, not configuration: a cron binds from no variable. It is in the\n // graph so a host knows a project's schedules BEFORE running it, which is\n // what lets a console show them and a placement decision read them.\n envBases: [],\n optionKeys: [\"schedule\", \"timezone\", \"description\", \"enabled\", \"timeoutSeconds\", \"catchUpWindowSeconds\"],\n implicitDefault: false\n});\n\n/** What a cron declaration records, beyond its handler. */\nexport interface CronResourceOptions extends DeclareOptions {\n /** Five-field cron expression, e.g. `0 3 * * *`. */\n schedule: string;\n /**\n * IANA zone the schedule is read in, e.g. `Europe/Madrid`.\n *\n * Without it the schedule is read in the process's own zone, which is\n * whatever the host happens to be set to — UTC in nearly every container,\n * the developer's own on a laptop. \"3 AM\" then means two different hours\n * either side of a deploy. Naming the zone makes the declaration mean one\n * thing everywhere.\n */\n timezone?: string;\n description?: string;\n enabled?: boolean;\n timeoutSeconds?: number;\n catchUpWindowSeconds?: number;\n}\n\n/**\n * Declare a cron, as the scheduler's `defineCron` does on its way through.\n *\n * Projects do not call this: `defineCron` in `@rebasepro/server` does, so a\n * cron file is both the handler and the declaration — one file, one name, and\n * the graph derived from it says what a host needs to know without evaluating\n * the handler. Exported so the derive step and the scheduler spell the\n * declaration identically.\n */\nexport function declareCron(name: string, options: CronResourceOptions): ResourceHandle {\n if (typeof options.schedule !== \"string\" || options.schedule.trim() === \"\") {\n throw new Error(`Cron \"${name}\" needs a schedule — a five-field cron expression such as \"0 3 * * *\".`);\n }\n return declareResource(\"cron\", name, options);\n}\n\n// ── function ─────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n kind: \"function\",\n // Mounted by this runtime at `/api/functions/<name>`. A host that runs a\n // function elsewhere — an edge runtime, say — is a second engine, and the\n // bundle's `portable` analysis already says which ones could move.\n engines: [\"http\"],\n defaultEngine: \"http\",\n envBases: [],\n optionKeys: [\"portable\", \"requires\", \"file\"],\n implicitDefault: false\n});\n\n/**\n * What a function declaration records.\n *\n * Recorded by the derive step from the bundler's static analysis rather than\n * by evaluating the function module: a function's handler is a Hono app that\n * only needs to exist at request time, and evaluating it at build time would\n * run its module-scope code in a process with none of its environment.\n */\nexport interface FunctionResourceOptions extends DeclareOptions {\n /** Path inside the project, so a host can point at the file. */\n file?: string;\n /** `false` when the source imports a Node built-in or a package that needs one. */\n portable?: boolean;\n /** Why it is not portable — one short phrase per reason. */\n requires?: string[];\n}\n\n/** Declare a function. Called by the derive step, not by projects. */\nexport function declareFunction(name: string, options: FunctionResourceOptions = {}): ResourceHandle {\n return declareResource(\"function\", name, options);\n}\n\n// ── queue ────────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n kind: \"queue\",\n // Same durable queue topics ride on: a row per job, claimed with\n // `FOR UPDATE SKIP LOCKED`, retried on a backoff, kept when it gives up.\n engines: [\"jobs\"],\n defaultEngine: \"jobs\",\n envBases: [],\n optionKeys: [\"maxAttempts\"],\n implicitDefault: false\n});\n\n/** Options a queue accepts beyond the common ones. */\nexport interface QueueOptions extends DeclareOptions {\n /** Attempts before a job is left failed. Default 5. */\n maxAttempts?: number;\n}\n\n/** What a queue's handler receives. `attempt` counts from 1. */\nexport type QueueHandler<T> = (\n payload: T,\n context: { attempt: number; queue: string; jobId: string }\n) => Promise<void> | void;\n\n/** Per-job options at enqueue time. */\nexport interface QueueEnqueueOptions {\n /** Earliest time the job may run. Defaults to now. */\n runAt?: Date;\n /** Attempts for this job, overriding the queue's. */\n maxAttempts?: number;\n}\n\n/**\n * What a queue enqueues through.\n *\n * Installed by `@rebasepro/server` at boot, alongside the topic runtime.\n * Absent — config evaluated by the CLI, a unit test — enqueueing throws with\n * the cause named, rather than resolving and dropping the job.\n */\nexport interface QueueRuntime {\n enqueue(queue: string, payload: unknown, options?: QueueEnqueueOptions): Promise<{ id: string }>;\n}\n\nconst queueRuntimeHolder: { current: QueueRuntime | null } = { current: null };\n\n/** Install the transport queues enqueue through. Called by the server at boot. */\nexport function setQueueRuntime(runtime: QueueRuntime | null): void {\n queueRuntimeHolder.current = runtime;\n}\n\n/** A queue's handler, as recorded for the worker to wire. */\nexport interface QueueConsumer<T = unknown> {\n queue: string;\n handler: QueueHandler<T>;\n}\n\nconst queueConsumers = new Map<string, QueueConsumer>();\n\n/** Every declared queue handler, for the worker to wire. */\nexport function declaredQueueConsumers(): QueueConsumer[] {\n return [...queueConsumers.values()];\n}\n\n/** Forget declared queue handlers. For tests, alongside `resetDeclaredResources`. */\nexport function resetDeclaredQueueConsumers(): void {\n queueConsumers.clear();\n}\n\n/** A queue handle, carrying its payload type. */\nexport interface QueueHandle<T> extends ResourceHandle {\n /**\n * Put a job on the queue.\n *\n * Resolves once the job is durably recorded, not once it has run. A row\n * insert, so enqueued inside a transaction that rolls back it was never\n * enqueued.\n */\n enqueue(payload: T, options?: QueueEnqueueOptions): Promise<{ id: string }>;\n /**\n * Declare the handler.\n *\n * One per queue: a queue is a work list with one consumer, which is what\n * separates it from a topic. Work that several things must react to is a\n * topic with several subscriptions.\n */\n handler(fn: QueueHandler<T>): void;\n}\n\n/**\n * Declare a queue.\n *\n * ```ts\n * export const thumbnails = queue<{ key: string }>(\"thumbnails\");\n * thumbnails.handler(async ({ key }) => { … });\n * await thumbnails.enqueue({ key }, { runAt: new Date(Date.now() + 60_000) });\n * ```\n *\n * The difference from a topic is the number of consumers: a queue has one, a\n * topic fans out to every subscription. Both ride on the durable job queue, so\n * declaring either turns it on.\n */\nexport function queue<T = unknown>(key: string, options: QueueOptions = {}): QueueHandle<T> {\n const handle = declareResource(\"queue\", key, options);\n\n return {\n ...handle,\n toString() { return key; },\n async enqueue(payload: T, enqueueOptions?: QueueEnqueueOptions): Promise<{ id: string }> {\n const runtime = queueRuntimeHolder.current;\n if (!runtime) {\n throw new Error(\n `Cannot enqueue on queue \"${key}\": no queue runtime is installed. ` +\n \"Enqueueing works inside a running Rebase backend; this looks like config \" +\n \"being evaluated outside one (a build, a script, or a test without a harness).\"\n );\n }\n return runtime.enqueue(key, payload, enqueueOptions);\n },\n handler(fn: QueueHandler<T>): void {\n if (queueConsumers.has(key)) {\n throw new Error(\n `Queue \"${key}\" already has a handler. A queue has exactly one consumer; ` +\n \"work that several things react to is a topic with several subscriptions.\"\n );\n }\n queueConsumers.set(key, { queue: key, handler: fn as QueueHandler<unknown> });\n }\n } as QueueHandle<T>;\n}\n\n// ── Handing declarations to the readers ──────────────────────────────────────\n\n/**\n * One declaration, as the data layer's definition.\n *\n * There is exactly one of these per kind, and everything that needs a\n * definition goes through it — the frontend, the managed runtime's boot path,\n * and an ejected project's own entrypoint. That is not tidiness: the mapping\n * used to exist twice, once here and once in `@rebasepro/server`'s\n * `graphToStorageSources`, and the two disagreed. The server's copy carried a\n * bucket's `account`; this one dropped it, so a bucket declared with shared\n * credentials resolved them on the managed runtime and resolved *nothing* in an\n * ejected backend — the source was skipped and every upload to it answered 501.\n *\n * A field-by-field map is one line away from that failure at all times, so\n * there is now one line to keep right instead of two to keep equal.\n */\nexport function resourceToDataSource(declaration: ResourceDeclaration): DataSourceDefinition {\n return {\n // The graph and the data layer spell \"the unnamed one\" identically\n // today, but they are separate constants and nothing stops them\n // drifting. Mapped explicitly so a divergence is a compile error rather\n // than a default database that silently fails to bind.\n key: declaration.key === DEFAULT_RESOURCE_KEY ? DEFAULT_DATA_SOURCE_KEY : declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n ...(typeof declaration.options.databaseId === \"string\"\n ? { databaseId: declaration.options.databaseId }\n : {}),\n ...(declaration.label !== undefined ? { label: declaration.label } : {})\n };\n}\n\n/** One declaration, as the storage layer's definition. See {@link resourceToDataSource}. */\nexport function resourceToStorageSource(declaration: ResourceDeclaration): StorageSourceDefinition {\n return {\n key: declaration.key === DEFAULT_RESOURCE_KEY ? DEFAULT_STORAGE_SOURCE_KEY : declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n // Carried, or the declaration's `account` is accepted at the call site\n // and lost on the way to the reader — a declared option that does\n // nothing, which is the exact failure this whole model exists to remove.\n ...(typeof declaration.options.account === \"string\"\n ? { account: declaration.options.account }\n : {}),\n ...(declaration.options.default === true ? { default: true } : {}),\n ...(declaration.label !== undefined ? { label: declaration.label } : {})\n };\n}\n\n/**\n * The declared databases, as definitions.\n *\n * Both the frontend and a project's own backend entrypoint read this. The\n * frontend needs to know which sources exist and how they are reached — a\n * `direct`-transport source is one the browser talks to itself — and it imports\n * the same config package the backend does. Without these it would mean writing\n * the list a second time, by hand, next to the declarations, which is precisely\n * the two-homes problem this model removed everywhere else.\n *\n * ```tsx\n * import \"../config/resources\"; // registers them\n * import { declaredDataSources, declaredStorageSources } from \"@rebasepro/types\";\n *\n * <Rebase dataSources={declaredDataSources()} storageSources={declaredStorageSources()} />\n * ```\n *\n * The import is what registers them, so a bundler that drops an unused module\n * would leave this empty — hence the side-effect import above rather than a\n * bare re-export.\n */\nexport function declaredDataSources(): DataSourceDefinition[] {\n return declaredResources(\"database\").map(resourceToDataSource);\n}\n\n/** The declared buckets, as definitions. */\nexport function declaredStorageSources(): StorageSourceDefinition[] {\n return declaredResources(\"bucket\").map(resourceToStorageSource);\n}\n","/**\n * How a collection points at a UI component without the backend learning about React.\n *\n * This file is the hinge the BaaS/admin split turns on. `ComponentRef` is named\n * by a property's `admin` block (`admin.Field`, `admin.Preview`, `admin.Filter`)\n * and imported by `properties.ts`, which must stay in the React-free core\n * because every backend subsystem — validation,\n * the drizzle schema generator, the OpenAPI generator, the SDK codegen — reads\n * property definitions. If `ComponentRef` needed `React.ComponentType`, the whole\n * property model would have to move to the admin layer with it.\n *\n * So the React types are described structurally instead of imported. Every form\n * a React component takes is assignable to {@link ComponentLike}:\n *\n * - a function component is `(props: P) => ReactNode`\n * - a class component satisfies the construct signature (`Component` has `render`)\n * - `memo` and `forwardRef` return exotic components, which are callable\n *\n * The cost is that the return type is `unknown` rather than `ReactNode`, so a\n * function that returns something React could not render is accepted here.\n * `@rebasepro/cms-types` re-exports a `ReactComponentRef<P>` narrowed against\n * the real `React.ComponentType` for authoring and for the admin's internals,\n * which restores that check where it can be enforced.\n */\n\n/**\n * Structural stand-in for `React.ComponentType<P>`.\n *\n * Deliberately not `Function` or `unknown`: those would accept anything and the\n * resolver's runtime heuristics ({@link ComponentRef} form 3) would be all that\n * stood between a typo and a blank screen.\n */\nexport type ComponentLike<P = any> =\n | ((props: P) => unknown)\n | (new (props: P, context?: unknown) => { render(): unknown });\n\n/**\n * Internal marker for a lazily-loaded component reference.\n * Created by the Vite transform plugin when converting string paths\n * to deferred `import()` calls. Users should NOT create these manually.\n *\n * @internal\n */\nexport interface LazyComponentRef<P = unknown> {\n readonly __rebaseLazy: true;\n readonly load: () => Promise<{ default: ComponentLike<P> }>;\n}\n\n/**\n * A reference to a UI component that can be provided in three forms:\n *\n * 1. **String path** (recommended for collection configs):\n * ```ts\n * Field: \"../../frontend/src/components/MyField\"\n * ```\n * The Vite plugin transforms this into a `LazyComponentRef` at build time.\n * On the backend, the string stays inert and is never evaluated.\n *\n * 2. **Lazy import function**:\n * ```ts\n * Field: () => import(\"../../frontend/src/components/MyField\")\n * ```\n * Standard ES dynamic import. Backend never calls the function.\n *\n * 3. **Direct component reference** (use only in frontend-only code):\n * ```ts\n * Field: MyFieldComponent\n * ```\n * Importing a component at the top level will pull React into the\n * backend runtime — only safe in code that the backend never imports.\n * `pnpm check:headless` fails on a collection file that does this.\n *\n * @group Types\n */\nexport type ComponentRef<P = any> =\n | string\n | LazyComponentRef<P>\n | (() => Promise<{ default: ComponentLike<P> }>)\n | ComponentLike<P>;\n\n/**\n * Type guard: checks if a value is a `LazyComponentRef` produced by the\n * Vite transform plugin.\n */\nexport function isLazyComponentRef<P = unknown>(ref: unknown): ref is LazyComponentRef<P> {\n return (\n typeof ref === \"object\" &&\n ref !== null &&\n \"__rebaseLazy\" in ref &&\n (ref as Record<string, unknown>).__rebaseLazy === true\n );\n}\n","/**\n * The project manifest (`rebase.json`) and the build artifacts derived from it.\n *\n * Three separate documents live in this file, and keeping them distinct matters:\n *\n * 1. {@link RebaseProjectManifest} — `rebase.json`. **Authored** by the developer,\n * committed to the repository. Declares topology only: which runtime major the\n * project targets, and which apps *this repository* contributes to the project.\n * Schema, security rules, hooks and functions stay in TypeScript under the\n * config package — nothing that needs a type system belongs here.\n *\n * 2. {@link RebaseProjectLink} — the per-checkout link (`.rebase/cloud.json`).\n * **Not committed**, because it is per-developer like a git remote. Says which\n * deployed project this working copy points at, whether that is a Rebase Cloud\n * project or the base URL of a self-hosted backend.\n *\n * 3. {@link RebaseBundleManifest} — `manifest.json` inside a built bundle.\n * **Generated**, never hand-edited. It is the lockfile analogue: the exact\n * contract a built artifact claims to satisfy, which the runtime validates\n * before it boots and a control plane validates before it deploys.\n *\n * A repository declares only the apps it contains. The set of apps belonging to a\n * project is held by the project itself, which is what makes multi-repo projects\n * work: two repositories never need to know about each other, only about the\n * project.\n */\n\nimport type { StorageSourceDefinition } from \"./storage_source\";\nimport type { ResourceGraph } from \"./resources\";\n\n/**\n * Which kind of thing an app is.\n *\n * - `backend` — the collections/hooks/functions that define the project's API.\n * Exactly one per *project* (not per repository); the registry enforces it.\n * - `static` — a pre-built client bundle (SPA, static site), served from the\n * backend process at its declared `path` or from a CDN. The admin panel is\n * one of these: it is an app in the user's repository like any other.\n *\n * That is the whole list. Ownership of the server process is a property of the\n * backend app ({@link RebaseBackendAppConfig.runtime}), not an app type.\n */\nexport type RebaseAppType = \"backend\" | \"static\";\n\n/**\n * The backend app: the project's API surface.\n *\n * Paths are relative to the directory holding `rebase.json`. The defaults match\n * the layout `rebase init` scaffolds, so a stock project may declare simply\n * `{ \"type\": \"backend\", \"runtime\": \"managed\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\n /**\n * Who owns the process this backend runs in.\n *\n * - `managed` — the platform's runtime image boots this project's bundle.\n * You supply collections, functions, crons and schema; Rebase supplies the\n * server.\n * - `custom` — this repository builds its own image and entrypoint. The\n * escape hatch: full control, no managed-runtime guarantees.\n *\n * Independent of *where* it runs. Both run on Rebase Cloud and both\n * self-host — the destination lives in `.rebase/cloud.json`, not here. See\n * `infra/docker/docker-compose.selfhost.yml`, which boots a managed bundle on a\n * developer's own Docker host.\n *\n * This is authored rather than inferred on purpose. It is the single most\n * consequential fact about a deployment, and inferring it is what used to\n * land projects on the custom runtime without anyone choosing it.\n */\n runtime: \"managed\" | \"custom\";\n /** Directory of the config package (collections + index). Default `config`. */\n config?: string;\n /** Directory of server functions. Default `backend/functions`. */\n functions?: string;\n /** Directory of cron job definitions. Default `backend/crons` when present. */\n crons?: string;\n /**\n * Path to the generated Drizzle schema module (tables/enums/relations).\n * Default `backend/src/schema.generated.ts`.\n */\n schema?: string;\n /**\n * Module path (relative to `config`) exporting the auth users collection as\n * its default export. Default `collections/users`.\n */\n usersCollection?: string;\n\n /**\n * `runtime: \"custom\"` only. Dockerfile path relative to the repository root.\n * Default `Dockerfile`.\n */\n dockerfile?: string;\n /** `runtime: \"custom\"` only. Build context relative to the root. Default `.`. */\n context?: string;\n /** `runtime: \"custom\"` only. Port the container listens on. Default 8080. */\n port?: number;\n}\n\n/**\n * A static client bundle — SPA or static site — built here and served at `path`.\n */\nexport interface RebaseStaticAppConfig {\n type: \"static\";\n /** Package directory containing the client sources. */\n root: string;\n /** Command that produces `output`. Run from the repository root. */\n build?: string;\n /** Directory of built assets, relative to the repository root. */\n output: string;\n /**\n * Public base path this app is served under. Default `/`.\n *\n * Several static apps run in one process, each at its own path — the API at\n * `/api`, a site at `/`, the admin at `/admin` — which is what keeps a\n * self-hosted deployment a single container.\n *\n * **This is a build-time input, not only a serving concern.** An app mounted\n * at `/admin` must be *built* for `/admin` (Vite's `base`), or `index.html`\n * loads and every asset 404s: a blank page with no server error. `rebase\n * build` passes it as `REBASE_APP_BASE` and asserts the emitted HTML honours\n * it. Changing this value requires rebuilding the app.\n */\n path?: string;\n /**\n * Serve `index.html` for unmatched paths under `path` (client-side routing).\n * Default `true` — the overwhelmingly common case for a client app, and a\n * static *site* generator emits real files for its routes anyway.\n */\n spa?: boolean;\n}\n\nexport type RebaseAppConfig = RebaseBackendAppConfig | RebaseStaticAppConfig;\n\n/**\n * Path prefixes the backend owns, which no static app may claim.\n *\n * One process — and, on the platform, one hostname — serves both the API and\n * however many static apps a project has. Mounting is longest-path-first, so an\n * app declaring `/api` would win against the API itself and every request to it\n * would be answered with that app's `index.html`: a 200 carrying HTML where the\n * caller expected JSON, from a project that looks deployed and healthy.\n *\n * Declared here rather than in either enforcer because both must agree. The CLI\n * checks it so a developer finds out while editing `rebase.json`; the control\n * plane checks it again at deploy intake, because the front door's correctness\n * cannot rest on a check that ran in somebody else's CLI — and a repository can\n * be deployed by a CLI older than this rule.\n */\nexport const RESERVED_BACKEND_PREFIXES = [\"/api\", \"/health\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"] as const;\n\n/**\n * Whether `path` collides with a prefix the backend owns.\n *\n * Compares at segment boundaries, so `/api` and `/api/v2` collide while\n * `/apidocs` does not — the same rule the router matches with, because a check\n * that is stricter than the router rejects paths that would have worked, and one\n * that is looser admits paths that will not.\n */\nexport function reservedPrefixFor(path: string): string | undefined {\n const normalized = path.endsWith(\"/\") && path !== \"/\" ? path.slice(0, -1) : path;\n return RESERVED_BACKEND_PREFIXES.find(\n reserved => normalized === reserved || normalized.startsWith(`${reserved}/`)\n );\n}\n\n/**\n * One declared storage source, as authored in `rebase.json`.\n *\n * The key comes from the enclosing record, so this is\n * {@link StorageSourceDefinition} minus its `key` — the same document the\n * runtime registry and the frontend router consume, expressed the way a JSON\n * object naturally expresses \"a set of named things\".\n */\nexport interface RebaseStorageSourceConfig {\n /** Engine backing this source: `local`, `s3`, `gcs`, or a custom id. */\n engine: string;\n /**\n * How the frontend reaches it. Default `server` (proxied through\n * `/api/storage`). `direct` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n */\n transport?: \"server\" | \"direct\";\n /** Human-readable label for the console and the admin UI. */\n label?: string;\n}\n\n/**\n * `rebase.json` — the authored project manifest.\n */\nexport interface RebaseProjectManifest {\n /** JSON Schema URL, for editor completion. Ignored by the tooling. */\n $schema?: string;\n /**\n * The runtime contract **major** this project targets, as a semver range\n * (e.g. `^1`, `~1.4`, or an exact `1.4.2` to pin).\n *\n * The platform upgrades patches and minors underneath a project without\n * asking; it never crosses a major. See {@link RUNTIME_CONTRACT_VERSION}.\n *\n * Named `rebase` rather than `runtime` so that `runtime` means exactly one\n * thing — {@link RebaseBackendAppConfig.runtime}, who owns the process. It\n * reads like `engines` in a `package.json`, which is what it is.\n */\n rebase: string;\n /**\n * Apps this repository contributes, keyed by app name. The key is the app's\n * identity within the project: it is what `rebase deploy <app>` names, what\n * client credentials are issued against, and what a second repository must\n * not collide with.\n */\n apps: Record<string, RebaseAppConfig>;\n /**\n * Buckets are NOT declared here any more.\n *\n * They were, and the runtime merged this block with the declarations in\n * config code — a bucket named in both had one engine kept and the other\n * silently discarded. Two homes for one concept, with a merge to decide\n * between them, is the shape this whole model replaced.\n *\n * `bucket(\"media\", { engine: \"s3\" })` in the project's config declares one\n * now, and `rebase resources --write` generates `rebase.resources.json`,\n * which is what a host reads before a build. A `storage` block left in this\n * file is refused by the validator, by name, with the replacement in the\n * message — not ignored, because a key that still parses and does nothing\n * is the failure this removed.\n */\n /**\n * Repository-wide opt-out from anonymous CLI usage sharing.\n *\n * **Only `false` does anything.** It suppresses sharing for everyone who\n * clones this repository, overriding each developer's own opt-in — an\n * organisation setting policy for work done on its behalf, the same shape\n * as a committed `.npmrc`.\n *\n * `true` is deliberately ignored, and the CLI says so rather than obeying\n * quietly. This file is committed, so a `true` here would be one developer\n * answering a privacy question for every colleague who later clones the\n * repo — consent by proxy, which is the exact thing opt-in exists to\n * prevent. Individuals opt in with `rebase telemetry enable`.\n */\n telemetry?: boolean;\n}\n\n/**\n * The per-checkout project link.\n *\n * Deliberately separate from `rebase.json`: the manifest is committed and shared,\n * while the link is per-developer. Keeping them in one file would mean either\n * committing someone's project id or gitignoring the topology.\n */\nexport interface RebaseProjectLink {\n /**\n * A Rebase Cloud project id, or the base URL of any running Rebase backend\n * (`https://api.example.com`). Both are first-class: every command that\n * accepts a project reference accepts either, so a self-hosted project has\n * the same tooling as a cloud one.\n */\n project: string;\n /** Organization slug. Cloud projects only. */\n org?: string;\n /** Explicit API base URL, when it differs from the project's default. */\n apiUrl?: string;\n}\n\n/**\n * Whether a project can run on the managed runtime, and if not, precisely why.\n *\n * The reasons are returned rather than summarised so tooling can print something\n * a developer can act on. \"Not eligible\" is never a dead end — it selects the\n * custom-runtime path, which still deploys.\n */\nexport interface ManagedCompatibility {\n eligible: boolean;\n reasons: string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Bundle\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Version of the bundle *format* itself.\n *\n * Bumped only when the on-disk layout changes in a way an older runtime could\n * not read. A runtime accepts any bundle whose `bundleFormat` is less than or\n * equal to its own — old bundles keep booting on new runtimes, which is the\n * whole point of separating the artifact from the engine.\n *\n * - **1** — `mode: \"cms\" | \"baas\" | \"static\"`, `entry.static` a single directory\n * string, `entry.admin` for a bundled admin panel.\n * - **2** — `kind: \"backend\" | \"static\"`, `entry.static` a list of\n * {@link RebaseBundleStatic}, `entry.admin` removed. A format-1 runtime reading\n * one of these would find no `mode` and an array where it expects a string, so\n * the bump is what turns that into a refusal to boot instead of a bundle that\n * starts and serves nothing.\n */\nexport const BUNDLE_FORMAT_VERSION = 2;\n\n/**\n * The runtime contract major.\n *\n * Distinct from the `@rebasepro/server` package version: the package may release\n * any number of minors and patches while this stays put. It changes only when\n * the bundle/runtime contract breaks compatibility, and a project's\n * `manifest.runtime` range is matched against *this*.\n *\n * ## v2 — resources are declared, not configured\n *\n * `RebaseBackendConfig.dataSources` and `.storageSources` are gone. A project\n * declares its databases and buckets with `database()` / `bucket()` in its\n * config, and the runtime reads those declarations.\n *\n * This had to be a major, and the reason is the managed tier: it moves projects\n * onto new images WITHOUT rebuilding them. A bundle built against v1 exports\n * those keys, and a v2 runtime refuses them at boot — so without this bump, one\n * image rollout would crash-loop every tenant that had ever declared a second\n * database or bucket, in a wave, with the cause in a container log nobody is\n * watching.\n *\n * With the bump, a v1 bundle on a v2 runtime is refused by\n * `assertBundleCompatibility` with the remedy in the message, and the platform\n * keeps it on a v1 image until it is rebuilt. That is the whole purpose of this\n * number.\n *\n * **Release order matters and is not optional.** The control plane is the side\n * that rejects, so it ships FIRST: raise `SUPPORTED_RUNTIME_CONTRACT` in the\n * saas repo (it rejects only `contract >` its own, so it then accepts both),\n * deploy that, and only then release a runtime implementing v2. Shipping the\n * runtime first turns every deploy into a rejected intake blaming the tenant's\n * bundle.\n */\nexport const RUNTIME_CONTRACT_VERSION = 1;\n\n/** Where the runtime finds each part of the bundle. Paths are bundle-relative. */\nexport interface RebaseBundleEntrypoints {\n /** Compiled config package directory (collections live under it). */\n config?: string;\n /** Compiled collections directory, when it differs from `<config>/collections`. */\n collections?: string;\n /** Compiled functions directory. */\n functions?: string;\n /** Compiled crons directory. */\n crons?: string;\n /** Compiled Drizzle schema module. */\n schema?: string;\n /** Module exporting the auth users collection (default export). */\n usersCollection?: string;\n /**\n * Built static apps to serve from this process, in declaration order.\n *\n * A list rather than a single directory because one process serves several\n * apps at different paths — a site at `/` and the admin at `/admin`. The\n * runtime mounts them longest-path-first so the `/`-rooted app's catch-all\n * does not claim its siblings' URLs.\n */\n static?: RebaseBundleStatic[];\n}\n\n/** One built static app inside a bundle. */\nexport interface RebaseBundleStatic {\n /** Public base path, e.g. `/` or `/admin`. */\n path: string;\n /** Bundle-relative directory holding the built assets. */\n dir: string;\n /** Serve `index.html` for unmatched paths under `path`. */\n spa: boolean;\n}\n\n/**\n * A native module found in the dependency closure.\n *\n * Recorded rather than merely counted so a rejection can name the offending\n * package instead of saying \"something here is native\".\n */\nexport interface NativeDependency {\n name: string;\n /** Why it was flagged — a `.node` binary, a gyp build, or an install script. */\n reason: string;\n}\n\n/**\n * `manifest.json` — generated, and the document the runtime and control plane\n * both validate against.\n */\n/**\n * One custom function, as recorded in a built bundle.\n *\n * @see RebaseBundleManifest.functions\n */\nexport interface RebaseBundleFunction {\n /**\n * The filename without its extension — which is also the URL segment it\n * mounts at (`/api/functions/<name>`), the API-key permission that grants\n * it, and the name `REBASE_FUNCTIONS_ONLY` selects by. One identity, used\n * everywhere.\n */\n name: string;\n /** Path inside the bundle, so a host can point at the file. */\n file: string;\n /**\n * `false` when the function's own source imports a Node built-in or a\n * package that needs one.\n *\n * Descriptive, never a gate: nothing refuses to build or deploy on this. It\n * says where this function *could* run, not where it should.\n */\n portable: boolean;\n /**\n * Why it is not portable — one short phrase per reason, deduplicated.\n * Absent when it is.\n */\n requires?: string[];\n}\n\nexport interface RebaseBundleManifest {\n /** @see BUNDLE_FORMAT_VERSION */\n bundleFormat: number;\n runtime: {\n /** The `runtime` range copied from `rebase.json`. */\n range: string;\n /** Exact `@rebasepro/server` version this bundle was built against. */\n builtAgainst: string;\n /** Runtime contract major this bundle requires. */\n contract: number;\n };\n /**\n * Hash of the compiled collection definitions.\n *\n * This is the contract stamp. A generated SDK records the value it was built\n * from, a client sends it back, and a mismatch is what lets the platform say\n * \"this app was built against an older schema\" instead of failing mysteriously\n * at the first request. It covers collections only — a hook edit does not\n * change a client's contract, so it must not invalidate every SDK.\n */\n schemaVersion: string;\n /** Which app in `rebase.json` this bundle was built from. */\n app: string;\n /**\n * What the runtime does with this bundle.\n *\n * - `backend` — boot the full server: database, auth and the data API, plus\n * any static apps in `entry.static`.\n * - `static` — no backend at all: serve `entry.static` and nothing else. No\n * database, no auth, no data sources. This is how a static app runs on the\n * same image as the backend.\n *\n * Replaces an earlier `mode: \"cms\" | \"baas\" | \"static\"`. The cms/baas\n * distinction was never a third kind of thing — it is simply whether\n * `entry.config` is present, so it is derived rather than declared.\n */\n kind: \"backend\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\n /**\n * Every custom function in the bundle, named and classified.\n *\n * Two things are recorded per function, and both are answers a host would\n * otherwise have to get by importing user code:\n *\n * - **What it is called.** That name is the function's identity everywhere —\n * the URL segment it mounts at, the `functions/<name>` API-key\n * permission, the value `REBASE_FUNCTIONS_ONLY` selects by. A host that\n * wants to give one slow function its own replica count currently has to\n * boot the bundle to discover what is in it.\n * - **Whether it needs Node.** Purely descriptive: a function that opens a\n * file or runs raw SQL is a fine function, and every deployment today is\n * a Node process. It is recorded because the question \"which of these\n * could run somewhere else\" has to be answerable from the artifact, and\n * because answering it per-file after the fact — across a codebase\n * already written — is the expensive version of the same question.\n *\n * Absent on a bundle built before this field existed, which is why every\n * consumer must treat it as optional rather than as an empty list.\n */\n functions?: RebaseBundleFunction[];\n hooks: {\n /**\n * Whether the dependency closure contains native code.\n *\n * The managed runtime refuses these: a prebuilt binary cannot be run on\n * an image the platform did not build it for, and the honest failure is\n * at deploy time rather than at 3am in a crash loop.\n */\n native: boolean;\n nativeModules?: NativeDependency[];\n };\n /**\n * What the bundle's config says about storage access control.\n *\n * Storage is not under RLS and its keys share one flat namespace, so a\n * deployment with file storage enabled and no access model serves every\n * user's files to every signed-in user. The runtime refuses to boot in that\n * state — which, on a hosted platform that enables storage from the *console*\n * rather than from the bundle, surfaces as a crash loop the developer cannot\n * read.\n *\n * Recording it here lets a host reject the deploy with the reason instead.\n * Absent on bundles built before this field existed.\n */\n storage?: {\n /** Whether the config package exports a `storageAuthorize` hook. */\n authorize: boolean;\n /**\n * Buckets, on bundles built before {@link RebaseBundleManifest.resources}.\n *\n * No longer written. A host reads `resources`, which carries every kind\n * in one list; this stays declared so a control plane can keep reading\n * the bundles a project shipped before it was rebuilt.\n */\n sources?: StorageSourceDefinition[];\n };\n /**\n * Everything the project declares it needs — databases, buckets, topics,\n * and whatever kind is registered next.\n *\n * Recorded so a host can tell, from the artifact alone and before starting\n * anything, what a deploy will need provisioned. That question used to be\n * answerable for buckets and for nothing else, because buckets were the\n * only kind written into an artifact — which is how a project's databases\n * became invisible to the platform that runs them.\n *\n * Absent on bundles built before this field existed.\n */\n resources?: ResourceGraph;\n deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\n /**\n * The dependency tree ships *inside* the bundle, already installed.\n *\n * Absent or false means the tree is declared but not present, and\n * whoever boots the bundle has to install it. On the managed runtime that\n * install runs in an init container on **every** pod start — the bundle\n * lives on a volume that is wiped each time — and it is the single\n * largest cost in a managed pod's life: 35–55 seconds of a 40–60 second\n * cold start. Since a pod restarts on every eviction, node failure, OOM\n * and runtime rollout, that number is not a startup detail. It is what an\n * outage costs.\n *\n * Vendoring moves the install to build time, where it happens once. It is\n * skipped when the closure contains native code, because a prebuilt\n * binary is only valid for the platform it was built for — see\n * {@link vendorTarget} for what \"the platform\" means here.\n */\n vendored?: boolean;\n /**\n * What {@link vendored} was resolved for, recorded so a mismatch can be\n * refused rather than discovered at import time.\n *\n * Cross-platform vendoring is safe for pure JavaScript and unsafe for\n * anything compiled, and the boundary between them is not always visible\n * in a dependency list: `esbuild` is pure-JS with a *platform-specific\n * optional dependency* holding the actual binary, so an install run on a\n * developer's Mac silently produces a tree that cannot run on the Linux\n * image. The install therefore resolves optional dependencies for the\n * target explicitly rather than for the machine it runs on, and records\n * the answer here.\n */\n vendorTarget?: {\n /** npm `--os`, e.g. `linux`. */\n os: string;\n /** npm `--cpu`, e.g. `x64`. */\n cpu: string;\n /** Node major the tree was resolved for. */\n node: string;\n };\n };\n build: {\n /** `@rebasepro/cli` version that produced this bundle. */\n cli: string;\n /** Node major the bundle was compiled on. */\n node: string;\n /** ISO-8601. */\n createdAt: string;\n };\n}\n\n/** The contract a running backend serves at `GET /api/meta/contract`. */\nexport interface RebaseProjectContract {\n /** Matches {@link RebaseBundleManifest.schemaVersion}. */\n schemaVersion: string;\n runtime: {\n /** `@rebasepro/server` version currently running. */\n version: string;\n contract: number;\n };\n /** Full collection definitions, serialized — the input to SDK generation. */\n collections: unknown[];\n /** Collection slugs, for cheap inspection without parsing the definitions. */\n collectionSlugs: string[];\n generatedAt: string;\n}\n\n/** Header carrying the schema version an SDK was generated from. */\nexport const SCHEMA_VERSION_HEADER = \"x-rebase-schema\";\n","import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import type { VectorSearchParams } from \"./data_driver\";\nimport type { ComputedSortField, SearchMatch } from \"../types/search\";\nimport { Entity, EntityValues } from \"../types/entities\";\nimport { WhereFilterOp, FieldPath, NonColumnFieldPath, FilterValues, NullsPlacement, OrderBySpec, RelationAggregateSort } from \"../types/filter-operators\";\n\n/**\n * The element type of an array column, and the column's own type otherwise.\n *\n * A generated SDK emits an `array` property as `Array<X>` and a to-many\n * relation as `Array<TargetRow>`, so this is what `array-contains` compares\n * against on either.\n */\nexport type ElementOf<T> = T extends readonly (infer E)[] ? E : T;\n\n/**\n * The `id` of a row-shaped element, and `never` for anything else.\n *\n * A to-many relation is emitted as `Array<TargetRow>`, but the filter compilers\n * compare a relation by **id** — `buildRelationFilterPredicate` in\n * `@rebasepro/server-postgres` unwraps a relation value down to its id — so\n * `where(\"tags\", \"array-contains\", tagId)` is the call that works, and the\n * element type alone would refuse it.\n */\nexport type IdOf<E> = E extends { id: infer I } ? I : never;\n\n/**\n * One member of an array column: its element, or — when the element is a row —\n * that row's id, which is what a relation filter is actually compared against.\n */\nexport type WhereElementOf<T> = ElementOf<T> | IdOf<ElementOf<T>>;\n\n/**\n * The value a given operator takes on a column of type `T`.\n *\n * `WhereValue<T>` was one value type for all sixteen operators, which made\n * `array-contains` uncallable from a generated SDK — it is the one operator\n * whose value is an *element* of the column rather than the column's own type,\n * so on `tags: string[]` it wanted a `string[]` and the documented\n * `.where(\"tags\", \"array-contains\", \"featured\")` was a compile error. The\n * spelling that did compile, `[\"featured\"]`, builds `@> ARRAY[$1]` with the\n * whole array bound as the single element and matches nothing: the correct\n * query rejected, the accepted query silently wrong.\n *\n * The branches mirror `buildSingleFilterCondition` in `@rebasepro/server-postgres`:\n *\n * - `array-contains` → one element of the column (or a related row's id).\n * - `in` / `not-in` / `array-contains-any` → a list of elements; a bare element\n * is read as the one-element list, and `null` is a null check.\n * - `like` / `ilike` / `not-like` / `not-ilike` → a SQL pattern. Always a\n * string, including on numeric and date columns, which the driver casts.\n * - `is-null` / `is-not-null` → nothing; the value is ignored everywhere.\n * - everything else → the column's own type, or `null` for a null comparison.\n *\n * Distributes over `Op`, so a caller holding an unnarrowed `WhereFilterOp`\n * (a dynamic filter UI, say) gets the union of every branch and stays as\n * permissive as it was.\n */\nexport type WhereValueFor<Op extends WhereFilterOp, T> =\n Op extends \"array-contains\"\n ? WhereElementOf<T>\n : Op extends \"in\" | \"not-in\" | \"array-contains-any\"\n ? readonly WhereElementOf<T>[] | WhereElementOf<T> | null\n : Op extends \"like\" | \"ilike\" | \"not-like\" | \"not-ilike\"\n ? string\n : Op extends \"is-null\" | \"is-not-null\"\n ? null | undefined\n : T | null;\n\n/**\n * A group of conditions combined with `and`, `or`, or negated with `not`.\n *\n * ## `not`\n *\n * `not` negates the **conjunction** of its `conditions`: `not(a)` is `NOT a`,\n * and `not(a, b)` is `NOT (a AND b)`. One rule, stated here and applied\n * identically by the wire codec (`or(...)`/`and(...)`/`not(...)` in\n * `@rebasepro/common`), the REST `?not=` parameter and every driver compiler,\n * so a negation means the same thing whichever end writes it.\n *\n * Negation is not expressible by inverting the operators inside the group: SQL\n * three-valued logic makes `NOT (a AND b)` and `(NOT a) OR (NOT b)` differ the\n * moment a NULL is involved, and only one of them is what the caller wrote. It\n * compiles to a real `NOT (...)`.\n */\nexport interface LogicalCondition {\n type: \"and\" | \"or\" | \"not\";\n conditions: (FilterCondition | LogicalCondition)[];\n}\n\nexport interface FilterCondition {\n column: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * How one relation is loaded by {@link FindParams.include}.\n *\n * `true` loads the relation whole. The object form narrows it — the same four\n * knobs a top-level query has, applied to the rows *inside* one relation — and\n * `include` nests, so a query can ask for \"each post's five newest published\n * comments, each with its author\" in one request.\n *\n * ```ts\n * include: {\n * comments: {\n * limit: 5,\n * where: { published: [\"==\", true] },\n * orderBy: [\"created_at\", \"desc\"],\n * include: { author: true }\n * }\n * }\n * ```\n *\n * Nesting is bounded at {@link MAX_INCLUDE_DEPTH} hops. Each hop is another\n * batched query, and the bound is what stops one request from walking a\n * self-referencing relation forever.\n *\n * @group Data\n */\nexport interface IncludeOptions {\n /** Rows to load per parent row. Applied per parent, not across the page. */\n limit?: number;\n /** Filter the related rows, in the same dialect as {@link FindParams.where}. */\n where?: FilterValues<string>;\n /** An `and`/`or`/`not` group over the related rows. */\n logical?: LogicalCondition;\n /**\n * Sort the related rows — the tuple form, or the `field:direction[:nulls]`\n * shorthand the REST `?orderBy=` parameter uses.\n *\n * The string is accepted because this whole object travels over a query\n * string, where a tuple is three characters of JSON heavier for no gain.\n */\n orderBy?: OrderBySpec<string> | string;\n /** Columns of the *related* row to return. `id` is always included. */\n fields?: string[];\n /** Relations of the related row to load in turn. */\n include?: IncludeSpec;\n}\n\n/**\n * The relations a read loads, as a list of (possibly dotted) names or as a\n * tree.\n *\n * - `[\"author\", \"comments.author\"]` — a dotted path is the same thing as the\n * nested object form, spelled flat. It is what the REST `?include=` parameter\n * carries, and the two forms compile to the same request.\n * - `[\"*\"]` — every relation, one hop deep. The admin panel's shape.\n * - `{ comments: { limit: 5, include: { author: true } } }` — the parametrised\n * form.\n *\n * A name that is not a relation of the collection is a **400\n * `UNKNOWN_RELATION`**, not a silent omission: a read that quietly drops an\n * `include` answers 200 with the field missing, which is indistinguishable from\n * a row that genuinely has no related row.\n *\n * @group Data\n */\nexport type IncludeSpec = string[] | Record<string, true | IncludeOptions>;\n\n/**\n * Hops an {@link IncludeSpec} may nest. `comments.author` is two.\n *\n * @group Data\n */\nexport const MAX_INCLUDE_DEPTH = 3;\n\n/**\n * Parameters for querying a collection.\n *\n * ## How the filter parameters combine\n *\n * `where`, `logical`, and `searchString` are **independent** and, when more\n * than one is present, are combined with **AND** — every clause must match.\n * Concretely the backend builds:\n *\n * ```text\n * (where filters, AND-ed together)\n * AND (logical group)\n * AND (searchString matches, OR-ed across searchable columns)\n * ```\n *\n * So `where` does **not** conflict with or override `logical` — they stack.\n * If you need `where` fields OR-ed with each other, move them into `logical`\n * instead. There is no way to OR `where` against `logical`; express anything\n * that isn't a plain AND of the three groups inside a single `logical` tree.\n *\n * ## Pagination precedence\n *\n * `limit`/`offset` and `page` describe the same window two ways. If **both\n * `offset` and `page` are provided, `page` wins** — the backend computes\n * `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the\n * explicit `offset`. Pick one style per query.\n *\n * @group Data\n */\nexport interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Maximum number of items to return.\n *\n * Omit it and the backend applies {@link DEFAULT_LIST_LIMIT}, so a read is\n * never unbounded. Provide it and it must be a whole number between 1 and\n * {@link MAX_LIST_LIMIT}: the backend **rejects** anything else with a 400\n * rather than trimming it to fit, because a page quietly smaller than the\n * one you asked for is indistinguishable from having reached the end of the\n * collection. To read past the ceiling, page with `offset` — or let\n * {@link SDKCollectionClient.iterate} / {@link SDKCollectionClient.findAll}\n * do it for you.\n */\n limit?: number;\n /**\n * Number of items to skip. Ignored when {@link FindParams.page} is also\n * set — `page` takes precedence.\n */\n offset?: number;\n /**\n * Page number (1-indexed), alternative to {@link FindParams.offset}.\n * When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.\n */\n page?: number;\n /**\n * Filter conditions keyed by field name.\n * Each value is a `[WhereFilterOp, value]` tuple or an array of tuples\n * for multiple conditions on the same field. Multiple fields, and multiple\n * tuples on one field, are **AND-ed**; also AND-ed with `logical` and\n * `searchString` when present (see the interface docs).\n *\n * @example\n * { status: [\"==\", \"active\"] }\n * { age: [\">=\", 18] }\n * { role: [\"in\", [\"admin\", \"editor\"]] }\n * { age: [[\">=\", 18], [\"<\", 65]] }\n */\n where?: FilterValues<FieldPath<M>>;\n /**\n * Logical grouping conditions (AND/OR). Use this for anything `where`\n * can't express — notably OR-ing conditions. AND-ed with `where` and\n * `searchString` when present (see the interface docs).\n */\n logical?: LogicalCondition;\n /**\n * Sort order as a `[field, direction]` tuple, or a list of them applied in\n * order of significance — the second key breaks ties on the first, and so on.\n *\n * @example orderBy: [\"created_at\", \"desc\"]\n * @example orderBy: [[\"roles\", \"asc\"], [\"created_at\", \"desc\"]]\n */\n orderBy?: OrderBySpec<FieldPath<M> | ComputedSortField>;\n /**\n * Relations to load into the response — see {@link IncludeSpec}.\n *\n * Not checked against `M` here: a relation name comes from the collection's\n * `relations`, not from its columns, so nothing in a *hand-written* row type\n * can validate one. A **generated** `Database` narrows this to the\n * collection's actual relation keys, recursively — see `rebase codegen`.\n *\n * An unknown name is a 400 `UNKNOWN_RELATION`. It used to be ignored.\n */\n include?: IncludeSpec;\n\n /**\n * Columns to return, instead of all of them.\n *\n * A real column projection: only these columns are read from the database,\n * so a query that needs two fields of a wide row does not pay for the rest.\n * `excludeFromApi` still applies — naming such a column here does not\n * un-hide it — and the primary key is always returned, because a row that\n * cannot be addressed cannot be updated, deleted or paged past.\n *\n * A relation named in {@link FindParams.include} is loaded regardless of\n * whether it appears here; use {@link IncludeOptions.fields} to narrow the\n * columns *within* an included relation.\n */\n fields?: string[];\n\n /**\n * Collapse rows that are identical over the columns being returned.\n *\n * `SELECT DISTINCT` over the projection — so it is only meaningful\n * alongside {@link FindParams.fields}, and with the primary key in the\n * projection (which it always is) every row is already distinct. Pair it\n * with `fields` naming the columns you actually want the distinct values of.\n *\n * `meta.total` counts distinct rows too, so a distinct listing's `hasMore`\n * describes the set it is paging.\n */\n distinct?: boolean;\n\n /**\n * Continue from where a previous page ended — keyset (\"seek\") pagination.\n *\n * The value is the opaque `meta.nextCursor` of the previous response. It\n * encodes the sort keys the query was ordered by and the last row's values\n * for them, so a page picks up strictly after the last row served rather\n * than at a row *count* that concurrent writes have already moved.\n *\n * It has to describe the same query: `after` alongside a different\n * `orderBy` is a 400 `CURSOR_ORDER_MISMATCH` rather than a page of rows\n * seeked in an order nobody asked for. Mutually exclusive with `offset` and\n * `page` for the same reason.\n *\n * Multi-key sorts and nullable keys both work — the comparison is built\n * over every key, in order, with the NULL placement the sort declared.\n */\n after?: string;\n /**\n * Text search string, AND-ed with `where`/`logical`. This is the value\n * behind the query builder's `.search()` method.\n *\n * What it compiles to depends on the collection. By default — matching\n * every collection that has not said otherwise — it is a case-insensitive\n * substring match OR-ed across the collection's top-level `string`\n * properties: it does not reach inside `map` or `array` properties, it does\n * not stem or rank, and it cannot use an index.\n *\n * A Postgres collection that declares a `search` block instead gets a\n * ranked full-text match over exactly the fields it named, and rows come\n * back with a {@link FindParams.orderBy}-able `_score`.\n */\n searchString?: string;\n\n /**\n * Nearest-neighbour search over a `vector` property.\n *\n * Postgres only, and only for a collection that declares a property of\n * type `vector`. Rows come back ordered by distance, closest first, each\n * carrying a `_distance`. Combines with `where` and `logical`, which are\n * applied as filters before the ordering — so this is \"the nearest rows\n * that also match\", not \"the nearest rows, then filtered\".\n *\n * Supplying the query vector is the caller's job: rebase stores and\n * searches embeddings, it does not compute them.\n */\n vectorSearch?: VectorSearchParams;\n\n /**\n * Ask each returned row to explain itself: which declared search fields\n * matched, with a highlighted snippet from each. Populates `_matches`.\n *\n * Off by default because it is not free — one `ts_headline` per declared\n * field per returned row, and `ts_headline` re-parses the document rather\n * than reading the index. Fine for a page of results, not for an export.\n *\n * Ignored unless the collection declares a `search` block and the query\n * carries a `searchString`; there is nothing to explain otherwise.\n */\n searchExplain?: boolean;\n}\n\n/**\n * Paginated response from a collection query.\n * @group Data\n */\nexport interface FindResponse<M extends Record<string, unknown> = Record<string, unknown>> {\n /** Array of entities matching the query */\n data: Entity<M>[];\n /** Pagination metadata */\n meta: {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n };\n}\n\n\n\n/**\n * Fluent query builder for the **admin panel** — resolves to `FindResponse<M>`\n * (Snapshot-wrapped rows).\n *\n * @internal App developers should use {@link SDKQueryBuilderInterface}\n * (flat rows, returned by `client.data.*` / `context.data.*`). This\n * Snapshot-flavored variant backs the admin panel internals only.\n *\n * @group Data\n */\nexport interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;\n where(logicalCondition: LogicalCondition): this;\n orderBy(column: (keyof M & string) | ComputedSortField, direction?: \"asc\" | \"desc\"): this;\n limit(count: number): this;\n offset(count: number): this;\n search(searchString: string, options?: { explain?: boolean }): this;\n\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n *\n * Postgres only, over a property declared as `type: \"vector\"`. Each row\n * comes back with a `_distance`. Any `where` on the same query filters\n * before the ordering; distance decides the order.\n *\n * The query embedding is the caller's to produce.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): this;\n include(...relations: string[]): this;\n find(): Promise<FindResponse<M>>;\n listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;\n}\n\n/**\n * A single collection's CRUD accessor for the **admin panel** — every method\n * resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).\n *\n * @internal App developers do **not** use this. The public, symmetric surface\n * is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`\n * in the SDK and `context.data.products` in framework callbacks. This\n * Snapshot-flavored accessor backs the admin panel view-model only.\n *\n * @group Data\n */\nexport interface CollectionAccessor<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Find multiple records with optional filtering, pagination, and sorting.\n */\n find(params?: FindParams<M>): Promise<FindResponse<M>>;\n\n /**\n * Find a single record by its ID.\n */\n findById(id: string | number): Promise<Entity<M> | undefined>;\n\n /**\n * Create a new record.\n * @param data The entity data to create.\n * @param id Optional specific ID to use for the new record.\n * @returns The created entity\n */\n create(data: Partial<EntityValues<M>>, id?: string | number): Promise<Entity<M>>;\n\n /**\n * Create many records in a single transaction.\n *\n * See {@link SDKCollectionClient.createMany}. Optional: not every driver can\n * write in bulk, and callers should fall back to `create` per record.\n */\n createMany?(\n data: Partial<EntityValues<M>>[],\n options?: { upsert?: boolean; onConflict?: readonly string[] }\n ): Promise<Entity<M>[]>;\n\n /**\n * Update an existing record by ID.\n * @returns The updated entity\n */\n update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;\n\n /**\n * Update many records in a single transaction.\n *\n * See {@link SDKCollectionClient.updateMany}. Optional, as `createMany` is.\n */\n updateMany?(updates: { id: string | number; data: Partial<EntityValues<M>> }[]): Promise<Entity<M>[]>;\n\n /**\n * Delete many records in a single transaction.\n *\n * See {@link SDKCollectionClient.deleteMany}. Optional, as `createMany` is.\n */\n deleteMany?(ids: (string | number)[]): Promise<void>;\n\n /**\n * Delete a record by ID.\n */\n delete(id: string | number): Promise<void>;\n\n /**\n * Subscribe to a collection for real-time updates.\n * Optional method, may not be supported by all implementations (like stateless HTTP clients).\n */\n listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;\n\n /**\n * Subscribe to a single record for real-time updates.\n * Optional method.\n */\n listenById?(id: string | number, onUpdate: (entity: Entity<M> | undefined) => void, onError?: (error: Error) => void): () => void;\n\n /**\n * Count the number of records matching the given filter.\n *\n * Optional on this contract because a data source need not support it, and\n * required on `CollectionClient` — the HTTP implementation always has it.\n * So `client.data.posts.count()` compiles in the browser while the same\n * call through a `context.data` accessor needs `count?.()`, which is the\n * one place the two halves of this API are not interchangeable.\n */\n count?(params?: FindParams<M>): Promise<number>;\n\n /**\n * {@link SDKCollectionClient.aggregate}. Optional here for the same reason\n * `count` is: not every data source can compute one, and the SDK wraps an\n * absent implementation in a stub that says so rather than returning a\n * number nothing counted.\n */\n aggregate?(params: AggregateParams<M>): Promise<AggregateRow[]>;\n\n // Fluent Query Builder\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): QueryBuilderInterface<M>;\n where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;\n orderBy(column: (keyof M & string) | ComputedSortField, direction?: \"asc\" | \"desc\"): QueryBuilderInterface<M>;\n limit(count: number): QueryBuilderInterface<M>;\n offset(count: number): QueryBuilderInterface<M>;\n search(searchString: string, options?: { explain?: boolean }): QueryBuilderInterface<M>;\n\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n *\n * Postgres only, over a property declared as `type: \"vector\"`. Each row\n * comes back with a `_distance`. Any `where` on the same query filters\n * before the ordering; distance decides the order.\n *\n * The query embedding is the caller's to produce.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): QueryBuilderInterface<M>;\n include(...relations: string[]): QueryBuilderInterface<M>;\n}\n\n// =============================================================================\n// SDK-facing types — flat rows, no Entity wrapper\n// =============================================================================\n\n/**\n * Pagination metadata returned with collection queries.\n * @group Data\n */\nexport interface PaginationMeta {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n /**\n * The opaque cursor that continues this listing — pass it back as\n * {@link FindParams.after}.\n *\n * Present whenever there is a next page to describe (`hasMore` is true and\n * the page returned at least one row). Absent on the last page, and absent\n * on a query no cursor can describe (relevance ordering, whose scores are\n * computed per query and are not comparable between two of them).\n *\n * Opaque on purpose: it encodes the sort keys *and* the last row's values\n * for them, and a client that parsed it would be depending on an encoding\n * that exists to be changed.\n */\n nextCursor?: string;\n}\n\n/**\n * Paginated response from a collection query (SDK-facing).\n * Returns flat rows instead of Entity-wrapped objects.\n *\n * @example\n * const { data, meta } = await rebase.data.posts.find();\n * console.log(data[0].title); // direct access — no .values\n * console.log(meta.total);\n *\n * @group Data\n */\nexport interface FindResult<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Flat rows matching the query, each carrying whatever the query computed\n * for it — see {@link QueryComputedFields}.\n */\n data: (M & QueryComputedFields)[];\n /** Pagination metadata */\n meta: PaginationMeta;\n}\n\n/**\n * Values a query attaches to a row that are not columns of it.\n *\n * Both are absent unless the query asked for the thing that produces them, so\n * both are optional — and reading one on a query that did not ask returns\n * `undefined` rather than a wrong number.\n *\n * They live here rather than on the row type because a generated row type\n * describes a *table*, and neither of these is in one. Without this, a caller\n * who sorted by relevance could not then read the relevance.\n *\n * A `type` alias, deliberately, not an `interface`. TypeScript grants an\n * implicit index signature to a type alias and withholds it from an interface,\n * so `Row & QueryComputedFields` stops being assignable to\n * `Record<string, unknown>` the moment this becomes an interface. Seven casts\n * in one downstream app broke on exactly that.\n *\n * @group Data\n */\nexport type QueryComputedFields = {\n /**\n * Relevance, when the collection declares a {@link SearchConfig} and the\n * query carried a search string. Higher is better; the scale is not\n * comparable between two different search strings.\n */\n _score?: number;\n /**\n * Which declared fields matched, and the text around each hit. Present only\n * when the query asked for it — `.search(term, { explain: true })` — because\n * it costs a `ts_headline` per field per row.\n */\n _matches?: SearchMatch[];\n /**\n * Distance to the query vector, when the query used\n * {@link FindParams.vectorSearch}. Lower is closer, and the rows are\n * already ordered by it.\n */\n _distance?: number;\n};\n\n/**\n * One aggregate a query asks for.\n *\n * `count` alone counts rows; every other function names a column, and `count`\n * with a column counts its non-NULL values.\n *\n * The result key is derived rather than chosen: `sum(total)` comes back as\n * `sum_total` and a bare `count()` as `count`. Letting a caller name it would\n * mean checking their name is not also a `groupBy` field — a rule nobody would\n * guess, and a silently overwritten value if it went unchecked.\n *\n * @group Data\n */\nexport type AggregateSelect<M extends Record<string, unknown> = Record<string, unknown>> =\n | { fn: \"count\"; field?: Extract<keyof M, string> }\n | { fn: \"sum\" | \"avg\" | \"min\" | \"max\"; field: Extract<keyof M, string> };\n\n/**\n * One row of an aggregate result: the `groupBy` columns, plus one key per\n * {@link AggregateSelect} under its derived alias.\n *\n * `count`, `sum` and `avg` arrive as numbers — Postgres returns bigint and\n * numeric as strings, and they are parsed once at the driver rather than by\n * every caller. `min`/`max` keep the column's own type.\n *\n * @group Data\n */\nexport type AggregateRow = Record<string, unknown>;\n\n/**\n * What {@link SDKCollectionClient.aggregate} takes: the same narrowing a\n * `find()` takes, minus the parts of it that describe a *page* of rows.\n *\n * `limit` survives and means what it means on the REST route — a bound on the\n * number of **groups**, because grouping by a high-cardinality column is a whole\n * table's worth of rows in one response. It is ignored when there is no\n * `groupBy`, since an ungrouped aggregate is one row.\n *\n * `orderBy`, `include`, `after` and the rest are absent on purpose: an\n * aggregate has no rows to sort, no relations to load and no page to continue.\n * They were silently ignored on the REST route; here they do not typecheck.\n *\n * @group Data\n */\nexport interface AggregateParams<M extends Record<string, unknown> = Record<string, unknown>> {\n /** The aggregates to compute. At least one. */\n select: AggregateSelect<M>[];\n /** Columns to group by. Omit for a single row over everything that matches. */\n groupBy?: Extract<keyof M, string>[];\n /** Filter conditions, as {@link FindParams.where}. */\n where?: FilterValues<FieldPath<M>>;\n /** An `and`/`or`/`not` group, AND-ed with `where`. */\n logical?: LogicalCondition;\n /** Text search, AND-ed with the filters. */\n searchString?: string;\n /** Most groups to return. Ignored without `groupBy`. */\n limit?: number;\n}\n\n/**\n * Which column an iteration seeks on, for keyset (\"seek\") pagination.\n *\n * Either the column name on its own — sorted ascending — or the column plus an\n * explicit direction. The column must be **unique** and must be the column the\n * query is ordered by; see {@link PageWalkOptions.cursor}.\n *\n * @group Data\n */\nexport type CursorSpec<M extends Record<string, unknown> = Record<string, unknown>> =\n | (Extract<keyof M, string>)\n | { field: Extract<keyof M, string>; direction?: \"asc\" | \"desc\" };\n\n/**\n * How {@link SDKCollectionClient.iterate} / {@link SDKCollectionClient.findAll}\n * walk a collection, layered on top of the normal `find()` parameters.\n *\n * @group Data\n */\nexport interface PageWalkOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Rows fetched per request. Defaults to 200; values below 1 are clamped up.\n * This is the request size, not a result cap — the iteration keeps going\n * until the server says there is nothing left.\n */\n pageSize?: number;\n /**\n * Paginate by **seeking on a column** instead of by offset.\n *\n * Offset paging — the default — re-counts rows on every request, so a row\n * inserted or deleted *while the iteration runs* shifts the window and the\n * walk silently skips or repeats rows. Seeking is immune to that: each page\n * asks for rows strictly after the last one seen, so concurrent writes\n * before the cursor cannot move it.\n *\n * Prefer this whenever the collection has a unique, sortable column\n * (typically its primary key). The column must be unique — a repeated value\n * at a page boundary either skips rows or stalls, and the iterator throws\n * rather than looping — and the query is ordered by it, so a `cursor` and a\n * conflicting `orderBy` is an error, not a silent override.\n *\n * Implemented with the parameters `find()` already takes (an `orderBy` plus\n * a `>` / `<` filter on the cursor column), so it works on every transport\n * and needs nothing new from the server.\n *\n * @example\n * for await (const job of client.data.jobs.iterate({ cursor: \"id\" })) { … }\n */\n cursor?: CursorSpec<M>;\n /**\n * Hard ceiling on the number of requests one walk may make, so a server\n * that never stops saying `hasMore` cannot spin forever. Defaults to\n * 10 000 pages; hitting it throws.\n */\n maxPages?: number;\n}\n\n/**\n * Parameters accepted by {@link SDKCollectionClient.iterate} — everything\n * `find()` takes except the window itself (`limit`, `offset`, `page`), which\n * the iterator owns, plus the walk options.\n *\n * @group Data\n */\nexport type IterateParams<M extends Record<string, unknown> = Record<string, unknown>> =\n Omit<FindParams<M>, \"limit\" | \"offset\" | \"page\"> & PageWalkOptions<M>;\n\n/**\n * Parameters accepted by {@link SDKCollectionClient.findAll}: the iteration\n * parameters plus the ceiling that keeps a whole collection from being pulled\n * into memory unnoticed.\n *\n * @group Data\n */\nexport type FindAllParams<M extends Record<string, unknown> = Record<string, unknown>> =\n IterateParams<M> & {\n /**\n * Most rows to materialise. Defaults to 10 000. Exceeding it **throws**\n * — a truncated array returned as if it were the whole answer is the\n * kind of quiet wrong that shows up months later in a report. Pass\n * `Infinity` to opt out deliberately, or use `iterate()` to stream.\n */\n maxRows?: number;\n };\n\n/**\n * Fluent Query Builder Interface for the SDK client.\n * Returns `FindResult<M>` (flat rows) instead of `FindResponse<M>` (Entity-wrapped).\n *\n * @group Data\n */\nexport interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;\n /**\n * Filter on a relation path (`author.name`) or a JSON path\n * (`metadata->>tier`).\n *\n * A separate overload because the value cannot be typed: neither addresses\n * a column of `M`, so there is nothing in a generated row type to check\n * against — the driver resolves the path and refuses what it cannot. The\n * key is still constrained to a *path*, so a mistyped column name does not\n * fall through to here and lose its check.\n *\n * `find({ where })` has accepted both all along ({@link FieldPath}); the\n * builder did not, so the documented relation-path filters were compile\n * errors on a typed client.\n */\n where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): this;\n where(logicalCondition: LogicalCondition): this;\n /**\n * Sort by a column, a relation or JSON path, `_score`, or an aggregate over\n * a to-many relation — the same key set {@link FindParams.orderBy} takes.\n */\n orderBy(\n column: FieldPath<M> | ComputedSortField | RelationAggregateSort,\n direction?: \"asc\" | \"desc\",\n nulls?: NullsPlacement\n ): this;\n limit(count: number): this;\n offset(count: number): this;\n search(searchString: string, options?: { explain?: boolean }): this;\n\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n *\n * Postgres only, over a property declared as `type: \"vector\"`. Each row\n * comes back with a `_distance`. Any `where` on the same query filters\n * before the ordering; distance decides the order.\n *\n * The query embedding is the caller's to produce.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): this;\n /**\n * Load relations — names, dotted paths (`\"comments.author\"`), or the\n * parametrised tree. Repeated calls merge rather than replace.\n */\n include(...relations: (string | IncludeSpec)[]): this;\n /**\n * Return only these columns. A real projection: the columns are what is\n * read from the database, not what survives a trim of the response.\n */\n fields(...columns: (FieldPath<M> | string)[]): this;\n /** `SELECT DISTINCT` over the projection — see {@link FindParams.distinct}. */\n distinct(enabled?: boolean): this;\n /** Continue after a previous page's `meta.nextCursor`. */\n after(cursor: string): this;\n find(): Promise<FindResult<M>>;\n /**\n * Aggregate the rows this query matches instead of returning them.\n *\n * The builder's `where`/`logical`/`search` narrow which rows are\n * aggregated; its `orderBy`, `include` and window do not apply and are\n * ignored, exactly as they are on the REST route.\n */\n aggregate(params: Omit<AggregateParams<M>, \"where\" | \"logical\" | \"searchString\">): Promise<AggregateRow[]>;\n\n /**\n * Page through everything this query matches, one row at a time.\n *\n * The same walker {@link SDKCollectionClient.iterate} uses, so the ceiling\n * on `limit` is not a ceiling on what a query can read. `.limit()` set on\n * the builder becomes the **page size** here, not a total.\n */\n iterate(options?: PageWalkOptions<M>): AsyncIterableIterator<M>;\n\n /**\n * Collect everything this query matches into one array.\n *\n * {@link SDKCollectionClient.findAll}'s `maxRows` guard applies: an\n * unbounded collect is a memory hazard, so it stops and says so rather than\n * growing until the process dies.\n */\n findAll(options?: PageWalkOptions<M> & { maxRows?: number }): Promise<M[]>;\n\n count(): Promise<number>;\n listen(onUpdate: (data: FindResult<M>) => void, onError?: (error: Error) => void): () => void;\n}\n\n/**\n * SDK collection client — returns flat rows, no Entity wrapper.\n *\n * This is the public API surface for app developers using\n * `createRebaseClient()`. admin internals use `CollectionAccessor` instead.\n *\n * Type parameters:\n * - `M` — the **Row** shape returned by reads (`find`, `findById`, `listen`).\n * - `I` — the **Insert** shape accepted by {@link create}. Defaults to\n * `Partial<M>`; the generated SDK supplies a dedicated `Insert` type where\n * required columns are required and auto-generated / read-only columns are\n * omitted, so `create({})` on a table with required fields is a compile error.\n * - `U` — the **Update** shape accepted by {@link update}. Defaults to\n * `Partial<M>`; the generated SDK supplies a dedicated `Update` type.\n *\n * @example\n * const { data: posts } = await rebase.data.posts.find();\n * console.log(posts[0].title); // flat access\n * console.log(posts[0].id); // id at top level\n *\n * const post = await rebase.data.posts.findById(1);\n * console.log(post?.title); // no .values needed\n *\n * @group Data\n */\n/**\n * A change expressed as an operation on the column's current value, rather than\n * as the value to store.\n *\n * `{ views: 5 }` says what the number becomes; `{ views: { $inc: 1 } }` says\n * what happens to it. The difference is the read the caller no longer has to\n * make — and the race that read opens. Two requests that each read `4`, add one\n * and write `5` lose an increment between them; `SET views = views + 1` cannot,\n * because the arithmetic happens inside the statement holding the row lock.\n *\n * Exactly one operator per field. `{ views: { $inc: 1, $push: \"x\" } }` is\n * refused rather than applied in an order the caller cannot see.\n *\n * @group Data\n */\n/**\n * The operator names, as a value.\n *\n * A runtime list beside the type because three layers have to *recognise* an\n * operation, not just accept one: the REST validator, the driver that compiles\n * it, and the offline queue that must refuse to apply one locally. Three copies\n * of four strings is three chances for one of them to miss an operator added to\n * the other two, and the failure is silent in the worst direction — an\n * unrecognised marker is written to the column as a JSON document.\n *\n * @group Data\n */\nexport const FIELD_OPERATORS = [\"$inc\", \"$push\", \"$pull\", \"$merge\"] as const;\n\n/**\n * The key of a {@link BatchRef}. Declared here, beside the field operators,\n * because the two share one namespace: a `$`-prefixed key in a write payload is\n * a marker, and every reader of that namespace has to know all of it.\n *\n * @group Data\n */\nexport const BATCH_REF_KEY = \"$ref\";\n\n/**\n * Whether a value is *trying* to be a field operation — including a misspelled\n * one, which is the case worth catching.\n *\n * Any `$`-prefixed key counts, because `{ $increment: 1 }` written to a number\n * column as a JSON document is the failure this exists to prevent. No collection\n * can declare a column whose value legitimately has a key beginning with `$`: a\n * `map` property's sub-keys are declared, and `$` is not valid in the\n * identifiers the DDL generators emit.\n *\n * The one exception is `{ $ref: … }`, the batch's backward reference. It stands\n * where a *value* goes and is resolved to one before the row is written, so it\n * is not an operation on a column — reading it as a misspelled operator refused\n * every `$ref` in a batch with \"unknown field operator '$ref'\".\n *\n * @group Data\n */\nexport function isFieldOperation(value: unknown): boolean {\n if (typeof value !== \"object\" || value === null || Array.isArray(value) || value instanceof Date) {\n return false;\n }\n const keys = Object.keys(value);\n if (keys.length === 1 && keys[0] === BATCH_REF_KEY) return false;\n return keys.some((key) => key.startsWith(\"$\"));\n}\n\n/** True when any value in a write payload is (or is attempting to be) one. @group Data */\nexport function hasFieldOperation(values: Record<string, unknown> | undefined): boolean {\n return !!values && Object.values(values).some(isFieldOperation);\n}\n\nexport type FieldOperation =\n /** Add to a `number` column; negative to subtract. `SET col = col + n`. */\n | { $inc: number }\n /** Append one value, or each of an array of values, to an `array` column. */\n | { $push: unknown }\n /** Remove every occurrence of a value from an `array` column. */\n | { $pull: unknown }\n /** Shallow-merge an object into a `map` column. `SET col = col || …::jsonb`. */\n | { $merge: Record<string, unknown> };\n\n/**\n * The payload {@link SDKCollectionClient.update} accepts: plain values, field\n * operations, or both in one body.\n *\n * @group Data\n */\nexport type UpdateValues<U> = { [K in keyof U]?: U[K] | FieldOperation };\n\n/**\n * Where an upsert looks for the row it might be replacing.\n *\n * The columns must carry a uniqueness guarantee the database can use as an\n * `ON CONFLICT` target — the primary key, a property with\n * `validation.unique`, or the columns of a declared `unique` index. Anything\n * else is refused with a 400 rather than sent to Postgres, which would answer\n * `there is no unique or exclusion constraint matching the ON CONFLICT\n * specification` from inside a transaction that has already done work.\n *\n * @group Data\n */\nexport interface UpsertOptions extends WriteOptions {\n /** Column names forming the conflict target. Defaults to the primary key. */\n onConflict?: readonly string[];\n}\n\n/**\n * A placeholder standing for a value only the server will know: the id of a row\n * an earlier operation in the same batch creates.\n *\n * `{ \"$ref\": \"order.id\" }` reads the field `id` off the result of the operation\n * that named itself `ref: \"order\"`. Without it a batch cannot express the one\n * thing a cross-collection batch exists for — writing a parent and its children\n * together — because the child's foreign key is not knowable until the parent\n * has been inserted, and splitting the two into separate requests is exactly\n * the non-atomic sequence the batch replaces.\n *\n * Only backward references resolve. `ref` names must be unique within a batch,\n * and an operation may not reference itself or anything after it.\n *\n * @group Data\n */\nexport interface BatchRef {\n /** `<ref name>.<field>`, e.g. `order.id`. */\n $ref: string;\n}\n\n/** One entry of a batch request. @group Data */\nexport type BatchOperation<DB = Record<string, unknown>> = {\n [K in Extract<keyof DB, string>]:\n | {\n op: \"create\";\n collection: K;\n values: { [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef } & Record<string, unknown>;\n /** Name this row so a later operation can reference its columns. */\n ref?: string;\n }\n | {\n op: \"upsert\";\n collection: K;\n values: { [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef } & Record<string, unknown>;\n /** See {@link UpsertOptions.onConflict}. Defaults to the primary key. */\n onConflict?: readonly string[];\n ref?: string;\n }\n | {\n op: \"update\";\n collection: K;\n id: string | number | BatchRef;\n values: { [F in keyof UpdateOf<DB[K]>]?: UpdateOf<DB[K]>[F] | FieldOperation | BatchRef } & Record<string, unknown>;\n ref?: string;\n }\n | {\n op: \"delete\";\n collection: K;\n id: string | number | BatchRef;\n ref?: string;\n };\n}[Extract<keyof DB, string>];\n\n/**\n * What `POST /api/data/_batch` answers with.\n *\n * `data` is aligned to `operations`: the written row for a create, upsert or\n * update, and `null` for a delete — so an index into one is an index into the\n * other, whatever the batch mixed.\n *\n * @group Data\n */\nexport interface BatchResult<R = Record<string, unknown>> {\n data: (R | null)[];\n meta: { operations: number };\n}\n\n/**\n * Per-request options for a write.\n * @group Data\n */\nexport interface WriteOptions {\n /**\n * Names this write, so re-sending it is recognised instead of repeated.\n *\n * A client that does not see a response cannot know whether the write\n * committed. Retrying is therefore the only option, and without a key the\n * server has no way to tell a retry from a second, genuinely new write — so\n * it performs it again. On a table with a server-assigned id that is a\n * duplicate row, because the id the client chose was never used.\n *\n * A key names **one** request, not a job. It records the method, the path\n * and the body it was claimed for, so re-sending that exact request replays\n * its answer, while the same key on a different one is refused with\n * `IDEMPOTENCY_KEY_REUSED` (422) rather than silently answered with the\n * first request's result. Pass a fresh key — a uuid — per call; a reusable\n * business id shared by the create and the delete of one import means the\n * second of them never runs.\n *\n * Set by the offline queue on every replay. Honoured for 24 hours and scoped\n * to the authenticated user — an unauthenticated caller has no principal to\n * scope it to, so the key is ignored there. A retry sent while the first\n * attempt is still being answered gets `IDEMPOTENCY_KEY_IN_PROGRESS` (409)\n * and should be sent again. A server that cannot store keys ignores the\n * header rather than refusing the write.\n */\n idempotencyKey?: string;\n\n /**\n * Whether the server should send the written row back.\n *\n * `false` sends `Prefer: return=minimal`, and the write answers `204 No\n * Content` — `200` carrying the ids only, for a batch. The row is the\n * default because it carries what the server decided: a serial id, an\n * `autoValue` timestamp, whatever `beforeSave` rewrote. A caller that\n * needs none of that is paying for a full row serialisation and, on\n * Postgres, a read-back per written row.\n *\n * Reach for it on imports and fire-and-forget writes. The method resolves\n * to `undefined` (or `[]`) when it is set, so a caller cannot accidentally\n * use a row the server never sent.\n */\n returning?: boolean;\n\n /**\n * The version of the row this write was made against, so it is refused if\n * the row has moved on.\n *\n * The `ETag` from the read that produced the row — `etagOf(row)` on a row\n * from `findById`, or the `ETag` response header. A mismatch answers `412`\n * rather than writing, which is the difference between \"update the row I\n * read\" and \"overwrite whatever is there now\". Without it a read, an edit\n * and a write is last-writer-wins over everything the write did not send,\n * and the loser is told nothing.\n *\n * `\"*\"` asserts only that the row exists.\n *\n * Honoured on `update` and `delete`.\n */\n ifMatch?: string;\n}\n\nexport interface SDKCollectionClient<\n M extends Record<string, unknown> = Record<string, unknown>,\n I = Partial<M>,\n U = Partial<M>\n> {\n /**\n * Find multiple records with optional filtering, pagination, and sorting.\n *\n * ## What a list method returns\n *\n * Two shapes, and one rule that tells them apart: **a window is wrapped, a\n * whole answer is not.**\n *\n * - `find()` and `listen()` return {@link FindResult} — `{ data, meta }` —\n * because they hand back *one page*. `meta.total` and `meta.hasMore` are\n * the caller's only way to know there is more, so a bare array would lose\n * the answer to the question the call raises.\n * - `findAll()`, `createMany()` and `updateMany()` return a plain `M[]`,\n * because there is nothing left over to report: the walk finished, or the\n * batch is exactly the rows that were written. A `meta` there would be\n * `{ total: rows.length, hasMore: false }`, which says nothing.\n * - `iterate()` yields rows one at a time and never materialises a list at\n * all.\n *\n * So `data` is not a wrapper the SDK sometimes adds and sometimes forgets —\n * it is where the pagination metadata lives, and it is present exactly when\n * there is some.\n */\n find(params?: FindParams<M>): Promise<FindResult<M>>;\n\n /**\n * Walk every record matching a query, one row at a time, fetching pages as\n * the consumer consumes them.\n *\n * This is the pagination primitive: `find()` returns one window, `iterate()`\n * returns all of them without the caller hand-rolling the\n * `limit` / `offset += ` / \"am I done yet\" loop. Nothing is buffered — rows\n * are yielded as each page arrives, so a million-row walk costs one page of\n * memory. `break` stops the walk and no further requests are made.\n *\n * Termination is driven by the server's `meta.hasMore`, never by comparing\n * a page's length against the requested limit — a final page that happens\n * to be exactly full is indistinguishable that way, and a walk that stops\n * there drops rows. An empty page also ends the walk, and\n * {@link PageWalkOptions.maxPages} bounds a server that never stops saying\n * there is more.\n *\n * ## Consistency\n *\n * By default this pages by **offset**, which is only as stable as the table\n * is still: a row inserted or deleted ahead of the cursor between two\n * requests shifts every later window, so the walk can skip a row or hand\n * back the same one twice. That is inherent to offset paging, not a bug\n * here. On a collection with a unique sortable column, pass\n * {@link PageWalkOptions.cursor} to seek on it instead — the walk then\n * asks for rows strictly after the last one it saw, which concurrent writes\n * cannot perturb.\n *\n * @example\n * for await (const job of client.data.jobs.iterate({\n * where: { status: [\"==\", \"queued\"] },\n * cursor: \"id\",\n * pageSize: 500\n * })) {\n * await handle(job);\n * }\n */\n iterate(params?: IterateParams<M>): AsyncIterableIterator<M>;\n\n /**\n * {@link iterate}, collected into an array.\n *\n * Convenient when the result is known to be small and awkward to stream.\n * Because \"known to be small\" is an assumption and not a fact, the result is\n * capped — 10 000 rows by default — and going over the cap **throws**\n * rather than returning a short array that reads like a complete one. Raise\n * {@link FindAllParams.maxRows} when the data really is bigger, or switch to\n * `iterate()` and stream it.\n *\n * The offset-drift caveat on {@link iterate} applies here too.\n *\n * @throws When more rows match than `maxRows` allows.\n *\n * @example\n * const overdue = await client.data.invoices.findAll({\n * where: { due_at: [\"<\", today] },\n * cursor: \"id\"\n * });\n */\n findAll(params?: FindAllParams<M>): Promise<M[]>;\n\n /**\n * Find a single record by its ID.\n */\n findById(id: string | number): Promise<M | undefined>;\n\n /**\n * Read one record by its ID, or throw if it is not there.\n *\n * The counterpart to {@link findById}, and the one most reads want. A row\n * fetched by an id that came from a link, a route parameter or another row\n * is expected to exist; when it does not, that is the error case, not a\n * value to thread through the rest of the function.\n *\n * `findById` returns `M | undefined`, so every caller had to prove the row\n * existed before touching a field:\n *\n * ```ts\n * const post = await rebase.data.posts.findById(id);\n * post.title; // TS18048: 'post' is possibly 'undefined'\n * const ok = (await rebase.data.posts.findById(id))!.title; // the `!` everyone reaches for\n * ```\n *\n * With `get`, the absent case is an exception with a code you can branch on,\n * and the happy path is typed as present:\n *\n * ```ts\n * const post = await rebase.data.posts.get(id); // M, not M | undefined\n * ```\n *\n * Same split as Prisma's `findUnique` / `findUniqueOrThrow`: two contracts,\n * both wanted, named so the choice is visible at the call site.\n *\n * @throws {RebaseApiError} `NOT_FOUND` (status 404) when no such row exists,\n * or is visible to the caller — row-level security makes a row the caller\n * may not read indistinguishable from one that is not there, deliberately.\n */\n get(id: string | number): Promise<M>;\n\n /**\n * Create a new record.\n * @param data The record data to create (the collection's `Insert` shape).\n * @param id Optional specific id, sent as an `id` column. This is for tables\n * whose key *is* `id`: the value goes in as that column. For a table keyed\n * on anything else (a `sku`, a composite key), there is no `id` column to\n * receive it — put the key in `data` instead, where it belongs among the\n * columns.\n * @returns The created row\n */\n create(data: I, id?: string | number, options?: WriteOptions): Promise<M>;\n\n /**\n * Write many records in a single request and a single transaction.\n *\n * Built for imports and ETL, where one call per row means one HTTP round\n * trip and one transaction per row. Every record still runs the normal\n * pipeline — callbacks, relations, row-level security — and the batch is\n * all-or-nothing: if any record is rejected, none of them land and the\n * error names the offending index.\n *\n * A record carrying its primary key updates that row; one without inserts.\n * With `{ upsert: true }` each record is written as INSERT ... ON CONFLICT\n * DO UPDATE on the primary key instead, which is what makes a re-runnable\n * import idempotent.\n *\n * Batches are capped server-side (1000 rows by default) because one batch\n * holds its locks for the whole transaction — chunk larger jobs.\n *\n * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.\n * A client that never sees the response cannot know whether the batch\n * committed, and without a key the server cannot tell the retry from a\n * second genuine import — so it performs it again, duplicating every row in\n * the batch rather than just one.\n *\n * @returns The written rows, in the order given.\n *\n * @example\n * ```ts\n * for (const chunk of chunks(rows, 1000)) {\n * await client.data.products.createMany(chunk, { upsert: true });\n * }\n * ```\n */\n createMany(data: I[], options?: { upsert?: boolean; onConflict?: readonly string[] } & WriteOptions): Promise<M[]>;\n\n /**\n * Update an existing record by ID.\n * @param data The fields to update (the collection's `Update` shape).\n * @param options Per-request write options — notably `idempotencyKey`.\n * @returns The updated row.\n * @throws {RebaseApiError} with status 404 when the record does not exist.\n *\n * `create`, `createMany`, `updateMany`, `delete` and `deleteMany` all took\n * {@link WriteOptions}; this one did not, so the single-row update was the\n * one write on the surface that could not be made idempotent. A client that\n * never sees the response retries, and without a key the server cannot tell\n * that retry from a second deliberate edit — which on a `PATCH` that\n * increments or appends is a second edit applied.\n */\n update(id: string | number, data: U | UpdateValues<U>, options?: WriteOptions): Promise<M>;\n\n /**\n * Insert the row, or replace the one already occupying its key.\n *\n * `INSERT ... ON CONFLICT DO UPDATE`, in one statement — so unlike a\n * `findById` followed by `create`-or-`update` it cannot lose the race\n * between the two, and unlike `create` it does not fail when the row is\n * already there. That is what makes a re-runnable import idempotent\n * without a key.\n *\n * The conflict target defaults to the primary key. Pass `onConflict` to\n * upsert on a natural key instead — `[\"email\"]`, `[\"tenant_id\", \"slug\"]` —\n * and the columns must carry a uniqueness guarantee the database can use:\n * a property with `validation.unique`, or the columns of a declared\n * `unique` index. Anything else is a 400 rather than a Postgres error\n * raised half-way through a transaction.\n *\n * The `on_create` timestamp of a row that already existed is left alone: a\n * conflict means the row's creation is a fact about the past, and a nightly\n * re-import that reset `createdAt` on everything it touched would take\n * every \"new this week\" query with it.\n *\n * @example\n * ```ts\n * await client.data.users.upsert(\n * { email: \"a@b.c\", name: \"Ada\" },\n * { onConflict: [\"email\"] }\n * );\n * ```\n */\n upsert(data: I, options?: UpsertOptions): Promise<M>;\n\n /**\n * Update many records in a single request and a single transaction.\n *\n * The counterpart to {@link createMany}, and the reason it exists is the\n * same: one call per row means one HTTP round trip and one transaction per\n * row. Every record still runs the normal pipeline — callbacks, relations,\n * row-level security — and the batch is all-or-nothing, so a rejected\n * record leaves none of them written and the error names the offending\n * index.\n *\n * Each entry is `{ id, data }` rather than a flat row carrying its own key.\n * That is deliberate: on a table keyed on something other than `id` — a\n * `sku`, a composite key — a flat row cannot say whether a column is the\n * address or a value to write. Naming the address separately mirrors\n * single-row `update(id, data)` exactly and leaves nothing to infer.\n *\n * An id that matches no row fails the batch with a 404 rather than being\n * skipped, for the same reason `update()` does: silently updating four of\n * five rows is worse than updating none.\n *\n * Batches share `createMany`'s server-side cap (1000 rows by default),\n * because one batch holds its locks for the whole transaction.\n *\n * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.\n * An update replayed in full is naturally idempotent, but one interleaved\n * with another writer's is not — the key is what stops a lost ACK from\n * re-applying a stale batch over newer data.\n *\n * @returns The updated rows, in the order given.\n *\n * @example\n * ```ts\n * await client.data.orders.updateMany([\n * { id: \"o-1\", data: { status: \"shipped\" } },\n * { id: \"o-2\", data: { status: \"shipped\" } }\n * ]);\n * ```\n */\n updateMany(updates: { id: string | number; data: U | UpdateValues<U> }[], options?: WriteOptions): Promise<M[]>;\n\n /**\n * Delete a record by ID.\n * @throws {RebaseApiError} with status 404 when the record does not exist.\n *\n * Takes {@link WriteOptions} like every other write. It did not, so the one\n * mutation that cannot be made safe by repeating it — a delete replayed\n * after the row is gone answers 404, which an offline queue reads as a\n * permanent failure — was also the one that could not carry an\n * `idempotencyKey`.\n */\n delete(id: string | number, options?: WriteOptions): Promise<void>;\n\n /**\n * Delete many records in a single request and a single transaction.\n *\n * Takes ids, not a filter. A filter-shaped bulk delete is a different and\n * far more dangerous operation — the failure mode is an omitted or\n * mistyped condition emptying a table, and it cannot be reviewed at the\n * call site the way an explicit list can. Read first, then pass the ids you\n * meant.\n *\n * `beforeDelete` and `afterDelete` fire per row, exactly as they do for\n * single deletes, and returning `false` from `beforeDelete` fails the batch\n * rather than quietly dropping one row from it. All-or-nothing, so an id\n * that matches no row 404s the whole call.\n *\n * Shares `createMany`'s row cap.\n *\n * @example\n * ```ts\n * const stale = await client.data.sessions.findAll({\n * where: { expires_at: [\"<\", cutoff] }\n * });\n * await client.data.sessions.deleteMany(stale.map(s => s.id as string));\n * ```\n */\n deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;\n\n /**\n * The low-level realtime subscription: raw server pushes, nothing else.\n *\n * **Prefer `observe()`** on a client from `@rebasepro/client`, which wraps\n * this one and is what a UI actually wants — it emits from the local\n * database first when offline is enabled, re-emits on local writes and\n * rollbacks, and de-duplicates emissions so a refresh that changes nothing\n * does not call back. `listen` does none of that; it forwards what the\n * socket sends.\n *\n * Always present. A client that cannot subscribe — one built with\n * `realtime: false`, or on a driver with no `listenCollection` — installs a\n * stub that throws a `RebaseClientError` naming the configuration that\n * would make it work. It used to be optional, which made every call site\n * either write `listen!(…)` or a null check the type system could not tell\n * apart from a real capability question; the answer to *that* question is\n * {@link isUnsupported}, and the answer for ordinary code is to just call\n * it.\n *\n * `observe()` degrades to a single fetch instead of throwing, which is the\n * other reason to reach for it instead.\n */\n listen(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;\n\n /** {@link listen} for a single row. Prefer `observeById()`. */\n listenById(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;\n\n /**\n * Count the number of records matching the given filter.\n *\n * Always present; see {@link listen} for what a transport that cannot serve\n * it does instead.\n */\n count(params?: FindParams<M>): Promise<number>;\n\n /**\n * `count`/`sum`/`avg`/`min`/`max` over the matching rows, optionally\n * grouped — the SDK half of `GET /<collection>/aggregate`.\n *\n * The whole point is not to fetch rows in order to reduce them: \"revenue by\n * status\" over a million orders is one query and one row per status here,\n * and a `findAll()` plus a loop everywhere else — which is wrong under a\n * `limit` and unaffordable without one. It runs through the same\n * request-scoped handle as every other read, so RLS applies to the rows\n * being aggregated.\n *\n * ```ts\n * const rows = await rebase.data.orders.aggregate({\n * select: [{ fn: \"sum\", field: \"total\" }, { fn: \"count\" }],\n * groupBy: [\"status\"],\n * where: { created_at: [\">=\", startOfMonth] }\n * });\n * // [{ status: \"paid\", sum_total: 41822.5, count: 317 }, …]\n * ```\n *\n * Always present; a backend whose driver cannot aggregate answers 501\n * naming the capability rather than an empty result set, which would read\n * as \"nothing matched\".\n */\n aggregate(params: AggregateParams<M>): Promise<AggregateRow[]>;\n\n // Fluent Query Builder\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M>;\n /** A relation path (`author.name`) or a JSON path (`metadata->>tier`). */\n where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): SDKQueryBuilderInterface<M>;\n where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;\n orderBy(\n column: FieldPath<M> | ComputedSortField | RelationAggregateSort,\n direction?: \"asc\" | \"desc\",\n nulls?: NullsPlacement\n ): SDKQueryBuilderInterface<M>;\n limit(count: number): SDKQueryBuilderInterface<M>;\n offset(count: number): SDKQueryBuilderInterface<M>;\n search(searchString: string, options?: { explain?: boolean }): SDKQueryBuilderInterface<M>;\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n * Postgres only, over a `type: \"vector\"` property. See\n * {@link SDKQueryBuilderInterface.vectorSearch}.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): SDKQueryBuilderInterface<M>;\n include(...relations: (string | IncludeSpec)[]): SDKQueryBuilderInterface<M>;\n /** {@link SDKQueryBuilderInterface.fields} */\n fields(...columns: (FieldPath<M> | string)[]): SDKQueryBuilderInterface<M>;\n /** {@link SDKQueryBuilderInterface.distinct} */\n distinct(enabled?: boolean): SDKQueryBuilderInterface<M>;\n /** {@link SDKQueryBuilderInterface.after} */\n after(cursor: string): SDKQueryBuilderInterface<M>;\n}\n\n/**\n * The unified data access object for the **admin panel** (Entity-shaped).\n *\n * Access collections as dynamic properties: `data.products.find(...)`. Each\n * accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the\n * view-model the admin renders. This is what `useData()` / the admin\n * `RebaseContext.data` are backed by.\n *\n * @internal App developers do **not** use this — they use\n * {@link RebaseSdkData} (flat rows), which is what the SDK client and backend\n * `context.data` expose. This Entity-shaped map backs the admin panel only.\n *\n * @group Data\n */\nexport type RebaseData<DB = unknown> = {\n /**\n * Get a collection accessor by slug.\n * Alternative to dynamic property access for cases where\n * the collection name is a variable.\n *\n * @example\n * const accessor = data.collection(\"products\");\n * await accessor.find({ limit: 10 });\n */\n collection<M extends Record<string, unknown> = Record<string, unknown>>(slug: string): CollectionAccessor<M>;\n} & (\n DB extends Record<string, unknown>\n ? { [K in keyof DB]: CollectionAccessor<DB[K] extends { Row: infer R extends Record<string, unknown> } ? R : Record<string, unknown>> }\n : {\n /**\n * Dynamic collection accessor.\n * Access any collection by its slug as a property.\n *\n * The index signature is `CollectionAccessor` alone, for the reason\n * spelled out on {@link RebaseSdkData}: unioning in the `collection`\n * method's own signature is unnecessary across an intersection, and it\n * costs `data.products.find()` — the access this `@example` documents.\n *\n * @example\n * data.products.find({ where: { status: [\"==\", \"published\"] } })\n */\n [collectionSlug: string]: CollectionAccessor;\n }\n);\n\n/**\n * The unified data access object for the **SDK** — flat rows, no Entity wrapper.\n *\n * This is the symmetric developer-facing data API, identical in shape on both\n * sides of the stack:\n * - The frontend SDK client (`client.data.products.find()`)\n * - Backend framework callbacks & scripts (`context.data.products.find()`)\n *\n * Every accessor returns flat rows (the table's columns) via\n * {@link SDKCollectionClient} — access fields directly (`row.title`), never\n * `row.values.title`. The admin uses {@link RebaseData} (Entity) instead.\n *\n * @example\n * // Frontend SDK\n * const { data: posts } = await client.data.posts.find();\n * console.log(posts[0].title); // flat — no .values\n *\n * // Backend callback — identical shape\n * callbacks: {\n * beforeSave: async ({ context }) => {\n * const product = await context.data.products.findById(id);\n * console.log(product?.price); // flat — no .values\n * }\n * }\n *\n * @group Data\n */\n/**\n * Extract the `Row` shape from a generated `Database[slug]` entry, falling\n * back to an open record when the entry is untyped.\n * @group Data\n */\nexport type RowOf<T> = T extends { Row: infer R extends Record<string, unknown> } ? R : Record<string, unknown>;\n\n/**\n * Extract the `Insert` shape from a generated `Database[slug]` entry (the\n * input accepted by `create`), falling back to `Partial<Row>`.\n * @group Data\n */\nexport type InsertOf<T> = T extends { Insert: infer I extends Record<string, unknown> } ? I : Partial<RowOf<T>>;\n\n/**\n * Extract the `Update` shape from a generated `Database[slug]` entry (the\n * input accepted by `update`), falling back to `Partial<Row>`.\n * @group Data\n */\nexport type UpdateOf<T> = T extends { Update: infer U extends Record<string, unknown> } ? U : Partial<RowOf<T>>;\n\n/**\n * Note on the untyped branch below: its index signature is\n * `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.\n *\n * The union looks like it is needed so `collection` — a method on this same\n * object — satisfies the index signature. It is not, because `collection` is\n * declared in a *separate* member of the intersection, and TypeScript only\n * requires named properties to be assignable to an index signature declared\n * alongside them. Including the function arm cost the documented accessor:\n *\n * rebase.dataAsAdmin.projects.find()\n * // ^ Property 'find' does not exist on type\n * // 'SDKCollectionClient | ((slug: string) => …)'\n *\n * Every project without a generated `Database` type lands on this branch, so\n * property-style access — the form used by the `@example` below, by the\n * scaffolded function template, and by the 0.13 migration note — did not\n * compile for any of them. Do not restore the arm; use `collection(slug)` if a\n * caller genuinely needs the by-slug function.\n */\nexport type RebaseSdkData<DB = unknown> = {\n /**\n * Get a flat collection accessor by slug.\n *\n * @example\n * const accessor = data.collection(\"products\");\n * await accessor.find({ limit: 10 });\n */\n collection<M extends Record<string, unknown> = Record<string, unknown>>(slug: string): SDKCollectionClient<M>;\n} & (\n DB extends Record<string, unknown>\n ? { [K in keyof DB]: SDKCollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>> }\n : {\n /**\n * Dynamic flat collection accessor.\n * Access any collection by its slug as a property.\n *\n * @example\n * data.products.find({ where: { status: [\"==\", \"published\"] } })\n */\n [collectionSlug: string]: SDKCollectionClient;\n }\n);\n","import { RebaseApiError } from \"../errors\";\nimport type { CollectionRegistryController } from \"./collection_registry\";\nimport type { EntityStatus, EntityValues } from \"../types/entities\";\nimport type { CollectionConfig, FilterValues } from \"../types/collections\";\nimport type { OrderByTuple } from \"../types/filter-operators\";\nimport type { RebaseCallContext } from \"../call_context\";\nimport type { IncludeSpec, LogicalCondition } from \"./data\";\nimport type { CollectionUpdateMeta } from \"../types/websockets\";\n\n\n/**\n * @internal\n */\nexport interface FetchOneProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n id: string | number;\n databaseId?: string;\n collection?: CollectionConfig<M>\n /**\n * See {@link FetchCollectionProps.withDeleted}. A soft-deleted row is a 404\n * here by default, so `findById` and `find` agree about which rows exist —\n * a row you cannot find in a listing and can still open by id is the kind\n * of inconsistency that makes a feature untrustworthy.\n */\n withDeleted?: boolean | \"only\";\n}\n\n/**\n * @internal\n */\nexport type ListenOneProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchOneProps<M>\n & {\n onUpdate: (row: Record<string, unknown> | null) => void,\n onError?: (error: Error) => void,\n }\n\n/**\n * Configuration for vector similarity search queries.\n * Vector search applies an ORDER BY distance expression and optionally\n * filters results by a distance threshold.\n */\nexport interface VectorSearchParams {\n /** Property name containing the vector column */\n property: string;\n /** Query vector to compare against */\n vector: number[];\n /** Distance function (default: \"cosine\") */\n distance?: \"cosine\" | \"l2\" | \"inner_product\";\n /** Only return results within this distance threshold */\n threshold?: number;\n}\n\n// ── List pagination bounds ────────────────────────────────────────────────\n//\n// Client-driven list reads (REST `GET /<collection>` and the WebSocket\n// `subscribe_collection` message) accept a client-supplied `limit`. Without\n// bounds, an ABSENT limit streams the entire table into memory — a trivial\n// OOM/DoS — and `limit=100000000` (or `limit=0`, historically an unlimited\n// bypass) is honoured verbatim. `resolveClientListLimit` is the single shared\n// enforcement point so every untrusted ingress behaves identically. Trusted\n// server-side callers build fetch options directly and are intentionally NOT\n// bounded here (migrations, admin exports, and CDC refetches may need the full\n// set).\n//\n// A limit the platform will not serve is REFUSED, not quietly shrunk. Clamping\n// answers a request for 100 000 rows with 1 000 of them, and a short page is\n// indistinguishable from \"that is all the data there is\" — which is how a CSV\n// export shipped 50 rows of a 100 000-row collection under a filename that read\n// like the whole thing. `meta.total`/`meta.hasMore` make truncation *detectable*\n// on the REST list response, but only for a caller who thinks to compare what it\n// asked for against what it got, and the WebSocket `collection_update` frame\n// carries neither — so signalling cannot be the answer on every surface and\n// rejecting is. An ABSENT limit still defaults: naming no window is not the same\n// as asking for one that cannot be served.\n\n/** Rows returned for a plain / text-search list read when the client sends no `limit`. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Rows returned for a vector-search list read when the client sends no `limit`. */\nexport const DEFAULT_VECTOR_LIST_LIMIT = 10;\n/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */\nexport const MAX_LIST_LIMIT = 1000;\n\n/** Overridable bounds for {@link resolveClientListLimit}. */\nexport interface ListLimitBounds {\n /** Default page size for plain and text-search reads. */\n defaultLimit?: number;\n /** Default page size for vector-search reads. */\n vectorDefaultLimit?: number;\n /** Largest limit a client may ask for. A larger one is rejected, not clamped. */\n maxLimit?: number;\n}\n\n/**\n * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not\n * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it\n * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.\n *\n * @group Errors\n */\nexport class ListLimitError extends RebaseApiError {\n /** The ceiling that was exceeded — what the caller should page by instead. */\n readonly maxLimit: number;\n\n constructor(message: string, maxLimit: number) {\n super(message, { status: 400, code: \"INVALID_LIMIT\" });\n this.name = \"ListLimitError\";\n this.maxLimit = maxLimit;\n // Keeps `instanceof` working when this is compiled down for an older\n // target, where extending a builtin otherwise loses the prototype.\n Object.setPrototypeOf(this, ListLimitError.prototype);\n }\n}\n\n/**\n * Resolve a client-supplied list `limit` into a safe, always-defined value.\n *\n * - An absent / blank limit falls back to the mode default:\n * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.\n * - A limit that is present must be an integer in `[1, maxLimit]`. Anything\n * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws\n * {@link ListLimitError} rather than being coerced into range, because every\n * coercion answers a question the caller did not ask with a page it cannot\n * tell apart from the whole collection.\n *\n * The return is never `undefined` — no ingress that routes its client limit\n * through this can produce an unbounded read.\n *\n * @throws {ListLimitError} when a present `limit` is not an integer in range.\n */\nexport function resolveClientListLimit(\n rawLimit: number | string | null | undefined,\n opts: ListLimitBounds & { vectorSearch?: boolean } = {}\n): number {\n const maxLimit = opts.maxLimit ?? MAX_LIST_LIMIT;\n if (rawLimit != null && String(rawLimit).trim() !== \"\") {\n // `Number`, not `parseInt`: `parseInt(\"50rows\")` is 50, which silently\n // reads a typo as a window the caller never wrote.\n const parsed = typeof rawLimit === \"number\" ? rawLimit : Number(String(rawLimit).trim());\n if (!Number.isInteger(parsed) || parsed < 1) {\n throw new ListLimitError(\n `Invalid \\`limit\\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`,\n maxLimit\n );\n }\n if (parsed > maxLimit) {\n throw new ListLimitError(\n `\\`limit\\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows ` +\n \"per read and page through the rest with `offset` — answering with a smaller page would be \" +\n \"indistinguishable from there being no more rows.\",\n maxLimit\n );\n }\n return parsed;\n }\n return opts.vectorSearch\n ? (opts.vectorDefaultLimit ?? DEFAULT_VECTOR_LIST_LIMIT)\n : (opts.defaultLimit ?? DEFAULT_LIST_LIMIT);\n}\n\n/**\n * @internal\n */\nexport interface FetchCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n collection?: CollectionConfig<M>;\n filter?: FilterValues<Extract<keyof M, string>>,\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * The REST layer parsed `?or=` into this and then had nowhere to put it, so\n * the group was dropped and the read ran unfiltered — returning every row\n * the caller's policies allowed rather than the ones they asked for.\n */\n logical?: LogicalCondition;\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n /**\n * The sort, in either of two spellings:\n *\n * - a field name, whose direction is the separate `order` below — the\n * original single-column contract, which every existing driver reads;\n * - a list of `[field, direction]` tuples applied in order of significance,\n * which carries a multi-column sort and ignores `order` entirely.\n *\n * `normalizeDriverOrderBy` in `@rebasepro/common` collapses the pair to the\n * list form. A driver that has not been taught the list form should read it\n * through that helper rather than assume a string: handed an array, `String()`\n * would produce a field name like `roles,asc` and the sort would 400 (or,\n * with unknown-field warnings on, silently vanish).\n */\n orderBy?: string | OrderByTuple[];\n searchString?: string;\n /** Ask each row which declared search field matched — populates `_matches`. */\n searchExplain?: boolean;\n /** Direction for the string form of `orderBy`. Ignored when `orderBy` is a list. */\n order?: \"desc\" | \"asc\";\n /** Vector similarity search configuration */\n vectorSearch?: VectorSearchParams;\n /**\n * What to do about rows a soft delete has stamped.\n *\n * Unset (the default) hides them, which is the whole point of the feature:\n * a deleted row is deleted as far as the application is concerned. `true`\n * includes them alongside the live ones — a trash view, an admin audit.\n * `\"only\"` returns nothing but them, which is the trash view proper and is\n * not expressible as a filter, because the field is not part of the\n * caller's vocabulary.\n *\n * Ignored by collections that do not declare {@link\n * PostgresCollectionConfig.softDelete}: there is no stamp to look at, and\n * silently returning nothing for `\"only\"` on such a collection would be a\n * worse answer than ignoring it.\n */\n withDeleted?: boolean | \"only\";\n /**\n * Relations to load — see {@link IncludeSpec}.\n *\n * Absent means *no* relations, the same as it does over REST. It used to be\n * absent from this contract entirely, and the driver's own fetch then loaded\n * every relation of every row unconditionally: `find()` returned a row with\n * a foreign key and `listen()` returned the same row with a nested object\n * where that key was, for the same query.\n */\n include?: IncludeSpec;\n /** Columns to read, as a projection. See `FindParams.fields`. */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n}\n\n/**\n * @internal\n */\nexport type ListenCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchCollectionProps<M> &\n {\n /**\n * Page number (1-indexed), as `FindParams.page`.\n *\n * A subscription could name a `limit` and an `offset` but not a `page`,\n * so a live list on page three had to compute the offset itself — and\n * the two spellings then disagreed about what a page was.\n */\n page?: number;\n onUpdate: (rows: Record<string, unknown>[], meta?: CollectionUpdateMeta) => void;\n onError?: (error: Error) => void;\n };\n\n/**\n * @internal\n */\nexport interface SaveProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n values: Partial<EntityValues<M>>;\n id?: string | number; // can be empty for new entities\n previousValues?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n status: EntityStatus;\n /**\n * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key\n * instead of choosing between insert and update up front.\n *\n * One statement, so it does not lose the race a read-then-write can, and it\n * succeeds whether or not the row is already there — what a re-runnable\n * import needs. Requires every primary key column to be present; without\n * them there is no conflict target and the row is inserted normally.\n */\n upsert?: boolean;\n\n /**\n * The columns the upsert matches a conflict on, instead of the primary key.\n *\n * The key is the only target that always exists, and it is the wrong one\n * for the write an upsert is usually reached for: \"this user, identified by\n * their email, exists with these values\". Keyed on the primary key that is\n * an insert, because the caller does not know the serial id — so the row is\n * duplicated on every run.\n *\n * Only column sets carrying a uniqueness guarantee are legal here; Postgres\n * refuses anything else with 42P10, from inside a transaction. The REST\n * layer checks the target against the collection's declarations first (see\n * `resolveConflictTarget`), so the answer is a 400 naming the available\n * targets rather than a 500 naming a constraint the caller never wrote.\n */\n onConflict?: readonly string[];\n}\n\n/**\n * @internal\n */\nexport interface SaveManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to write. A row carrying its primary key updates (or, with\n * `upsert`, inserts-or-updates) that row; one without inserts.\n */\n rows: Partial<EntityValues<M>>[];\n collection?: CollectionConfig<M>;\n /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */\n upsert?: boolean;\n /** The conflict target for those upserts. See {@link SaveProps.onConflict}. */\n onConflict?: readonly string[];\n}\n\n/**\n * @internal\n */\nexport interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to update, each named by its address.\n *\n * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the\n * values and is insert-shaped — `saveMany` passes `status: \"new\"` and no\n * `id`, so it cannot express \"update exactly this row\". This can, and it is\n * why bulk update is a separate driver method rather than a flag on that one.\n */\n updates: { id: string | number; values: Partial<EntityValues<M>> }[];\n collection?: CollectionConfig<M>;\n}\n\n/**\n * @internal\n */\nexport interface DeleteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n row: { id: string | number; path: string; values?: Partial<EntityValues<M>> };\n collection?: CollectionConfig<M>;\n /**\n * Issue a real `DELETE` on a collection that declares\n * {@link PostgresCollectionConfig.softDelete}.\n *\n * The row and every cascade behind it go. It needs the same permission an\n * ordinary delete does and nothing more: it is the same verb, and a second\n * access-control surface for one operation is a second thing to get wrong.\n * No effect on a collection without soft delete, where every delete is\n * already this one.\n */\n hard?: boolean;\n}\n\n/**\n * @internal\n */\nexport interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n ids: (string | number)[];\n collection?: CollectionConfig<M>;\n /** See {@link DeleteProps.hard}. */\n hard?: boolean;\n}\n\n/**\n * One operation of a {@link DataDriver.batchWrite}.\n *\n * `path` rather than a slug, because a batch entry addresses rows exactly as\n * the single-row props do and a nested path is a legal address there.\n *\n * @internal\n */\nexport interface BatchWriteOperation<M extends Record<string, unknown> = Record<string, unknown>> {\n op: \"create\" | \"update\" | \"upsert\" | \"delete\";\n path: string;\n /** Required for `update` and `delete`. May be a `$ref` marker; see `batchWrite`. */\n id?: unknown;\n values?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n /** See {@link SaveProps.onConflict}. `upsert` only. */\n onConflict?: readonly string[];\n /** Names this operation's result, for a later `$ref`. */\n ref?: string;\n}\n\n/**\n * @internal\n */\nexport interface BatchWriteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n operations: BatchWriteOperation<M>[];\n}\n\nexport type FilterCombinationValidProps = {\n path: string;\n databaseId?: string;\n collection: CollectionConfig;\n filterValues: FilterValues<string>;\n sortBy?: [string, \"asc\" | \"desc\"];\n};\n\n/**\n * The integration SPI for plugging a data backend into Rebase.\n *\n * Implement this interface to connect a custom backend (or use a built-in\n * driver such as the Firestore one) and register it on\n * `<Rebase dataSources>`. Rebase wraps drivers via `buildRebaseData` and\n * routes collections to them by their `dataSource` key.\n *\n * For *consuming* data in application code, use `RebaseData` /\n * `context.data` instead — this interface is only for providing it.\n *\n * @group Datasource\n */\nexport interface DataDriver {\n\n /**\n * Key that identifies this driver\n */\n key?: string;\n\n /**\n * If the driver has been initialised\n */\n initialised?: boolean;\n\n /**\n * Fetch data from a collection\n * @param props\n * @return Promise of flat rows\n */\n fetchCollection<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Listen to a collection in a given path. If you don't implement this method\n * `fetchCollection` will be used instead, with no real time updates.\n * @param props\n * @return Function to cancel subscription\n */\n listenCollection?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenCollectionProps<M>): () => void;\n\n /**\n * Retrieve a single row given a path and a collection\n * @param props\n */\n fetchOne<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Get realtime updates on one row.\n * @param props\n * @return Function to cancel subscription\n */\n listenOne?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenOneProps<M>): () => void;\n\n /**\n * Save a row to the specified path\n * @param props\n */\n save<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;\n\n /**\n * Save many rows as one unit of work.\n *\n * Every row runs the same pipeline as {@link save} — callbacks, relations\n * and row-level security all still apply — but they share a single\n * transaction, so the batch either lands whole or not at all. That, and the\n * single round trip, is what makes importing tens of thousands of rows\n * viable without dropping to raw SQL.\n *\n * Optional: drivers that cannot do this leave it undefined and callers fall\n * back to `save` per row.\n */\n saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Update many rows in one transaction, each addressed by id.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make the\n * batch atomic should not pretend to. The REST layer reports\n * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single\n * writes, which would be neither atomic nor one round trip — the two things\n * a caller reaches for a batch to get.\n */\n updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Delete the row `props.row` addresses.\n *\n * **Resolving means the row is gone because this call removed it.** A\n * delete that matched nothing must reject with a not-found error\n * (`ApiError.notFound`, `statusCode: 404`) rather than resolving quietly.\n *\n * The rule is here rather than in each driver because the two\n * implementations answered differently and each had a test pinning its own\n * habit: Postgres threw, Mongo logged a warning and resolved. Three things\n * decide it in favour of rejecting.\n *\n * The REST layer already says 404 — `DELETE /api/data/<c>/<id>` reads the\n * row before removing it — so a quiet resolve made the driver API disagree\n * with the HTTP API about the same operation, and only in-process\n * `rebase.data` callers could see the difference.\n *\n * A caller cannot tell \"deleted\" from \"there was nothing there\" without it,\n * and those are different facts: one means the caller's model of the data\n * was right, the other that it was stale. Silence hands back the wrong one\n * and the caller carries on.\n *\n * And on a driver with row-level security, \"matched nothing\" is *also* how\n * a policy refusal arrives — Postgres filters `DELETE` through `USING`\n * rather than raising. A driver that resolves on zero rows therefore\n * reports a refused delete as a completed one, which is the defect\n * `explainZeroRowWrite` exists to prevent (see `write-denial.ts`).\n *\n * Conformance for both server drivers lives in\n * `packages/server/test/contract/delete-contract.ts`, run by each driver's\n * own suite against its own database. `packages/firebase`'s Firestore\n * driver does not honour it: `deleteDoc` resolves for a missing document\n * and reporting otherwise would cost a read on every delete. It runs in the\n * browser against Firestore's own semantics rather than behind\n * `rebase.data`, and that exception is stated here rather than left to be\n * discovered.\n */\n delete<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;\n\n /**\n * Delete all entities from a collection.\n * @param path Collection path\n */\n deleteAll?(path: string): Promise<void>;\n\n /**\n * Delete many rows in one transaction, addressed by id.\n *\n * Ids rather than a filter, deliberately — see\n * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.\n */\n deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;\n\n /**\n * Apply a mixed list of writes across collections as one unit of work.\n *\n * The capability `saveMany` and `deleteMany` cannot express between them: a\n * batch that touches two tables. Sent as two requests those can\n * half-succeed, and the recovery — read back, work out which half landed,\n * undo it — is code nobody writes.\n *\n * Every operation runs the pipeline its single-row equivalent runs, in\n * order, in one transaction, under the caller's own role. Operations may\n * carry `{ \"$ref\": \"<name>.<field>\" }` markers in `values` or `id`, which\n * the driver resolves against the rows earlier operations wrote — the\n * driver, because inside the transaction is the only place those rows\n * exist. `@rebasepro/server` exports `resolveBatchRefs` so the resolution\n * is one implementation rather than one per driver.\n *\n * Resolves to one entry per operation, aligned to the input: the written\n * row for a create, update or upsert, and `null` for a delete.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make it\n * atomic must not pretend to. The REST layer answers `BATCH_UNSUPPORTED`\n * rather than falling back to a loop, which would be the non-atomic\n * sequence the caller reached for this to avoid.\n */\n batchWrite?<M extends Record<string, unknown> = Record<string, unknown>>(\n props: BatchWriteProps<M>\n ): Promise<(Record<string, unknown> | null)[]>;\n\n /**\n * Check if the given property is unique in the given collection\n * @param path Collection path\n * @param name of the property\n * @param value\n * @param id\n * @param collection\n * @return `true` if there are no other fields besides the given entity\n */\n checkUniqueField(\n path: string,\n name: string,\n value: unknown,\n id?: string | number,\n collection?: CollectionConfig\n ): Promise<boolean>;\n\n /**\n * Count the number of entities in a collection\n */\n count?<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number>;\n\n /**\n * Check if the given filter combination is valid\n * @param props\n */\n isFilterCombinationValid?(props: Omit<FilterCombinationValidProps, \"collection\"> & {\n databaseId?: string\n }): boolean;\n\n /**\n * Get the object to generate the current time in the driver\n */\n currentTime?: () => unknown;\n\n delegateToCMSModel?: (data: unknown) => unknown;\n\n cmsToDelegateModel?: (data: unknown) => unknown;\n\n initTextSearch?: (props: {\n context: RebaseCallContext,\n path: string,\n databaseId?: string,\n collection: CollectionConfig,\n parentCollectionSlugs?: string[];\n parentEntityIds?: string[];\n }) => Promise<boolean>;\n\n /**\n * Flag to indicate if the driver has requested the initialization of the text search index\n */\n needsInitTextSearch?: boolean;\n\n // ── REST fetch capabilities ─────────────────────────────────────────\n\n /**\n * Optional REST-optimised fetch service. When present, the REST API\n * generator uses these methods instead of the generic `fetchOne` /\n * `fetchCollection` pipeline, enabling include-aware eager-loading.\n */\n restFetchService?: RestFetchService;\n\n // ── Admin capabilities ─────────────────────────────────────────────\n //\n // Admin operations are now modelled as capability-specific interfaces\n // (SQLAdmin, DocumentAdmin, SchemaAdmin) in `@rebasepro/types/backend`.\n //\n // Drivers that support admin features should expose them here.\n // Consumers should use the `isSQLAdmin()`, `isSchemaAdmin()` etc.\n // type guards to safely narrow the type before calling methods.\n\n /**\n * Return the admin capabilities of this driver.\n * @see SQLAdmin\n * @see DocumentAdmin\n * @see SchemaAdmin\n */\n admin?: import(\"../types/backend\").DatabaseAdmin;\n\n}\n\n/**\n * REST-optimised fetch service exposed by drivers that support\n * eager-loading of relations via `include`.\n *\n * The methods return flattened rows — exactly the table's columns, under their\n * own names and with the types the database returned — and included relations\n * inlined as plain nested rows. This is the shape served to app developers\n * through the REST API / SDK client.\n *\n * No synthesized `id`: identity is a primary key, which may be named anything\n * and span several columns, so an address is derived by whoever needs one (see\n * `buildCompositeId`) rather than written into the row on top of the data.\n *\n * @group DataDriver\n */\nexport interface RestFetchService {\n /**\n * Fetch a collection of flattened entities with optional relation includes.\n */\n fetchCollectionForRest(\n collectionPath: string,\n options?: {\n filter?: FilterValues<string>;\n /** An `or(...)`/`and(...)` group, applied alongside `filter`. */\n logical?: LogicalCondition;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n searchString?: string;\n /** Ask each row which declared search fields matched — populates `_matches`. */\n searchExplain?: boolean;\n databaseId?: string;\n vectorSearch?: VectorSearchParams;\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n /**\n * Columns to read. A projection pushed into the SELECT, not a trim\n * of the response — `excludeFromApi` still applies on top, and the\n * primary key is always read whether or not it is named.\n */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n },\n include?: IncludeSpec\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * The opaque cursor that continues a listing after `row`.\n *\n * On the driver rather than the route because deriving it needs the\n * collection's primary key — which may be named anything and span several\n * columns — and that is the driver's knowledge. The route holds the last\n * row and the sort keys and asks for the string.\n *\n * `undefined` where no cursor can describe the page: an ordering with no\n * stored value to compare against (relevance), or a row missing a value for\n * one of the sort keys. The listing then reports no `nextCursor` and the\n * caller pages by offset, which is what it did before cursors existed.\n *\n * Optional: a driver that cannot seek simply never issues one, and\n * `meta.nextCursor` is absent for every read it serves.\n */\n cursorFor?(\n collectionPath: string,\n row: Record<string, unknown>,\n orderBy?: OrderByTuple[]\n ): string | undefined;\n\n /**\n * `count`/`sum`/`avg`/`min`/`max` over the rows a filter selects,\n * optionally grouped.\n *\n * Optional, and the REST route answers 501 where a driver does not\n * implement it — an aggregate is not a thing to approximate, and an empty\n * result set would read as \"nothing matched\".\n *\n * Any implementation **must apply the same row-level authorization as a\n * read**. An aggregate is an efficient way to learn about rows you cannot\n * select, and `count(*)` over a table whose policies would return nothing\n * has to be zero.\n */\n aggregate?(\n collectionPath: string,\n options: {\n aggregates: { fn: \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"; field?: string; alias: string }[];\n groupBy?: string[];\n filter?: FilterValues<string>;\n logical?: LogicalCondition;\n searchString?: string;\n limit?: number;\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch a single flattened entity with optional relation includes.\n */\n fetchOneForRest(\n collectionPath: string,\n id: string | number,\n include?: IncludeSpec,\n databaseId?: string,\n options?: {\n /** See `FetchCollectionProps.fields`. */\n fields?: string[];\n /** See {@link FetchOneProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown> | null>;\n}\n","/**\n * Path prefix that marks an object as **public**. Files stored under this\n * prefix are served without any auth token via a stable, permanent,\n * CDN-cacheable URL (see {@link StorageSource.getSignedUrl}). Shared by the\n * client SDK and the backend so both agree on which objects are public.\n *\n * @group Models\n */\nexport const PUBLIC_STORAGE_PREFIX = \"public/\";\n\n/**\n * True when a storage key/path points at a public object (lives under\n * {@link PUBLIC_STORAGE_PREFIX}). The check is applied to the key *within the\n * bucket* — strip any `bucket/` and `scheme://` prefixes first.\n *\n * @group Models\n */\nexport function isPublicStoragePath(path: string | null | undefined): boolean {\n if (!path) return false;\n let p = path;\n const scheme = p.indexOf(\"://\");\n if (scheme !== -1) p = p.substring(scheme + 3);\n p = p.replace(/^\\/+/, \"\");\n\n // Defense-in-depth: a path containing traversal segments is never public,\n // so an attacker can't reach a private object via `public/../secret`.\n if (p.split(\"/\").some((seg) => seg === \"..\")) return false;\n\n // Public iff the object **key** starts with the public prefix. A single\n // leading `default/` bucket segment is tolerated (the default bucket).\n // A substring match is deliberately NOT used — a private object under a\n // folder literally named `public` (e.g. `reports/public/q3.pdf`) must stay\n // private. Named buckets: pass the key (not `bucket/key`) so the prefix is\n // anchored; otherwise it falls back to a private, token-scoped URL (safe).\n return p.startsWith(PUBLIC_STORAGE_PREFIX) || p.startsWith(`default/${PUBLIC_STORAGE_PREFIX}`);\n}\n\n/**\n * @group Models\n */\nexport interface UploadFileProps {\n file: File,\n key: string,\n metadata?: Record<string, unknown>,\n bucket?: string,\n /**\n * Store this object as **public**: it is placed under\n * {@link PUBLIC_STORAGE_PREFIX} and served via a stable, token-less,\n * permanent URL (safe to persist in a database and cache on a CDN).\n * Defaults to `false` (private, short-lived signed URLs).\n */\n public?: boolean,\n /**\n * Which property this file is being uploaded *for* — the collection's slug\n * and the property path within it (`coverImage`, `meta.avatar`,\n * `gallery` for an array of files).\n *\n * The server reads it to enforce that property's own `storage.maxSize` and\n * `storage.acceptedFiles`, which were declared per property, published in\n * the generated types, rendered by the panel's file picker, and until now\n * enforced by nothing on the server — so a `curl` past the picker put a\n * 40 MB executable in a bucket whose config said \"images, under 200 KB\".\n *\n * Advisory in one direction only. The rules are resolved from the server's\n * own registry by slug, so naming a property can make an upload *stricter*\n * or leave it at the global cap; it can never widen anything.\n *\n * Omitted, the upload is checked against the deployment's global\n * `maxFileSize` exactly as before.\n */\n context?: UploadPropertyContext\n}\n\n/**\n * The property an upload is destined for.\n *\n * @group Models\n */\nexport interface UploadPropertyContext {\n /** The collection's slug, as the server registered it. */\n collection: string;\n /** Dotted path to the property — `coverImage`, `meta.avatar`. */\n property: string;\n}\n\n/**\n * @group Models\n */\nexport interface UploadFileResult {\n /**\n * Storage key including the file name where the file was uploaded.\n */\n key: string;\n /**\n * Bucket where the file was uploaded\n */\n bucket: string;\n\n /**\n * Fully qualified storage URL for the uploaded file.\n *\n * For example: `s3://my-bucket/path/to/file.png`. Every controller in the\n * framework returns one — S3, GCS and local alike — and a caller that stores\n * the reference needs it, so it is part of the result rather than a maybe.\n */\n storageUrl: string;\n}\n\n/**\n * @group Models\n */\nexport interface DownloadConfig {\n /**\n * Temporal url that can be used to download the file\n */\n url: string | null;\n\n metadata?: DownloadMetadata;\n\n fileNotFound?: boolean;\n}\n\n/**\n * The full set of object metadata, including read-only properties.\n * @public\n */\nexport declare interface DownloadMetadata {\n /**\n * The bucket this object is contained in.\n */\n bucket: string;\n /**\n * The full path of this object.\n */\n fullPath: string;\n /**\n * The short name of this object, which is the last component of the full path.\n * For example, if path is 'full/path/image.png', name is 'image.png'.\n */\n name: string;\n /**\n * The size of this object, in bytes.\n */\n size: number;\n /**\n * Type of the uploaded file\n * e.g. \"image/jpeg\"\n */\n contentType: string;\n\n customMetadata: Record<string, unknown>;\n /**\n * Optional short-lived download token (for local/server-mediated storage).\n * Absent for public objects, which need no token.\n */\n token?: string;\n /**\n * Optional remaining lifetime of the token, in seconds.\n */\n tokenExpiresIn?: number;\n /**\n * True when this object is public: it is served without a token via a\n * stable, permanent, CDN-cacheable URL. When set, the client builds a\n * token-less URL and caches it indefinitely.\n */\n public?: boolean;\n}\n\n/**\n * @group Models\n */\nexport interface StorageSource {\n /**\n * Upload an object, specifying a key\n * @param file\n * @param key\n * @param metadata\n * @param bucket\n */\n putObject: ({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps) => Promise<UploadFileResult>;\n\n /**\n * Convert a storage key or URL into a download configuration (signed URL equivalent)\n * @param keyOrUrl\n * @param bucket\n */\n getSignedUrl: (keyOrUrl: string, bucket?: string) => Promise<DownloadConfig>;\n\n /**\n * Get an object from a storage key.\n * It returns null if the object does not exist.\n * @param key\n * @param bucket\n */\n getObject: (key: string, bucket?: string) => Promise<File | null>;\n\n /**\n * Delete an object.\n * @param key\n * @param bucket\n */\n deleteObject: (key: string, bucket?: string) => Promise<void>;\n\n /**\n * List the contents of a prefix.\n * @param prefix\n * @param options\n */\n listObjects: (prefix: string, options?: {\n bucket?: string,\n maxResults?: number,\n pageToken?: string\n }) => Promise<StorageListResult>;\n\n}\n\n/**\n * Result returned by list().\n * @public\n */\nexport declare interface StorageListResult {\n /**\n * References to prefixes (sub-folders). You can call list() on them to\n * get its contents.\n *\n * Folders are implicit based on '/' in the object paths.\n * For example, if a bucket has two objects '/a/b/1' and '/a/b/2', list('/a')\n * will return '/a/b' as a prefix.\n */\n prefixes: StorageReference[];\n /**\n * Objects in this directory.\n * You can call getMetadata() and getDownloadUrl() on them.\n */\n items: StorageReference[];\n /**\n * If set, there might be more results for this list. Use this token to resume the list.\n */\n nextPageToken?: string;\n}\n\n/**\n * Represents a reference to an S3-compatible storage object. Developers can\n * upload, download, and delete objects, as well as get/set object metadata.\n * @public\n */\nexport declare interface StorageReference {\n /**\n * Returns a s3:// URL for this object in the form\n * `s3://<bucket>/<path>/<to>/<object>`\n * @returns The s3:// URL.\n */\n toString(): string;\n\n /**\n * A reference to the root of this object's bucket.\n */\n root: StorageReference;\n /**\n * The name of the bucket containing this reference's object.\n */\n bucket: string;\n /**\n * The full path of this object.\n */\n fullPath: string;\n /**\n * The short name of this object, which is the last component of the full path.\n * For example, if path is 'full/path/image.png', name is 'image.png'.\n */\n name: string;\n\n /**\n * A reference pointing to the parent location of this reference, or null if\n * this reference is the root.\n */\n parent: StorageReference | null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/errors.ts","../src/types/entities.ts","../src/types/filter-operators.ts","../src/types/admin_block.ts","../src/types/data_source.ts","../src/types/collections.ts","../src/types/search.ts","../src/types/relations.ts","../src/types/policy.ts","../src/types/rls-functions.ts","../src/types/tenancy.ts","../src/types/backend.ts","../src/types/schema_editing.ts","../src/types/channel_bus.ts","../src/types/resources.ts","../src/types/storage_source.ts","../src/types/resource_kinds.ts","../src/types/component_ref.ts","../src/types/project_manifest.ts","../src/types/collection_contract.ts","../src/types/schema_version.ts","../src/controllers/data.ts","../src/controllers/data_driver.ts","../src/controllers/storage.ts"],"sourcesContent":["/**\n * The error codes every route can produce, as `RebaseApiError.code`.\n *\n * These are the defaults on `ApiError`'s static constructors server-side, so\n * any endpoint can answer with one. They are **not** the complete set: routes\n * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,\n * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.\n *\n * Hence the union is deliberately open rather than closed. It exists to give\n * autocomplete and to catch a typo in the common cases — `code` was a bare\n * `string`, so `e.code === \"NOT_FOUND\"` and `e.code === \"NOTFOUND\"` were\n * equally valid and only one of them worked. Closing it would be a lie that\n * broke the moment a route added a code.\n *\n * @example\n * if (e instanceof RebaseApiError) {\n * switch (e.code) {\n * case \"NOT_FOUND\": return null; // completed\n * case \"FORBIDDEN\": return redirect();\n * default: throw e; // routes' own codes land here\n * }\n * }\n *\n * @group Errors\n */\nexport type RebaseErrorCode =\n | \"BAD_REQUEST\"\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"INTERNAL_ERROR\"\n | \"SERVICE_UNAVAILABLE\"\n | \"NETWORK_ERROR\"\n | \"OFFLINE\"\n | \"DB_PERMISSION_DENIED\"\n | \"SCHEMA_DRIFT\"\n // `string & {}` keeps the union open while preserving completion on the\n // literals above — a bare `| string` would collapse them and offer nothing.\n | (string & {});\n\n/**\n * Structured initializer for {@link RebaseApiError}.\n *\n * @group Errors\n */\nexport interface RebaseErrorInit {\n /**\n * HTTP status code, when the error originated from an HTTP response.\n *\n * Three states, and they mean different things:\n *\n * - a real status — the server answered, and this is what it said;\n * - **`0`** — the request never reached a server: DNS, a refused\n * connection, CORS, an abort. `XMLHttpRequest` has always spelled that\n * `0`, and a fabricated 5xx would be indistinguishable from one the\n * server actually sent. The original failure is on `cause`;\n * - `undefined` — nothing was sent at all: a realtime/WebSocket failure,\n * or a client-side logic error raised before any request.\n */\n status?: number;\n /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */\n code?: RebaseErrorCode;\n /** Structured error payload returned by the server, when present. */\n details?: unknown;\n /**\n * The server's correlation id for the request that failed, when it sent\n * one — the `requestId` in the error envelope, which also comes back on the\n * `X-Request-ID` header.\n *\n * The envelope has carried it for a while; the client dropped it on the\n * floor, so a bug report from an app could never quote the one string that\n * finds the server-side line.\n */\n requestId?: string;\n /**\n * Seconds to wait before retrying, from the response's `Retry-After`\n * header. Present on a 429 and on some 503s.\n *\n * Also dropped. The offline queue's own backoff therefore ignored a server\n * that had said exactly how long to wait — the one number that turns a\n * retry storm into a queue that drains.\n */\n retryAfterSeconds?: number;\n /** The underlying error this one wraps, if any. */\n cause?: unknown;\n}\n\n/**\n * The single error type thrown across the entire Rebase client surface —\n * HTTP data/control-plane calls, realtime/WebSocket operations, and\n * client-side logic errors (e.g. an unknown collection accessor). A `catch`\n * block only ever needs to check for this one class:\n *\n * ```ts\n * import { RebaseApiError } from \"@rebasepro/client\"; // re-exported\n *\n * try {\n * await client.data.products.update(id, { price: 9 });\n * } catch (e) {\n * if (e instanceof RebaseApiError) {\n * if (e.status === 404) { ... } // HTTP failures carry a status\n * console.error(e.code, e.details);\n * }\n * }\n * ```\n *\n * `status` is present for HTTP failures and `undefined` otherwise, so its\n * presence distinguishes transport-level errors from realtime/logic errors.\n *\n * @group Errors\n */\nexport class RebaseApiError extends Error {\n /** HTTP status code, or `undefined` for non-HTTP errors. */\n readonly status?: number;\n /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */\n readonly code?: RebaseErrorCode;\n /** Structured error payload from the server, when present. */\n readonly details?: unknown;\n /** See {@link RebaseErrorInit.requestId}. Quote it in a bug report. */\n readonly requestId?: string;\n /** See {@link RebaseErrorInit.retryAfterSeconds}. */\n readonly retryAfterSeconds?: number;\n\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message);\n this.name = \"RebaseApiError\";\n this.status = init.status;\n this.code = init.code;\n this.details = init.details;\n this.requestId = init.requestId;\n this.retryAfterSeconds = init.retryAfterSeconds;\n if (init.cause !== undefined) {\n // `cause` is standard on Error but not always in the lib target's type.\n (this as { cause?: unknown }).cause = init.cause;\n }\n }\n}\n\n/**\n * Client-side logic error — raised before any request is made (e.g. accessing\n * an unknown collection accessor when a typed dictionary is configured).\n *\n * A subclass of {@link RebaseApiError} (with no `status`), so a single\n * `catch (e) { if (e instanceof RebaseApiError) ... }` handles it too.\n *\n * @group Errors\n */\nexport class RebaseClientError extends RebaseApiError {\n /**\n * `init` is the same one {@link RebaseApiError} takes, and it is what makes\n * `code` reachable at all.\n *\n * The constructor used to accept a message and nothing else, so every\n * client-side failure — an undefined filter value, an unknown accessor,\n * `listen()` on a client built with `realtime: false`, a function name with\n * a `/` in it, `refreshSession()` while signed out — arrived with `code ===\n * undefined`. The documented `switch (e.code)` in this file's own example\n * fell to `default: throw e` for all of them, and the only client-side error\n * that *did* carry a code was `OFFLINE`, because that one path minted a\n * `RebaseApiError` instead.\n */\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message, init);\n this.name = \"RebaseClientError\";\n }\n}\n\n/**\n * Brand for a contract method a particular client cannot serve.\n *\n * `Symbol.for` rather than a fresh symbol: two copies of `@rebasepro/types` in\n * one tree — which happens, see `docs/dependency-duplication-traps.md` — must\n * agree about it, and a module-local symbol would not.\n */\nconst UNSUPPORTED_METHOD = Symbol.for(\"rebase.unsupportedMethod\");\n\n/**\n * Build the stub a client installs for a contract method it cannot serve.\n *\n * `listen`, `listenById` and `count` are part of `SDKCollectionClient`, not\n * optional extras — a caller should be able to write\n * `client.data.posts.count()` without asking first, and a transport that cannot\n * serve it should answer with a sentence naming the configuration that would,\n * rather than with `undefined is not a function` at the call site. Where the\n * transport genuinely cannot (a client built with `realtime: false`, a driver\n * with no `listenCollection`), it installs one of these instead of omitting the\n * method.\n *\n * @param message What to tell the caller, naming the fix.\n * @group Errors\n */\n/**\n * The tag {@link unsupportedMethod} puts on the stub it returns.\n *\n * Written as an optional member on a callable, which is what a tagged stub\n * actually is — so both the write and the read below are single casts and tsc\n * checks the symbol and the value type on each. Reading it as\n * `Record<symbol, boolean>` made every symbol key on the object a `boolean`\n * (which is not true of a function, and is why that needed `as unknown as` to\n * be written at all), and the write and the read were then free to disagree\n * about the tag.\n */\ntype UnsupportedTagged = ((...args: never[]) => unknown) & { [UNSUPPORTED_METHOD]?: boolean };\n\nexport function unsupportedMethod<F>(message: string): F {\n const stub = (): never => {\n // The two reasons a method is a stub — `realtime: false`, and a driver\n // with no `listenCollection` — are one thing to a caller: this client\n // cannot do realtime. One code covers both, and the message says which.\n throw new RebaseClientError(message, { code: \"REALTIME_DISABLED\" });\n };\n (stub as UnsupportedTagged)[UNSUPPORTED_METHOD] = true;\n return stub as F;\n}\n\n/**\n * Can this method actually do anything?\n *\n * `true` for a stub from {@link unsupportedMethod} **and** for a method that is\n * simply not there — a partial client, a hand-built test double, an\n * implementation written against an older shape of the interface. Both mean the\n * same thing to a caller, so both answer the same way, and an adapter that\n * checks this cannot be caught out by either.\n *\n * Ordinary code does not need it: calling the method and letting it throw is\n * the normal path. Adapters do — the admin panel chooses between subscribing\n * and a one-shot `find()` by asking whether the client can listen, and a UI\n * that subscribes into a throw is worse than one that polls. This is the\n * question `if (accessor.listen)` used to be asking, made explicit now that the\n * method is always there to call.\n *\n * @group Errors\n */\nexport function isUnsupported(method: unknown): boolean {\n if (typeof method !== \"function\") return true;\n return (method as UnsupportedTagged)[UNSUPPORTED_METHOD] === true;\n}\n","import type { SearchMatch } from \"./search\";\n/**\n * New or existing status\n * @group Models\n */\nexport type EntityStatus = \"new\" | \"existing\" | \"copy\";\n\n/**\n * Representation of a entity fetched from the driver\n * @group Models\n */\nexport interface Entity<M extends Record<string, unknown> = Record<string, unknown>> {\n\n /**\n * ID of the entity\n */\n id: string | number;\n\n /**\n * A string representing the path of the referenced document (relative\n * to the root of the database).\n */\n path: string;\n\n /**\n * Current values\n */\n values: EntityValues<M>;\n\n /**\n * Why this entity is in a search result: which declared fields matched, and\n * the text around each hit.\n *\n * Present only on rows returned by a search that asked for it. A sibling of\n * `values` rather than a key inside it, because it describes the *query*,\n * not the record — nothing in the collection declares it, no form edits it,\n * and a record fetched by id never has one.\n */\n searchMatches?: SearchMatch[];\n\n /**\n * Which driver this entity belongs to (e.g., 'postgres', 'firestore').\n * If not specified, the default driver is assumed.\n */\n driver?: string;\n\n /**\n * Which database within the driver (e.g., for Firestore multi-database).\n * If not specified, the default database of the driver is used.\n */\n databaseId?: string;\n}\n\n/**\n * This type represents a record of key value pairs as described in an\n * entity collection.\n * @group Models\n */\nexport type EntityValues<M extends Record<string, unknown>> = M;\n\n/**\n * Props for creating a EntityReference\n */\nexport interface EntityReferenceProps {\n /** ID of the entity */\n id: string;\n /** Path of the collection (relative to the root of the database) */\n path: string;\n /** Which driver (e.g., 'postgres', 'firestore'). Defaults to \"(default)\" */\n driver?: string;\n /** Which database within the driver. Defaults to \"(default)\" */\n databaseId?: string;\n}\n\n/**\n * Class used to create a reference to a entity in a different path.\n *\n * @example\n * // Simple reference (most common case - single driver, single db)\n * new EntityReference({ id: \"123\", path: \"users\" })\n *\n * // Reference to a different driver (e.g., Firestore)\n * new EntityReference({ id: \"123\", path: \"analytics\", driver: \"firestore\" })\n *\n * // Reference to a specific database within a driver\n * new EntityReference({ id: \"123\", path: \"orders\", driver: \"postgres\", databaseId: \"orders_db\" })\n */\nexport class EntityReference {\n\n readonly __type = \"reference\";\n /**\n * ID of the entity\n */\n readonly id: string;\n /**\n * A string representing the path of the referenced document (relative\n * to the root of the database).\n */\n readonly path: string;\n\n /**\n * Which driver (e.g., 'postgres', 'firestore').\n * Defaults to \"(default)\" if not specified.\n */\n readonly driver?: string;\n\n /**\n * Which database within the driver.\n * Defaults to \"(default)\" if not specified.\n */\n readonly databaseId?: string;\n\n /**\n * Create a reference to a entity.\n *\n * @example\n * // Simple reference (most common case)\n * new EntityReference({ id: \"123\", path: \"users\" })\n *\n * // With driver\n * new EntityReference({ id: \"123\", path: \"analytics\", driver: \"firestore\" })\n */\n constructor(props: EntityReferenceProps) {\n this.id = props.id;\n this.path = props.path;\n this.driver = props.driver;\n this.databaseId = props.databaseId;\n }\n\n get pathWithId() {\n return `${this.path}/${this.id}`;\n }\n\n /**\n * Get the full path including driver and database prefixes if specified.\n * For the common case (single driver, single db), this just returns pathWithId.\n */\n get fullPath() {\n const parts: string[] = [];\n\n // Add driver prefix if not default\n if (this.driver && this.driver !== \"(default)\") {\n parts.push(this.driver);\n }\n\n // Add database prefix if specified\n if (this.databaseId && this.databaseId !== \"(default)\") {\n parts.push(this.databaseId);\n }\n\n if (parts.length > 0) {\n return `${parts.join(\":\")}:::${this.path}/${this.id}`;\n }\n return this.pathWithId;\n }\n\n isEntityReference() {\n return true;\n }\n}\n\n/**\n * Class used to create a reference to a entity in a different path\n */\nexport class EntityRelation {\n\n readonly __type = \"relation\";\n /**\n * ID of the entity\n */\n readonly id: string | number;\n /**\n * A string representing the path of the referenced document (relative\n * to the root of the database).\n */\n readonly path: string;\n\n /**\n * Pre-fetched data payload to eliminate N+1 queries.\n * When present, clients can use this directly instead of fetching.\n */\n readonly data?: Record<string, unknown>;\n\n constructor(id: string | number, path: string, data?: Record<string, unknown>) {\n this.id = id;\n this.path = path;\n this.data = data;\n }\n\n get pathWithId() {\n return `${this.path}/${this.id}`;\n }\n\n isEntityReference() {\n return false;\n }\n\n isEntityRelation() {\n return true;\n }\n}\n\nexport class GeoPoint {\n\n /**\n * The latitude of this GeoPoint instance.\n */\n readonly latitude: number;\n /**\n * The longitude of this GeoPoint instance.\n */\n readonly longitude: number;\n\n constructor(latitude: number, longitude: number) {\n this.latitude = latitude;\n this.longitude = longitude;\n }\n}\n\nexport class Vector {\n readonly value: number[];\n\n constructor(value: number[]) {\n this.value = value;\n }\n}\n","/**\n * Canonical filter operators and REST wire-format mappings.\n *\n * `WhereFilterOp` is THE operator type used at every layer — from React\n * components through the SDK, server, and down to the database driver.\n *\n * PostgREST short-codes (`eq`, `gt`, `cs`, …) exist **only** at the\n * HTTP wire boundary, handled by `serializeFilter` / `deserializeFilter`\n * in `@rebasepro/common`.\n *\n * ┌──────────────────────┬───────────────┬──────────────────────────────┐\n * │ Canonical │ REST short │ Meaning │\n * ├──────────────────────┼───────────────┼──────────────────────────────┤\n * │ \"==\" │ \"eq\" │ Equal │\n * │ \"!=\" │ \"neq\" │ Not equal │\n * │ \">\" │ \"gt\" │ Greater than │\n * │ \">=\" │ \"gte\" │ Greater than or equal │\n * │ \"<\" │ \"lt\" │ Less than │\n * │ \"<=\" │ \"lte\" │ Less than or equal │\n * │ \"in\" │ \"in\" │ Value in list │\n * │ \"not-in\" │ \"nin\" │ Value not in list │\n * │ \"array-contains\" │ \"cs\" │ Array contains element │\n * │ \"array-contains-any\" │ \"csa\" │ Array contains any of │\n * │ \"like\" │ \"like\" │ SQL LIKE (case-sensitive) │\n * │ \"ilike\" │ \"ilike\" │ SQL ILIKE (case-insensitive) │\n * │ \"not-like\" │ \"nlike\" │ NOT LIKE (case-sensitive) │\n * │ \"not-ilike\" │ \"nilike\" │ NOT ILIKE (case-insensitive) │\n * │ \"is-null\" │ \"isnull\" │ Field IS NULL │\n * │ \"is-not-null\" │ \"notnull\" │ Field IS NOT NULL │\n * └──────────────────────┴───────────────┴──────────────────────────────┘\n *\n * Pattern matching (`like`/`ilike`) uses SQL wildcard syntax: `%` matches any\n * sequence of characters, `_` matches a single character. On MongoDB these are\n * translated to anchored regular expressions; Firestore has no native pattern\n * matching and rejects these operators (use `searchString` instead).\n *\n * @module\n */\n\n/**\n * Where NULLs sort relative to real values on one key.\n *\n * Absent means the convention Postgres itself applies and the driver writes\n * out: `NULLS LAST` ascending, `NULLS FIRST` descending. That convention was\n * hardcoded and unstateable — a \"newest first\" list put every row with no date\n * at the very top, and the only way out was to add a `is-not-null` filter and\n * lose those rows entirely.\n *\n * The keyset comparison honours whatever is chosen here, so a cursor over a\n * nullable key stays correct under either placement.\n *\n * @group Models\n */\nexport type NullsPlacement = \"first\" | \"last\";\n\n/**\n * Canonical sort representation: `[fieldName, direction]`, optionally with a\n * {@link NullsPlacement}.\n *\n * Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.\n * The colon-string form (`\"field:direction\"`, or `\"field:direction:nulls\"`)\n * exists only at the HTTP wire boundary, handled by `serializeOrderBy` /\n * `deserializeOrderBy` in `@rebasepro/common`.\n *\n * The third slot is optional so every `[field, direction]` written before it\n * existed is still exactly this type, and every `const [field, direction] =`\n * destructure still reads what it always read.\n *\n * @group Models\n */\nexport type OrderByTuple<Key extends string = string> = [Key, \"asc\" | \"desc\", NullsPlacement?];\n\n/**\n * One sort key, or several applied in order of significance.\n *\n * ```ts\n * orderBy: [\"created_at\", \"desc\"] // one key\n * orderBy: [[\"roles\", \"asc\"], [\"created_at\", \"desc\"]] // roles, then newest first\n * ```\n *\n * The two forms are told apart by whether the first element is itself an\n * array, so a single tuple never needs wrapping and every existing caller\n * keeps working unchanged. `normalizeOrderBy` in `@rebasepro/common` collapses\n * both to the list form, which is what every layer below the call site speaks.\n *\n * Ties on the last key are broken by the row id, so a multi-key sort is a\n * total order and pages over it neither repeat nor skip rows.\n *\n * @group Models\n */\nexport type OrderBySpec<Key extends string = string> =\n | OrderBySortTuple<Key>\n | OrderBySortTuple<Key>[];\n\n/**\n * A sort key: a field name, or an aggregate over a to-many relation.\n *\n * @group Models\n */\nexport type SortKey<Key extends string = string> = Key | RelationAggregateSort;\n\n/**\n * `[sortKey, direction]` — the authoring form of {@link OrderByTuple}, which\n * additionally accepts a {@link RelationAggregateSort} object.\n *\n * The object never reaches a driver: `normalizeOrderBy` in `@rebasepro/common`\n * encodes it to its string spelling on the way down, and everything below that\n * point speaks plain `OrderByTuple`. See {@link RelationAggregateSort} for why\n * the wire form is a string.\n *\n * @group Models\n */\nexport type OrderBySortTuple<Key extends string = string> = [SortKey<Key>, \"asc\" | \"desc\", NullsPlacement?];\n\n/**\n * The aggregate functions a relation sort can apply.\n *\n * Five, and no `array_agg`/`string_agg`: an aggregate used as a sort key has to\n * produce something with an order, and these are the ones that do.\n *\n * @group Models\n */\nexport type RelationAggregateFn = \"min\" | \"max\" | \"count\" | \"sum\" | \"avg\";\n\n/**\n * Order rows by an aggregate over the rows a to-many relation reaches —\n * \"candidates, oldest waiting first\", \"clients, busiest first\".\n *\n * ```ts\n * // The date of each candidate's earliest open application.\n * orderBy: [[{ relation: \"applications\", field: \"created_at\", agg: \"min\" }, \"asc\"]]\n *\n * // How many applications each candidate has.\n * orderBy: [[{ relation: \"applications\", agg: \"count\" }, \"desc\"]]\n * ```\n *\n * This is the half of a queue that cannot be worked around client-side. A\n * *filter* over a relation can be approximated by denormalising a flag onto the\n * row; an *ordering* cannot be approximated at all once the result set is\n * paged, because the client only ever holds one page and the page was chosen by\n * the wrong order.\n *\n * Rows the relation reaches nothing from sort last ascending and first\n * descending — the placement Postgres gives a `NULL`, stated rather than\n * inherited, because the keyset comparison behind cursor paging has to agree\n * with it exactly. Ties are broken by the row id, so the order is total and\n * paging over it neither repeats nor skips.\n *\n * Compiled by the driver into a correlated subquery, so it is subject to the\n * reader's own row-level security on the target table: a related row the reader\n * cannot see does not contribute to the aggregate. Offered only where\n * {@link DataSourceCapabilities.relationAggregateSorts} says the driver can\n * compile it.\n *\n * @group Models\n */\nexport interface RelationAggregateSort {\n /** The to-many relation to aggregate over, by its name on this collection. */\n relation: string;\n\n /** The aggregate to apply. */\n agg: RelationAggregateFn;\n\n /**\n * The column of the *target* to aggregate. Required by every function\n * except `count`, which counts the related rows themselves when it is\n * omitted — and counts the rows whose column is non-null when it is not.\n */\n field?: string;\n}\n\n/** The wire spelling of a {@link RelationAggregateSort}: `min(applications.created_at)`. */\nconst RELATION_AGGREGATE_SORT_PATTERN = /^(min|max|count|sum|avg)\\(([^().]+)(?:\\.([^()]+))?\\)$/;\n\n/**\n * A {@link RelationAggregateSort} as a single string — `min(applications.created_at)`,\n * `count(applications)`.\n *\n * The wire form is a string because every layer below the call site already is\n * one: `OrderByTuple` is `[string, direction]`, the REST parameter is\n * `?orderBy=key:direction`, the driver contract takes `orderBy?: string |\n * OrderByTuple[]`, and a cursor names its keys by string. `_score` established\n * the same pattern — a sort key that is not a column, spelled as one — and this\n * reuses it rather than widening five signatures to carry an object that would\n * be flattened at the end anyway.\n *\n * SQL's own spelling, so the key reads as what it compiles to. Neither `:` nor\n * `,` appears in it, which is what keeps it safe in the colon-delimited wire\n * shorthand.\n *\n * @group Models\n */\nexport function encodeRelationAggregateSort(sort: RelationAggregateSort): string {\n return `${sort.agg}(${sort.relation}${sort.field ? `.${sort.field}` : \"\"})`;\n}\n\n/**\n * Read the string spelling back, or `undefined` if it is not one.\n *\n * `undefined` rather than a throw: this is asked of *every* sort key to find\n * out which kind it is, and an ordinary column name is not an error.\n *\n * @group Models\n */\nexport function parseRelationAggregateSort(key: string): RelationAggregateSort | undefined {\n const match = RELATION_AGGREGATE_SORT_PATTERN.exec(key);\n if (!match) return undefined;\n const [, agg, relation, field] = match;\n // `min()` and friends have nothing to aggregate without a column, and a\n // key that parses to a half-built sort would resolve to no expression and\n // be dropped — leaving the rows unsorted while the caller believes\n // otherwise. `count` is the one function that means something on its own.\n if (!field && agg !== \"count\") return undefined;\n return { agg: agg as RelationAggregateFn, relation, ...(field && { field }) };\n}\n\n/** Is this sort key the object form rather than a field name? */\nexport function isRelationAggregateSort(key: unknown): key is RelationAggregateSort {\n return typeof key === \"object\" && key !== null &&\n typeof (key as RelationAggregateSort).relation === \"string\" &&\n typeof (key as RelationAggregateSort).agg === \"string\";\n}\n\n/** A sort key in the single-string form every layer below the call site speaks. */\nexport function sortKeyToString(key: SortKey): string {\n return isRelationAggregateSort(key) ? encodeRelationAggregateSort(key) : key;\n}\n\n/**\n * Canonical filter operators supported across all database backends.\n * Each DB driver translates these to its native query format.\n *\n * @group Models\n */\nexport type WhereFilterOp =\n | \"<\"\n | \"<=\"\n | \"==\"\n | \"!=\"\n | \">=\"\n | \">\"\n | \"array-contains\"\n | \"in\"\n | \"not-in\"\n | \"array-contains-any\"\n | \"like\"\n | \"ilike\"\n | \"not-like\"\n | \"not-ilike\"\n | \"is-null\"\n | \"is-not-null\";\n\n/**\n * Used to define filters applied in collections.\n *\n * A single condition is a tuple `[operator, value]`.\n * Multiple conditions on the same field use an array of tuples.\n *\n * @example\n * // Single condition per field\n * { status: [\"==\", \"active\"], price: [\">=\", 9.99] }\n *\n * // Multiple conditions on one field\n * { age: [[\">=\", 18], [\"<\", 65]] }\n *\n * // Array operators\n * { role: [\"in\", [\"admin\", \"editor\"]] }\n * { tags: [\"array-contains\", \"featured\"] }\n *\n * // Pattern matching (SQL wildcards: % and _)\n * { name: [\"ilike\", \"%john%\"] }\n * { slug: [\"like\", \"post-%\"] }\n *\n * // Null checks (the value is ignored; `null` is conventional)\n * { deleted_at: [\"is-null\", null] }\n * { published_at: [\"is-not-null\", null] }\n *\n * @group Models\n */\nexport type FilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;\n\n/**\n * The field names a query may address on a row type: every column, plus a\n * dotted path reaching inside one — or *through a relation* to a column of the\n * related row.\n *\n * A dotted path is not checked at all, in either direction. That is a\n * deliberate loosening, and it is worth being exact about what it costs. The\n * root used to be checked: `\"meta.tag\"` required a `meta` column. It cannot\n * stay checked, because the other thing a dotted path now means is\n * `\"applications.status\"` — and `applications` is a *relation*, which comes\n * from the collection's `relations` and is not a column of `M` at all. There is\n * nothing in a generated row type that could validate one. `FindParams.include`\n * is `string[]` for exactly this reason and says so.\n *\n * So the guarantee moves rather than disappears: an unresolvable path is a 400\n * from the driver, not a silently dropped condition. See\n * `UnknownFilterFieldsMode` in `@rebasepro/server-postgres` — dropping a filter\n * key *widens* the read to every row, which is why that resolution fails\n * closed. A typo'd relation path is refused at runtime with the target\n * collection's real column list in the message.\n *\n * A JSON path — `metadata->>tier` — is admitted on the same terms and for the\n * same reason. It addresses a key *inside* a `json`/`jsonb` column, so nothing\n * in a generated row type describes it either; the driver resolves it and\n * refuses what it cannot. It has no dot, so the dotted branch above never\n * covered it, and every documented `?metadata->>tier=eq.gold` filter was a\n * compile error on a typed client while working perfectly over HTTP.\n *\n * Undotted keys are unaffected and still checked against `keyof M`.\n *\n * When `M` is left at its default `Record<string, unknown>`, `keyof M` is\n * `string` and this collapses to `string`, so every query stays permissive.\n * That is what keeps an untyped `createRebaseClient()` behaving exactly as it\n * did before the row type was threaded through.\n *\n * @group Models\n */\nexport type FieldPath<M extends Record<string, unknown> = Record<string, unknown>> =\n | Extract<keyof M, string>\n | NonColumnFieldPath;\n\n/**\n * A field key that is not a column: a relation path (`author.name`) or a JSON\n * path (`metadata->>tier`).\n *\n * The fluent builder needs this on its own, where `FindParams` does not. Its\n * `where(column, operator, value)` types the value against `M[column]`, which\n * only means something for a real column — so paths take a second overload\n * whose value is `unknown`. Keying that overload on the *shape* of a path,\n * rather than on \"everything that is not a column\", is what keeps a real column\n * with a wrong value type from falling through to it and being accepted: a\n * mistyped column name has neither a dot nor a `->>`, so it matches neither\n * overload and is still refused.\n *\n * @group Models\n */\nexport type NonColumnFieldPath =\n | `${string}.${string}`\n | `${string}->>${string}`;\n\n/**\n * Relaxed filter type that also accepts pre-serialized PostgREST strings.\n * **Internal only** — used at the wire-format boundary\n * (`serializeFilter` / `deserializeFilter` in `@rebasepro/common`).\n *\n * Application code, UI components, and SDK consumers should use\n * {@link FilterValues} instead.\n *\n * @internal\n */\nexport type WireFilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][] | string>>;\n\n/**\n * A pre-defined filter preset for quick access in the collection toolbar.\n * Users can select a preset to instantly apply a set of filters and\n * optionally a sort order.\n *\n * @group Models\n */\nexport interface FilterPreset<Key extends string = string> {\n /**\n * Display label shown in the preset menu.\n * If omitted, a summary is auto-generated from the filter keys.\n */\n label?: string;\n\n /**\n * The filter values to apply when this preset is selected.\n */\n filterValues: FilterValues<Key>;\n\n /**\n * Optional sort override to apply alongside the filter values.\n * One key, or several in order of significance.\n */\n sort?: OrderBySpec<Key>;\n}\n\n/**\n * PostgREST short-code operators. Wire format only — these never appear\n * in application code. Used by `serializeFilter`/`deserializeFilter`\n * in `@rebasepro/common`.\n */\nexport type RestFilterOp =\n | \"eq\" | \"neq\"\n | \"gt\" | \"gte\"\n | \"lt\" | \"lte\"\n | \"in\" | \"nin\"\n | \"cs\" | \"csa\"\n | \"like\" | \"ilike\"\n | \"nlike\" | \"nilike\"\n | \"isnull\" | \"notnull\";\n\n/** Maps canonical operators to their REST short-code equivalents. */\nexport const CANONICAL_TO_REST: Readonly<Record<WhereFilterOp, RestFilterOp>> = {\n \"==\": \"eq\",\n \"!=\": \"neq\",\n \">\": \"gt\",\n \">=\": \"gte\",\n \"<\": \"lt\",\n \"<=\": \"lte\",\n \"in\": \"in\",\n \"not-in\": \"nin\",\n \"array-contains\": \"cs\",\n \"array-contains-any\": \"csa\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"not-like\": \"nlike\",\n \"not-ilike\": \"nilike\",\n \"is-null\": \"isnull\",\n \"is-not-null\": \"notnull\"\n};\n\n/** Maps REST short-code operators to their canonical equivalents. */\nexport const REST_TO_CANONICAL: Readonly<Record<RestFilterOp, WhereFilterOp>> = {\n \"eq\": \"==\",\n \"neq\": \"!=\",\n \"gt\": \">\",\n \"gte\": \">=\",\n \"lt\": \"<\",\n \"lte\": \"<=\",\n \"in\": \"in\",\n \"nin\": \"not-in\",\n \"cs\": \"array-contains\",\n \"csa\": \"array-contains-any\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"nlike\": \"not-like\",\n \"nilike\": \"not-ilike\",\n \"isnull\": \"is-null\",\n \"notnull\": \"is-not-null\"\n};\n\n/**\n * Operators that test for null/not-null and therefore ignore their value.\n * Codecs normalize the value of these conditions to `null`.\n */\nexport const NULL_OPS: ReadonlySet<WhereFilterOp> = new Set<WhereFilterOp>([\n \"is-null\", \"is-not-null\"\n]);\n\n/**\n * Operators whose operand is a **list** of values rather than one value.\n *\n * On the wire that list is always parenthesised — `in.(draft,review)` — which\n * is what lets the REST codec tell `?status=in.(a,b)` (the operator) from\n * `?status=in.progress` (a value that happens to start with an operator's\n * name). See `deserializeSingle` in `@rebasepro/common`.\n *\n * @group Models\n */\nexport const LIST_OPS: ReadonlySet<WhereFilterOp> = new Set<WhereFilterOp>([\n \"in\", \"not-in\", \"array-contains-any\"\n]);\n\n/**\n * Every canonical operator, in a stable order. Useful for engine capability\n * declarations ({@link DataSourceCapabilities.filterOperators}) and for\n * building operator subsets.\n * @group Models\n */\nexport const ALL_WHERE_FILTER_OPS: readonly WhereFilterOp[] = [\n \"<\", \"<=\", \"==\", \"!=\", \">=\", \">\",\n \"in\", \"not-in\",\n \"array-contains\", \"array-contains-any\",\n \"like\", \"ilike\", \"not-like\", \"not-ilike\",\n \"is-null\", \"is-not-null\"\n];\n\n/** All canonical operator strings for runtime validation. */\nconst CANONICAL_OPS: ReadonlySet<string> = new Set<WhereFilterOp>(ALL_WHERE_FILTER_OPS);\n\n/**\n * The REST table as a `Map`, because the key `toCanonicalOp` is handed comes\n * off the wire.\n *\n * Indexed as a plain object, every `Object.prototype` member answered:\n * `toCanonicalOp(\"valueOf\")` returned the inherited *function* as though it\n * were a `WhereFilterOp`, and every caller here treats a defined result as\n * \"known operator\". Same defect the REST codec's own lookup tables were\n * converted away from in `filter-dialect.ts`; this is the copy that survived\n * one package over, and it now sits under the operator validation the REST\n * parser does, which would otherwise have admitted `[\"constructor\", x]`.\n */\nconst REST_OP_LOOKUP: ReadonlyMap<string, WhereFilterOp> = new Map<string, WhereFilterOp>(\n Object.entries(REST_TO_CANONICAL) as [string, WhereFilterOp][]\n);\n\n/**\n * Resolve any operator string (canonical or REST short-code) to its\n * canonical `WhereFilterOp` form. Returns `undefined` for unknown operators.\n *\n * @example\n * toCanonicalOp(\"==\") // \"==\"\n * toCanonicalOp(\"eq\") // \"==\"\n * toCanonicalOp(\"cs\") // \"array-contains\"\n * toCanonicalOp(\"xyz\") // undefined\n */\nexport function toCanonicalOp(op: string): WhereFilterOp | undefined {\n if (CANONICAL_OPS.has(op)) return op as WhereFilterOp;\n return REST_OP_LOOKUP.get(op);\n}\n","/**\n * The keys of a collection's admin block, as data.\n *\n * There is no *type* for the block in this package any more, and that is the point:\n * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a\n * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by\n * declaration merging, which is why installing it is what makes the admin surface\n * appear.\n *\n * The *list* still has to live here, because three runtime consumers need it and two\n * of them are core — see below.\n */\n\n/**\n * Every key that belongs inside a collection's `admin` block, as data.\n *\n * The type that describes these fields is `AdminCollectionOptions` in\n * `@rebasepro/cms-types`, and it is erased at build time — but three runtime\n * consumers need the list, and two of them are core:\n *\n * - `serializeCollections`, to drop the block from the contract\n * - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection\n * files on disk from the admin panel and has to know where each key goes. A key\n * missing from this list gets written to the *top level* of the file, where the\n * backend ignores it and the panel never finds it again.\n * - the `collections-admin-block` codemod\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real option\n * keys; the count is pinned by a test there.\n *\n * @group Models\n */\nexport const ADMIN_COLLECTION_KEYS = [\n \"Actions\",\n \"additionalFields\",\n \"alwaysApplyDefaultValues\",\n \"browserCallbacks\",\n \"components\",\n \"customViews\",\n \"defaultEntityAction\",\n \"defaultFilter\",\n \"defaultSelectedView\",\n \"defaultSize\",\n \"defaultViewMode\",\n \"disableDefaultActions\",\n \"display\",\n \"enabledViews\",\n \"entityActions\",\n \"entityViews\",\n \"exportable\",\n \"filterPresets\",\n \"fixedFilter\",\n \"form\",\n \"formAutoSave\",\n \"formView\",\n \"group\",\n \"hideFromEntityViews\",\n \"hideFromNavigation\",\n \"hideIdFromCollection\",\n \"hideIdFromForm\",\n \"icon\",\n \"includeJsonView\",\n \"inlineEditing\",\n \"kanban\",\n \"listProperties\",\n \"localChangesBackup\",\n \"openEntityMode\",\n \"orderProperty\",\n \"pagination\",\n \"previewProperties\",\n \"propertiesOrder\",\n \"selectionController\",\n \"selectionEnabled\",\n \"sideDialogWidth\",\n \"sort\"\n] as const;\n\n/** A key of a collection's `admin` block. @group Models */\nexport type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];\n\n/**\n * Every key that belongs inside a *property's* `admin` block, as data.\n *\n * The union of `AdminPropertyOptions` and its per-type extensions\n * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.\n * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the\n * runtime consumers are core packages that the BaaS guard forbids from\n * importing `@rebasepro/cms-types`. Here it is the boot-time collection\n * validator in `@rebasepro/server`, which has to tell \"you left `readOnly` at\n * the top of the property, where nothing reads it\" apart from \"you invented a\n * key we have never heard of\".\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real\n * option keys.\n *\n * @group Models\n */\nexport const ADMIN_PROPERTY_KEYS = [\n \"canAddElements\",\n \"clearable\",\n \"columnWidth\",\n \"customProps\",\n \"disabled\",\n \"expanded\",\n \"Field\",\n \"Filter\",\n \"filterOperators\",\n \"fixedFilter\",\n \"format\",\n \"hideFromCollection\",\n \"includeEntityLink\",\n \"includeId\",\n \"markdown\",\n \"minimalistView\",\n \"multiline\",\n \"Preview\",\n \"previewAsTag\",\n \"previewProperties\",\n \"readOnly\",\n \"renderInForm\",\n \"sortable\",\n \"span\",\n \"spreadChildren\",\n \"urlPreview\",\n \"widget\",\n] as const;\n\n/** A key of a property's `admin` block. @group Models */\nexport type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];\n\n/**\n * Move flattened admin keys back down into the `admin` block.\n *\n * The admin panel works with a *flat* view model — the block merged onto the\n * collection — so what comes back from a form has `icon` and `defaultViewMode`\n * at the top level while `admin` still holds whatever the file was loaded with.\n * This is the way back.\n *\n * **The top-level value wins.** It is the one the form just wrote; the block is\n * the copy the collection was loaded with, and preferring it resolves every edit\n * in favour of the value the user changed away from.\n *\n * This lives here, next to the key lists, because it had two implementations —\n * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in\n * `@rebasepro/server`'s schema editor — that agreed on everything except that\n * precedence, which is the only part that decides whether a save is visible.\n *\n * @group Models\n */\nexport function nestAdminKeysOf(\n source: Record<string, unknown>,\n adminKeys: readonly string[]\n): Record<string, unknown> {\n const keys = new Set<string>(adminKeys);\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if (keys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top;\n}\n\n/**\n * {@link nestAdminKeysOf} for a collection.\n *\n * @group Models\n */\nexport function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {\n return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);\n}\n\n/** A record of properties, keyed by name — a map's `properties`, or a `oneOf` block's. */\nfunction nestEachProperty(properties: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(properties).map(([key, child]) => [\n key,\n isNestable(child) ? nestAdminPropertyKeys(child) : child\n ])\n );\n}\n\n/** Anything the walk can descend into: a plain object, not an array. */\nfunction isNestable(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * {@link nestAdminKeysOf} for a property, applied to its children too.\n *\n * A map property carries `properties`, an array property carries `of`, and an\n * array of typed blocks carries `oneOf.properties` — a record of properties like\n * a map's. All of them hold properties with `admin` blocks of their own. A flat\n * `readOnly` left on a child is as dead — and as fatal at the next boot — as one\n * left on the parent, so the walk goes all the way down.\n *\n * `oneOf` was the container this walk did not know about, and it is the one the\n * block-based collection templates are built out of: every block inside them\n * kept its flat `markdown`, and the collection they created would not boot.\n *\n * @group Models\n */\nexport function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);\n\n const children = nested.properties;\n if (isNestable(children)) {\n nested.properties = nestEachProperty(children);\n }\n\n // `oneOf` is not itself a property — it is a block holding `properties`\n // alongside `typeField`, `valueField` and `propertiesOrder`, none of which\n // may be walked as one.\n const oneOf = nested.oneOf;\n if (isNestable(oneOf) && isNestable(oneOf.properties)) {\n nested.oneOf = { ...oneOf, properties: nestEachProperty(oneOf.properties) };\n }\n\n const of = nested.of;\n if (Array.isArray(of)) {\n nested.of = of.map(entry => isNestable(entry) ? nestAdminPropertyKeys(entry) : entry);\n } else if (isNestable(of)) {\n nested.of = nestAdminPropertyKeys(of);\n }\n\n return nested;\n}\n","import { ALL_WHERE_FILTER_OPS, WhereFilterOp } from \"./filter-operators\";\n\n/**\n * Describes the capabilities and features supported by a data source (driver).\n *\n * Each driver (Postgres, Firebase, MongoDB, etc.) declares which features it\n * supports. The admin uses this descriptor to:\n * - Show/hide editor tabs (e.g. Relations for SQL, Subcollections for Firebase)\n * - Filter the property type picker (e.g. `relation` for SQL, `reference` for Firebase)\n * - Toggle driver-specific form controls (e.g. `columnType` for SQL)\n *\n * @group Models\n */\nexport interface DataSourceCapabilities {\n /** Unique driver key (e.g. \"postgres\", \"firestore\", \"mongodb\") */\n key: string;\n\n /** Human-readable label for the UI (e.g. \"PostgreSQL\", \"Firebase / Firestore\") */\n label: string;\n\n // ── Feature flags ─────────────────────────────────────────────────\n /** Does this source support SQL-style relations (JOINs)? */\n supportsRelations: boolean;\n\n /** Does this source support nested subcollections? */\n supportsSubcollections: boolean;\n\n /** Does this source support Row Level Security policies? */\n supportsRLS: boolean;\n\n /** Does this source support document references (Firebase-style)? */\n supportsReferences: boolean;\n\n /** Does this source support SQL column type annotations? */\n supportsColumnTypes: boolean;\n\n /** Does this source support real-time listeners? */\n supportsRealtime: boolean;\n\n /**\n * Does this source store vectors natively?\n *\n * `VectorProperty` carries a `dimensions` and is pgvector-shaped. It was\n * the one driver-specific property kind with no flag to gate it, so unlike\n * every other field in this descriptor there was not even a runtime answer\n * to appeal to — a Firestore collection could declare an embedding column\n * and no driver would do anything with it.\n */\n supportsVectors: boolean;\n\n /**\n * Canonical filter operators this engine can execute.\n *\n * The admin UI intersects this set with the property-type defaults and\n * any per-property narrowing (`property.ui.filterOperators`) to decide\n * which operators to offer in filter fields — so an engine that cannot\n * run `ilike` (e.g. Firestore) never shows a \"Contains\" filter that\n * would throw at query time.\n */\n filterOperators: readonly WhereFilterOp[];\n\n /**\n * Relation kinds this engine's driver can compile into a filter.\n *\n * Only `belongsTo` puts a column on the row being filtered; the others are\n * answered with a correlated subquery over the junction or the target\n * table, which not every driver can build. An engine with no relations at\n * all declares none.\n *\n * The admin uses this to decide whether a relation column offers a filter\n * control. Offering one an engine cannot answer is not cosmetic: a driver\n * that drops the key it cannot resolve *widens* the read to every row, and\n * one that fails closed answers a control the admin itself put on screen\n * with a 400.\n *\n * Optional, so a third-party driver registered before this existed still\n * compiles. Omitted means {@link DEFAULT_FILTERABLE_RELATION_KINDS} — the\n * one kind that is a plain column comparison, which every relational\n * driver can do. The subquery kinds are a real capability and have to be\n * claimed rather than assumed: assuming them wrongly is the widening.\n */\n filterableRelationKinds?: readonly string[];\n\n /**\n * Can a filter address a *column of the related row* — `applications.status`\n * — rather than only the related row's id?\n *\n * A separate capability from {@link filterableRelationKinds} because it is\n * a separate subquery: the id filter stops at the junction, one of these\n * reaches the target table and compares one of its columns. A driver can\n * do the first and not the second.\n *\n * Optional and defaulting to **false**, for the reason the relation kinds\n * default narrow: an unclaimed capability that the admin assumes is there\n * produces a control whose query the driver answers by dropping the key —\n * and a dropped filter key widens the read to every row.\n *\n * Meaningless without {@link supportsRelations}; a driver with no relations\n * has nothing to reach through.\n */\n supportsRelationFieldFilters?: boolean;\n\n /**\n * Can a sort key be an aggregate over a to-many relation — \"oldest waiting\n * first\", \"busiest first\"?\n *\n * Compiled as a correlated scalar subquery in `ORDER BY`, which a document\n * store cannot express at all. Optional and defaulting to **false**.\n *\n * A wrongly claimed sort capability fails differently from a wrongly\n * claimed filter one, and worse in one respect: a driver that cannot\n * resolve the key drops the `ORDER BY` and answers 200 with rows in\n * whatever order the database pleased, which reads as a sorted list. Paging\n * over that repeats and skips rows.\n */\n relationAggregateSorts?: boolean;\n\n // ── Admin capability flags ───────────────────────────────────────\n /** Does this source support SQL admin operations (SQL editor, EXPLAIN, etc.)? */\n supportsSQLAdmin: boolean;\n\n /** Does this source support document admin operations (aggregation, stats)? */\n supportsDocumentAdmin: boolean;\n\n /** Does this source support schema admin (unmapped tables, table metadata)? */\n supportsSchemaAdmin: boolean;\n}\n\n/**\n * Subset of DataSourceCapabilities containing only feature flags.\n * Useful when you only need to check capabilities without UI metadata.\n * @group Models\n */\nexport type DataSourceFeatures = Omit<DataSourceCapabilities, \"key\" | \"label\">;\n\n/**\n * The default data-source key, used when a collection does not name a\n * `dataSource`. Shared by the frontend router and the backend driver\n * registry so both agree on \"the default database\".\n * @group Models\n */\nexport const DEFAULT_DATA_SOURCE_KEY = \"(default)\";\n\n/**\n * How the *frontend* reaches a data source.\n *\n * - `\"server\"` — through the Rebase backend (the `RebaseClient`). The backend\n * holds the actual database adapter and routes by data-source key. This is\n * the default and covers Postgres, MongoDB, and any other server-mediated\n * engine.\n * - `\"direct\"` — straight from the client to the external backend via its own\n * SDK driver (e.g. Firestore). The Rebase backend is not in the data path.\n * - `\"custom\"` — a developer-supplied {@link DataDriver}, transport unspecified.\n *\n * @group Models\n */\nexport type DataSourceTransport = \"server\" | \"direct\" | \"custom\";\n\n/**\n * Declarative definition of a data source — a named place data lives.\n *\n * Declared once and shared front and back: the frontend uses it to decide\n * transport (client vs direct driver), the backend uses the same `key` to\n * resolve a database adapter, and the editor derives capabilities from\n * `engine`. Collections reference a definition by its `key` via\n * `collection.dataSource`.\n *\n * @group Models\n */\nexport interface DataSourceDefinition {\n /**\n * Unique identifier for this data source. Collections point at it via\n * `dataSource`. Defaults to {@link DEFAULT_DATA_SOURCE_KEY}.\n */\n key: string;\n\n /**\n * The engine backing this data source (e.g. `\"postgres\"`, `\"mongodb\"`,\n * `\"firestore\"`, or a custom id). Determines the\n * {@link DataSourceCapabilities} surfaced in the editor.\n */\n engine: string;\n\n /**\n * How the frontend reaches this source. Optional — when omitted it is\n * inferred: `\"direct\"` if the definition carries a client-side driver,\n * `\"server\"` otherwise.\n */\n transport?: DataSourceTransport;\n\n /**\n * The physical database/schema/Firestore-database within the engine.\n * Threaded to drivers/adapters as the existing `databaseId` runtime\n * parameter. Defaults to the engine's own default.\n */\n databaseId?: string;\n\n /** Human-readable label for the UI. */\n label?: string;\n}\n\n/**\n * The resolved data source for a collection: the single source of truth that\n * the frontend router, backend registry, and editor all derive from.\n * Produced by `resolveDataSource(collection, registry)`.\n *\n * @group Models\n */\nexport interface ResolvedDataSource {\n /** Data-source key (routing key, shared front + back). */\n key: string;\n /** Engine backing the source (drives capabilities). */\n engine: string;\n /** Frontend transport. */\n transport: DataSourceTransport;\n /** Within-engine instance, if any (the `databaseId` runtime param). */\n databaseId?: string;\n /** Capabilities derived from {@link engine}. */\n capabilities: DataSourceCapabilities;\n}\n\n/**\n * Relation kinds assumed filterable when a driver does not say.\n *\n * `belongsTo` alone: its filter is a comparison on a column of the row being\n * filtered, the one shape that needs no query construction a driver might not\n * have. Everything else is a correlated subquery over another table.\n *\n * @group Models\n */\nexport const DEFAULT_FILTERABLE_RELATION_KINDS: readonly string[] = [\"belongsTo\"];\n\n// ── Built-in driver capabilities ─────────────────────────────────────\n\n/** @group Models */\nexport const POSTGRES_CAPABILITIES: DataSourceCapabilities = {\n key: \"postgres\",\n label: \"PostgreSQL\",\n supportsRelations: true,\n supportsSubcollections: false,\n supportsRLS: true,\n supportsReferences: false,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // `via` is absent: its join path is authored source → target with no\n // stated inverse, so the driver has nothing to reverse into a filter.\n filterableRelationKinds: [\"belongsTo\", \"manyToMany\", \"hasMany\", \"hasOne\"],\n supportsRelationFieldFilters: true,\n relationAggregateSorts: true,\n supportsSQLAdmin: true,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: true\n};\n\n/** @group Models */\nexport const FIREBASE_CAPABILITIES: DataSourceCapabilities = {\n key: \"firestore\",\n label: \"Firebase / Firestore\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: true,\n supportsVectors: false,\n // Firestore has no SQL pattern matching — the driver throws on the LIKE\n // family, so the UI must never offer it.\n filterOperators: ALL_WHERE_FILTER_OPS.filter(op =>\n op !== \"like\" && op !== \"ilike\" && op !== \"not-like\" && op !== \"not-ilike\"),\n // No relations at all — a document store links by reference. Nothing to\n // reach through, so neither of the two relation-reaching features either.\n filterableRelationKinds: [],\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: false,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: false\n};\n\n/** @group Models */\nexport const MONGODB_CAPABILITIES: DataSourceCapabilities = {\n key: \"mongodb\",\n label: \"MongoDB\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: false,\n supportsVectors: false,\n filterOperators: ALL_WHERE_FILTER_OPS,\n filterableRelationKinds: [],\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: false,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\n/**\n * Fallback capabilities when the driver is unknown.\n * Enables everything so nothing is hidden unexpectedly.\n * @group Models\n */\nexport const DEFAULT_CAPABILITIES: DataSourceCapabilities = {\n key: \"(default)\",\n label: \"Default\",\n supportsRelations: true,\n supportsSubcollections: true,\n supportsRLS: true,\n supportsReferences: true,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // The exception to this descriptor's \"enable everything\" rule. The other\n // flags hide a tab or a picker when they are wrong; this one decides\n // whether a query is sent that an unknown driver may answer by dropping\n // the condition — which returns every row rather than none.\n filterableRelationKinds: DEFAULT_FILTERABLE_RELATION_KINDS,\n // Narrow for the same reason, and more sharply. An unknown driver that is\n // assumed to compile these answers by dropping the key: the filter widens\n // the read to every row, and the sort comes back unordered while looking\n // sorted. Both have to be claimed.\n supportsRelationFieldFilters: false,\n relationAggregateSorts: false,\n supportsSQLAdmin: true,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\nconst CAPABILITIES_REGISTRY: Record<string, DataSourceCapabilities> = {\n postgres: POSTGRES_CAPABILITIES,\n firestore: FIREBASE_CAPABILITIES,\n mongodb: MONGODB_CAPABILITIES,\n \"(default)\": DEFAULT_CAPABILITIES\n};\n\n/**\n * Look up capabilities for a given engine key.\n * If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.\n * @group Models\n */\nexport function getDataSourceCapabilities(engine?: string): DataSourceCapabilities {\n if (!engine) return POSTGRES_CAPABILITIES; // postgres is the default engine\n return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;\n}\n\n/**\n * Register custom capabilities for a third-party driver.\n * @group Models\n */\nexport function registerDataSourceCapabilities(capabilities: DataSourceCapabilities): void {\n CAPABILITIES_REGISTRY[capabilities.key] = capabilities;\n}\n","import type { CollectionCallbacks } from \"./entity_callbacks\";\n\nimport type { EnumValues, Properties, PostgresProperties, FirebaseProperties, MongoProperties } from \"./properties\";\n\nimport type { User } from \"../users\";\nimport type { EmailSendResult } from \"../controllers/email\";\nimport type { Relation } from \"./relations\";\nimport type { SecurityRule } from \"./security_rules\";\nimport { getDataSourceCapabilities } from \"./data_source\";\nimport type { WhereFilterOp, FilterValues, FilterPreset } from \"./filter-operators\";\nimport type { SearchConfig } from \"./search\";\nimport type { CollectionIndex } from \"./indexes\";\nimport type { CollectionTenantConfig } from \"./tenancy\";\n\n/**\n * Base interface containing all driver-agnostic collection properties.\n * Use {@link PostgresCollectionConfig} or {@link FirebaseCollectionConfig} for\n * driver-specific type safety, or {@link CollectionConfig} when you\n * need to handle any collection regardless of backend.\n *\n * @group Models\n */\nexport interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {\n\n /**\n * The collection's identity. Required, and the value nearly everything else\n * keys on:\n *\n * - the REST path — `/api/data/<slug>`\n * - the SDK accessor — `client.data.<slug>` / `client.data.collection(\"<slug>\")`\n * - the admin panel's URL\n * - the target of a `reference` or `relation` property\n *\n * Conventionally kebab-case and plural (`blog-posts`). It is independent of\n * {@link table}: the slug is what callers say, the table is where the rows\n * live, and renaming one does not rename the other.\n *\n * Treat it as frozen once anything has shipped against it — changing a slug\n * changes every URL and every generated accessor at once.\n *\n * @example\n * defineCollection({\n * slug: \"blog-posts\", // /api/data/blog-posts, client.data.blogPosts\n * table: \"posts\",\n * properties: { … }\n * })\n */\n slug: string;\n\n /**\n * Name of the collection, typically plural.\n * E.g. `Products`, `Blog`\n */\n name: string;\n\n /**\n * Singular name of an entry in this collection\n * E.g. `Product`, `Blog entry`\n */\n singularName?: string;\n\n /**\n * Optional description of this view. You can use Markdown.\n */\n description?: string;\n\n /**\n * Child collections nested under entities of this collection.\n * Populated automatically during normalization from driver-specific fields\n * (e.g. Firebase `subcollections`, Postgres `relations` with many-cardinality).\n *\n * Custom drivers can set this directly to expose child collections to the UI.\n */\n childCollections?: () => CollectionConfig<Record<string, unknown>>[];\n\n\n /**\n * The data source this collection belongs to — the routing key shared by\n * the frontend router and the backend driver registry. It points at a\n * {@link DataSourceDefinition} registered on `<Rebase dataSources>` (front)\n * and `initializeRebaseBackend({ dataSources })` (back).\n *\n * If not specified, the default data source `\"(default)\"` is used, which\n * for a standard Rebase app is the server-mediated Postgres backend.\n *\n * @example\n * // Default data source (server-mediated Postgres)\n * { slug: \"products\" }\n *\n * // A direct-transport Firestore data source registered as \"analytics\"\n * { slug: \"events\", dataSource: \"analytics\" }\n *\n * // The same, spelled once — `defineCollection` accepts the handle\n * // `database(\"analytics\")` returned and records its key here.\n * import { analytics } from \"../resources\";\n * defineCollection({ slug: \"events\", dataSource: analytics, … })\n *\n * A string on the recorded collection, because a collection is data past\n * `defineCollection`: it serialises, it compares with `===`, and it reaches\n * the admin UI over the wire, none of which a handle survives.\n */\n dataSource?: string;\n\n /**\n * The database engine backing this collection (`\"postgres\"`, `\"firestore\"`,\n * `\"mongodb\"`, or a custom id).\n *\n * On concrete collection types ({@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, {@link MongoDBCollectionConfig}) this is a literal\n * discriminant. On the base type it is optional and gets stamped\n * automatically during collection normalization from the registered\n * {@link DataSourceDefinition}.\n *\n * Prefer setting {@link dataSource} and letting the engine be resolved.\n */\n engine?: string;\n\n /**\n * Which database within the engine.\n * - For Firestore: The Firestore database ID (e.g., for multi-database projects)\n * - For PostgreSQL: Schema or database name\n * - For MongoDB: Database name\n *\n * If not specified, the default database of the engine is used. Resolved\n * from the collection's {@link DataSourceDefinition} when omitted here.\n */\n databaseId?: string;\n\n /**\n * Set of properties that compose a entity\n */\n properties: Properties;\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * Mark this collection as an authentication collection.\n * When true, this collection is used for user management, login, password hashing, and invitation flows.\n */\n auth?: boolean | AuthCollectionConfig;\n\n\n\n\n\n\n\n /**\n * Row-level authorization rules for this collection.\n *\n * Driver-agnostic on purpose, unlike `disableDefaultPolicies`, `table` and\n * `relations`, which are declared on {@link PostgresCollectionConfig} only.\n * The rules are a *contract* — who may read or write which rows — and each\n * engine enforces it its own way:\n *\n * - **Postgres** compiles them to real `CREATE POLICY` statements and lets\n * the database enforce them (see {@link PostgresCollectionConfig.securityRules},\n * which narrows this with the raw-SQL details).\n * - **MongoDB** translates them into a query filter it AND-s into every\n * read and write, honouring `access`, `ownerField`, `roles`, `mode` and\n * the `operation`/`operations` selectors, and making a best effort at raw\n * `using`/`withCheck` SQL.\n * - **Firestore** does not implement them at all; its own rules language is\n * evaluated by Google, not from here. `supportsRLS` on\n * {@link DataSourceCapabilities} reports which engines generate policies,\n * which is not the same question as whether an engine honours a rule.\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * This interface defines all the callbacks that can be used when a entity\n * is being created, updated or deleted.\n * Useful for adding your own logic or blocking the execution of the operation.\n */\n readonly callbacks?: CollectionCallbacks<M, USER>;\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * User id of the owner of this collection. This is used only by plugins, or if you\n * are writing custom code\n *\n * **Admin form only — not enforced by the API or the database.** The\n * collection editor stamps it on a collection it creates and shows it\n * beside the name; nothing on the request path consults it. It is not an\n * ownership check, and a collection with somebody else's id here is served\n * to exactly the same callers as one with none.\n */\n ownerId?: string;\n\n /**\n * Arbitrary key-value metadata for external consumers.\n * Not interpreted by Rebase — passed through serialization unchanged.\n * Used by domain apps to store custom per-collection config.\n */\n metadata?: Record<string, unknown>;\n\n\n\n\n /**\n * If set to true, changes to the entity will be saved in a subcollection.\n * This prop has no effect if the history plugin is not enabled\n */\n history?: boolean;\n\n /**\n * Whether a write naming a field this collection does not declare is\n * rejected with a 400. Defaults to `true`.\n *\n * Set to `false` where a column really does exist that the config never\n * declared — populated by a trigger, or introspected rather than declared —\n * and callers need to write it. The column still has to exist: the driver\n * checks the key against the table's own columns whatever this is set to,\n * because a key with no column behind it is not passed to the database and\n * refused, it is dropped from the statement and answered 201.\n *\n * It does not let a typo through to Postgres for Postgres to judge. That is\n * what this flag was documented as doing, and no such judgment ever\n * happened.\n */\n strictWrites?: boolean;\n\n\n\n\n\n\n\n\n}\n\n// ── Driver-specific collection types ──────────────────────────────────\n\n/**\n * A collection backed by PostgreSQL (or any SQL database).\n * Adds support for SQL-style relations (JOINs) and Row Level Security.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only SQL-relevant fields appear.\n *\n * @group Models\n */\nexport interface PostgresCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n properties: PostgresProperties;\n\n /**\n * The database engine for this collection. For Postgres collections this\n * can be omitted (Postgres is the default) or set to `\"postgres\"`.\n */\n engine?: \"postgres\" | undefined;\n\n /**\n * The PostgreSQL table name for this collection.\n *\n * Optional: it defaults to `toSnakeCase(slug)`, which is what\n * `getTableName()` has always returned when it was absent. The type simply\n * demanded what the runtime already derived, so the smallest collection\n * anyone could write named its table twice —\n * `{ slug: \"todos\", table: \"todos\", … }` — and \"why do I write it twice\"\n * is the first question every evaluator asked.\n *\n * Set it only when the table name differs from the slug: an existing\n * database whose table is `blog_posts` while the URL should stay `posts`.\n *\n * Note that a **derived** name is still a real name, and nothing yet warns\n * when one moves. Foreign-key and junction column defaults are derived from\n * the *slug* rather than from this field, so renaming a slug re-derives\n * them on the next `db push` even where `table` is pinned.\n */\n table?: string;\n\n /**\n * The PostgreSQL schema name for this table.\n * E.g. \"public\", \"rebase\", \"auth\".\n * If not specified, \"public\" is used (or the default search path).\n */\n schema?: string;\n\n /**\n * For SQL databases, you can define the relations between collections here.\n * Relations describe JOINs, foreign keys, and junction tables.\n */\n relations?: Relation[];\n\n /**\n * Security rules for this collection (PostgreSQL Row Level Security).\n * When defined, the schema generator will enable RLS on the table and\n * create the corresponding PostgreSQL policies.\n *\n * Supports three levels of expressiveness:\n * 1. **Convenience shortcuts** — `ownerField`, `access`, `roles`\n * 2. **Raw SQL** — `using` and `withCheck` for full PostgreSQL power\n * 3. **Combined** — mix shortcuts with `roles` for common patterns\n *\n * The authenticated user context is available in raw SQL via:\n * - `rebase.uid()` — the current user's ID\n * - `rebase.roles()` — comma-separated app role IDs\n * - `rebase.jwt()` — full JWT claims as JSONB\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * Opt out of the framework's default Row Level Security policies.\n *\n * The schema generator automatically injects, for every collection, a\n * baseline SELECT policy granting the trusted server context and the\n * `admin` role read access (reads run under a restricted role, so RLS\n * default-denies without it). For auth collections it additionally injects\n * a self-read policy (`id = rebase.uid()`) and an admin-only write gate\n * (INSERT/UPDATE/DELETE require the `admin` role or the trusted server\n * context), making privileged columns such as `roles` safe by default.\n *\n * Author-defined `securityRules` are permissive and broaden access on top\n * of these defaults. Set this flag to `true` to remove the defaults\n * entirely and take full responsibility for the collection's RLS.\n *\n * @default false\n */\n disableDefaultPolicies?: boolean;\n\n /**\n * Opt in to Postgres full-text search for this collection.\n *\n * Omit it and `.search()` keeps its existing behaviour exactly — an\n * `ILIKE '%term%'` across top-level string properties. Declare it and the\n * collection gains one generated `tsvector` column and a GIN index, and\n * `.search()` compiles to a ranked `@@ websearch_to_tsquery` against them.\n *\n * Postgres-only, like {@link VectorProperty}: the block is rejected at boot\n * on other engines rather than silently ignored.\n *\n * @see SearchConfig\n */\n search?: SearchConfig;\n\n /**\n * Ordinary indexes on this collection's table.\n *\n * Collection-level, not per-property, because an index over two columns\n * has no single property to hang on and a partial index has none at all —\n * and because a second declaration site for the single-column case would\n * put the same object in two places. An index's identity is a column list\n * in an order; the single-column case is a degenerate one, not a special\n * one.\n *\n * `VectorProperty.index` stays where it is: an ANN structure is a property\n * of the column's type, not of a query.\n *\n * Postgres-only, like {@link SearchConfig}: refused on another engine\n * rather than silently ignored.\n */\n indexes?: readonly CollectionIndex<Extract<keyof M, string>>[];\n\n /**\n * Turn `delete` into \"stamp a timestamp\", and hide stamped rows from reads.\n *\n * With this on, a delete — single, bulk or through a nested path — sets the\n * field to `now()` instead of issuing a `DELETE`, and every read filters\n * `<field> IS NULL` by default: `find`, `findById`, `count`, aggregates, the\n * realtime refetch, and the loading of this collection through a relation.\n * A restore is an ordinary update setting the field back to `null`. A real\n * `DELETE` is still available as `delete(…, { hard: true })` / `?hard=true`,\n * and needs exactly the same permission an ordinary delete does — it is the\n * same operation, and gating it separately would be a second access-control\n * surface for one verb.\n *\n * `true` uses `deletedAt` (column `deleted_at`). The object form renames the\n * field. **Either way the collection must declare that property itself**, as\n * a `date` — this flag says what a column *means*, it does not conjure the\n * column into existence. A config that turns it on without the property is\n * refused at boot rather than at the first delete, because the failure would\n * otherwise land on a caller trying to remove a row.\n *\n * The hooks do not change: `beforeDelete` can still veto and `afterDelete`\n * still fires. From the application's point of view the row was deleted;\n * how the table records that is this flag's business.\n *\n * Postgres-only, like {@link SearchConfig}.\n */\n softDelete?: boolean | {\n /**\n * The `date` property that records the deletion. Defaults to\n * `deletedAt`.\n */\n field?: string;\n };\n\n /**\n * Scope every row of this collection to a tenant.\n *\n * One declaration replaces the four hand-written pieces a tenant-scoped\n * table used to need — the `NOT NULL` column, the RLS rule, the value\n * stamped on insert, and the index — and keeps them in agreement, because\n * they are all derived from this.\n *\n * ```ts\n * tenant: { field: \"orgId\", from: { claim: \"org_id\" } }\n * ```\n *\n * The property must already be declared: this says what a column *means*,\n * it does not create one. Postgres-only, like {@link SearchConfig} — RLS is\n * what enforces the boundary.\n *\n * @see CollectionTenantConfig\n */\n tenant?: CollectionTenantConfig<M>;\n}\n\n/**\n * A collection backed by Firebase / Firestore.\n * Adds support for subcollections (nested document collections).\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only Firestore-relevant fields appear.\n *\n * @group Models\n */\nexport interface FirebaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n /**\n * The database engine for this collection. Must be set to `\"firestore\"`.\n */\n engine: \"firestore\";\n\n /**\n * Set of properties that compose a entity.\n * Firestore collections support `reference` properties but not `relation`.\n */\n properties: FirebaseProperties;\n\n /**\n * The Firestore collection path to query. Defaults to `slug` if not set.\n * Use this when the Firestore path differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const fsCustomer: FirebaseCollectionConfig = {\n * slug: \"fs_customer\", // URL: /c/fs_customer\n * path: \"customer\", // Firestore path: customer\n * name: \"Customers (Firestore)\",\n * engine: \"firestore\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n\n /**\n * You can add subcollections to your entity in the same way you define the root\n * collections. The collections added here will be displayed when opening\n * the side dialog of a entity.\n */\n subcollections?: () => CollectionConfig<Record<string, unknown>>[];\n}\n\n/**\n * A collection backed by MongoDB.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only MongoDB-relevant fields appear.\n *\n * @group Models\n */\nexport interface MongoDBCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n\n /**\n * The database engine for this collection. Must be set to `\"mongodb\"`.\n */\n engine: \"mongodb\";\n\n /**\n * Set of properties that compose a entity.\n * MongoDB collections support `reference` properties but not `relation`.\n */\n properties: MongoProperties;\n\n /**\n * The MongoDB collection name to use. Defaults to `slug` if not set.\n * Use this when the MongoDB collection name differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const mongoCustomer: MongoDBCollectionConfig = {\n * slug: \"mongo_customer\", // URL: /c/mongo_customer\n * path: \"customer\", // MongoDB collection: customer\n * name: \"Customers (MongoDB)\",\n * engine: \"mongodb\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n}\n\n/**\n * A collection backed by any data source.\n * This is a discriminated union — use {@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, or {@link MongoDBCollectionConfig} for\n * driver-specific type safety.\n *\n * @group Models\n */\nexport type CollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> =\n | PostgresCollectionConfig<M, USER>\n | FirebaseCollectionConfig<M, USER>\n | MongoDBCollectionConfig<M, USER>;\n\n/**\n * A collection of *any* row type.\n *\n * `CollectionConfig` is **invariant** in `M`: `callbacks` both consumes `M`\n * (`AfterReadProps<M>`) and produces it, so neither direction of assignment\n * holds. `CollectionConfig<SomeRow>` is therefore not assignable to a bare\n * `CollectionConfig`, whose `M` defaults to `Record<string, unknown>`.\n *\n * That matters wherever a collection is merely *referred to* rather than read\n * from. `defineCollection` returns a config whose `M` is inferred from the\n * properties — the whole point of it — so a field typed `() => CollectionConfig`\n * rejects every collection the builder produces, and `target: () => otherCollection`\n * (the documented way to point a relation at its other end) does not compile in\n * any project that uses the builder.\n *\n * `any` is deliberate and is what it is for here: these positions never read the\n * target's rows, they only identify which collection is meant, so there is no\n * type safety to preserve and invariance is pure obstruction.\n *\n * @group Models\n */\nexport type AnyCollectionConfig = CollectionConfig<any, any>;\n\n/**\n * Type guard for PostgreSQL collections.\n * Returns true if the collection uses the Postgres engine (or the default engine).\n *\n * Generic over the *input* type, and narrows by intersection rather than\n * replacement. Narrowing to a bare `PostgresCollectionConfig` discarded whatever\n * the caller actually had — most visibly the admin panel's view model, whose\n * flattened presentation fields vanished the moment a collection passed through\n * one of these guards.\n *\n * @group Models\n */\nexport function isPostgresCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return !collection.engine || collection.engine === \"postgres\";\n}\n\n/**\n * Narrows to the SQL collection fields — `table`, `relations`,\n * `disableDefaultPolicies` — by asking the engine's declared capabilities\n * rather than by naming Postgres.\n *\n * The two halves of this already existed and were never joined. The engine\n * split (`PostgresCollectionConfig` / `FirebaseCollectionConfig` /\n * `MongoDBCollectionConfig`) said which fields belong to which engine at the\n * type level; {@link DataSourceCapabilities} said the same thing at runtime,\n * down to a `supportsRelations` flag. So call sites guarded on the capability\n * and then read a field the base type had to declare for them — which is why\n * those fields were on the base, and why a MongoDB collection could be written\n * with a `table`.\n *\n * Prefer this over {@link isPostgresCollectionConfig} wherever the question is\n * \"does this collection live in a SQL table\", so a custom SQL engine\n * registered through `registerDataSourceCapabilities` is included.\n *\n * @group Models\n */\nexport function isRelationalCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return getDataSourceCapabilities(collection.engine).supportsRelations;\n}\n\n/**\n * Type guard for Firebase / Firestore collections.\n * @group Models\n */\nexport function isFirebaseCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & FirebaseCollectionConfig<any, any> {\n return collection.engine === \"firestore\";\n}\n\n/**\n * Type guard for MongoDB collections.\n * @group Models\n */\nexport function isMongoDBCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & MongoDBCollectionConfig<any, any> {\n return collection.engine === \"mongodb\";\n}\n\n/**\n * Returns the data path for a collection.\n * For Firestore or MongoDB collections with a `path`, returns that value;\n * otherwise falls back to `slug`.\n */\nexport function getCollectionDataPath<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): string {\n if (isFirebaseCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n if (isMongoDBCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n return collection.slug;\n}\n\n/**\n * Reads a collection's driver-declared subcollections thunk (the `subcollections`\n * field) independent of engine identity, so engine-agnostic code doesn't have to\n * type-guard against a specific driver. Returns `undefined` when the collection\n * declares none.\n *\n * Pair with `getDataSourceCapabilities(engine).supportsSubcollections` to decide\n * whether the engine honours subcollections at all before reading them.\n * @group Models\n */\nexport function getDeclaredSubcollections<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): (() => CollectionConfig<Record<string, unknown>>[]) | undefined {\n return (collection as FirebaseCollectionConfig<M, USER>).subcollections;\n}\n\n/**\n * Where the rows in an {@link EntityChildView} come from.\n *\n * The two are not the same thing, and conflating them is what made a Postgres\n * relation borrow Firestore's addressing:\n *\n * - `subcollection` is **containment**. The rows live under the parent; the\n * path is their identity, and they cannot exist without it. This is what\n * Firestore has natively.\n * - `relation` is a **link**. The rows are an ordinary collection, narrowed to\n * those the parent reaches. `owned` means the child carries the parent's\n * foreign key and belongs to it alone; `linked` means the row is shared\n * through a junction, so what the parent controls is the link, not the row.\n *\n * @group Models\n */\nexport type ChildViewSource =\n | { kind: \"subcollection\" }\n | {\n kind: \"relation\";\n relationKey: string;\n mode: \"owned\" | \"linked\";\n /**\n * Slug of the collection the rows actually live in.\n *\n * Distinct from the view's `key`, which is the relation. A `linked` view\n * needs both: the key addresses the parent's set, and this addresses the\n * whole collection to pick an existing row out of.\n */\n targetSlug: string;\n };\n\n/**\n * A list of rows rendered inside an entity view — the tab under a record.\n *\n * This is a *presentation* descriptor, which is the whole point: rendering a\n * related list as a tab used to require minting a child `CollectionConfig` with\n * its own slug, which dragged a URL grammar, a path resolver and a second\n * read/write pipeline along with it. A tab needs a key, a collection to list,\n * and to know where its rows come from.\n *\n * @group Models\n */\nexport interface EntityChildView<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Stable identifier for this view: the tab id and the path segment.\n *\n * For a relation this is the **relation key** — the name the backend\n * resolves a nested path segment by — not the target collection's slug.\n * Those differ whenever a relation is named, which is every inline relation\n * property, and the mismatch is why such a tab used to open onto an error.\n */\n key: string;\n\n /** The collection whose rows this view lists, with any overrides applied. */\n collection: CollectionConfig<M>;\n\n source: ChildViewSource;\n}\n\n\nexport type { WhereFilterOp, FilterValues, WireFilterValues, FilterPreset } from \"./filter-operators\";\n\n\nexport type InferCollectionConfigType<S extends CollectionConfig> = S extends CollectionConfig<infer M> ? M : never;\n\n/**\n * Configuration for authentication collections.\n *\n * Controls what happens when admins create users, reset passwords,\n * and which entity actions are auto-injected.\n *\n * Use `auth: true` as sugar for `{ enabled: true }` with all defaults.\n *\n * @example Override user creation\n * ```ts\n * auth: {\n * enabled: true,\n * onCreateUser: async (values, ctx) => {\n * const hash = await ctx.hashPassword(\"welcome123\");\n * return {\n * values: { ...values, passwordHash: hash, emailVerified: true },\n * temporaryPassword: \"welcome123\",\n * };\n * },\n * }\n * ```\n *\n * @example Disable the reset-password entity action\n * ```ts\n * auth: {\n * enabled: true,\n * actions: { resetPassword: false },\n * }\n * ```\n *\n * @group Models\n */\nexport interface AuthCollectionConfig {\n /** Set to true to mark this collection as the authentication collection. */\n enabled: boolean;\n\n /**\n * Called when an admin creates a user via the collection REST API.\n *\n * Default: generate password → hash → normalize email → save →\n * send invitation email (or return temp password if no email configured).\n *\n * Override to implement custom invitation flows, LDAP sync, etc.\n */\n onCreateUser?: (\n values: Record<string, unknown>,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionCreateResult>;\n\n /**\n * Called when an admin resets a user's password via the admin panel.\n *\n * Default: generate reset token → send email (or generate + return temp password).\n * Override for custom reset flows.\n */\n onResetPassword?: (\n uid: string,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionResetResult>;\n\n /**\n * Control which auth-specific entity actions are auto-injected.\n *\n * Default: `{ resetPassword: true }` — the framework auto-injects\n * the built-in `resetPasswordAction` into the collection's entity actions.\n *\n * Set to `false` to disable, or pass a custom `EntityAction` to replace the UI.\n *\n * The object form is an `EntityAction` from `@rebasepro/cms-types`, typed\n * here as `object` because it is a React component with admin controllers in\n * its props and nothing on the server reads it — only whether the built-in\n * action is injected, which is the boolean.\n */\n actions?: {\n resetPassword?: boolean | object;\n };\n}\n\n/**\n * Context provided to collection-level auth hooks.\n *\n * This is a simplified facade over the server internals —\n * it exposes only what's needed for custom auth flows without\n * coupling collection config to internal interfaces.\n *\n * @group Models\n */\nexport interface AuthCollectionContext {\n /** Hash a password using the configured algorithm (scrypt by default). */\n hashPassword: (password: string) => Promise<string>;\n /**\n * Send an email. Only available when email service is configured.\n *\n * Resolves with what the provider reported — the assigned Message-ID, most\n * usefully — so a hook that sends a message can store the id and later\n * thread a reply back to it. Callers that do not care may ignore it.\n */\n sendEmail?: (options: { to: string; subject: string; html: string; text?: string }) => Promise<EmailSendResult>;\n /** Whether the email service is configured and available. */\n emailConfigured: boolean;\n /** The app name from email config (for templates). */\n appName: string;\n /** The base URL for password reset links. */\n resetPasswordUrl: string;\n}\n\n/**\n * Result of a collection-level `onCreateUser` hook.\n * @group Models\n */\nexport interface AuthCollectionCreateResult {\n /** Processed values to persist (must include passwordHash, NOT raw password). */\n values: Record<string, unknown>;\n /** If set, shown to the admin in the creation result dialog. */\n temporaryPassword?: string;\n /** Whether an invitation email was sent. */\n invitationSent?: boolean;\n}\n\n/**\n * Result of a collection-level `onResetPassword` hook.\n * @group Models\n */\nexport interface AuthCollectionResetResult {\n /** If set, shown to the admin. */\n temporaryPassword?: string;\n /** Whether a reset email was sent. */\n invitationSent?: boolean;\n}\n","/**\n * Opt-in full-text search configuration.\n *\n * ## Why this is opt-in\n *\n * Without a `search` block, `.search()` behaves exactly as it always has: an\n * `ILIKE '%term%'` OR-ed across the collection's top-level, non-enum `string`\n * properties. That default is unchanged and will stay unchanged — declaring\n * this block is the only way to get anything else.\n *\n * The default has three limits that no amount of tuning inside it can fix:\n * it cannot reach inside `map` (JSONB) or `array` properties, it has no notion\n * of relevance, and a leading `%` means it can never use an index. Collections\n * that outgrow those limits declare what they want searched; collections that\n * have not are left completely alone.\n *\n * ## What declaring it does\n *\n * One `tsvector` column, `GENERATED ALWAYS AS … STORED`, plus one GIN index on\n * it. Postgres recomputes the column on every write of a source field, so it\n * cannot drift from the row, and refuses any attempt to write it directly.\n * `.search()` then compiles to `@@ websearch_to_tsquery(…)` against that\n * column, which stems, drops stopwords, AND-es the terms, and ranks.\n *\n * These are stated consequences, not hidden ones: the column and the index\n * appear in generated DDL, in `schema.generated.ts`, and in `rebase db push`\n * output like any other declared object.\n *\n * @example\n * ```ts\n * const talents: PostgresCollectionConfig = {\n * slug: \"talents\",\n * table: \"talents\",\n * properties: { … },\n * search: {\n * language: \"spanish\",\n * unaccent: true,\n * fields: [\n * { path: \"full_name\", weight: \"A\" },\n * \"location\",\n * \"questionnaire.certifications\" // into the JSONB\n * ]\n * }\n * };\n * ```\n *\n * @group Search\n */\nexport interface SearchConfig {\n /**\n * The fields to index, in the author's own words. Nothing is inferred: a\n * field is searched if and only if it is named here.\n *\n * A bare string is shorthand for `{ path, weight: \"B\" }`.\n *\n * A path may address:\n * - a top-level `string` property — `\"full_name\"`\n * - a `string[]` property — `\"tags\"` (every element is indexed)\n * - a path into a `map` property — `\"questionnaire.certifications\"`,\n * which indexes every string found at or below that point, including\n * nested objects and arrays of strings. JSON *keys* are never indexed,\n * only values.\n *\n * A path that does not resolve to one of those is a boot-time error, not\n * a silent omission — a search field you believe is live and is not is the\n * failure this whole block exists to prevent.\n */\n fields: readonly (string | SearchField)[];\n\n /**\n * The Postgres text search configuration, which decides stemming and\n * stopwords. `\"spanish\"` stems `auditores` to `auditor` and drops `de`;\n * `\"simple\"` does neither.\n *\n * Defaults to `\"simple\"`, which is the only choice that is never wrong:\n * a stemmer applied to the wrong language silently mangles lexemes. Set it\n * to your content's language to get stemming.\n *\n * @default \"simple\"\n */\n language?: string;\n\n /**\n * Fold accents before indexing, so `auditoria` matches `auditoría`.\n *\n * This is not cosmetic in accented languages. Postgres stems the two\n * spellings to *different* lexemes — `to_tsvector('spanish', 'auditoría')`\n * yields `auditor` while `'auditoria'` yields `auditori` — so without this\n * a query typed without accents misses the rows that carry them, which is\n * most queries most users type.\n *\n * Requires the `unaccent` extension. Boot fails with an explicit message if\n * it is not installed and cannot be created, rather than quietly indexing\n * accented text as-is.\n *\n * @default false\n */\n unaccent?: boolean;\n\n /**\n * Name of the generated column holding the `tsvector`.\n *\n * Only change this if `search_vector` collides with a column you already\n * have. It is part of your schema once created: renaming it later is a\n * column drop and recreate, which rewrites the table.\n *\n * @default \"search_vector\"\n */\n column?: string;\n\n /**\n * Also match on trigram similarity, so near-misses and typos still rank —\n * `iso14000` reaching `ISO 14001`, which no amount of stemming will do\n * because they are simply different lexemes.\n *\n * Adds a second generated `text` column and a GIN trigram index alongside\n * the `tsvector`, and requires the `pg_trgm` extension. Costs write time\n * and disk; buys the single most common class of failed search.\n *\n * Also changes what `_score` means: the trigram similarity is added to\n * `ts_rank`. It has to be. A typo matches nothing on the exact path, so\n * every row this finds has a `ts_rank` of zero — ranking by that alone\n * would order the results arbitrarily, which is the failure `fuzzy` exists\n * to fix.\n *\n * @default false\n */\n fuzzy?: boolean;\n\n /**\n * Similarity floor for {@link SearchConfig.fuzzy}, between 0 and 1. A row\n * whose trigram similarity to the query falls below this never matches on\n * the fuzzy path (it can still match on the exact one).\n *\n * Lower admits more typos and more noise. Ignored unless `fuzzy` is set.\n *\n * @default 0.3\n */\n fuzzyThreshold?: number;\n}\n\n/**\n * One indexed field, with the weight it carries in the ranking.\n *\n * @group Search\n */\nexport interface SearchField {\n /**\n * Property name, or dotted path into a `map` property.\n * @see SearchConfig.fields\n */\n path: string;\n\n /**\n * Postgres weight class. `ts_rank` scores an `A` hit far above a `D` hit,\n * which is how a name outranks a passing mention in a long description.\n *\n * The four classes are Postgres's own and there are exactly four.\n *\n * @default \"B\"\n */\n weight?: SearchWeight;\n}\n\n/**\n * Postgres tsvector weight classes, strongest to weakest.\n *\n * @group Search\n */\nexport type SearchWeight = \"A\" | \"B\" | \"C\" | \"D\";\n\n/** The column name used when {@link SearchConfig.column} is not given. */\nexport const DEFAULT_SEARCH_COLUMN = \"search_vector\";\n\n/** The text search configuration used when {@link SearchConfig.language} is not given. */\nexport const DEFAULT_SEARCH_LANGUAGE = \"simple\";\n\n/** The weight a field carries when it does not name one. */\nexport const DEFAULT_SEARCH_WEIGHT: SearchWeight = \"B\";\n\n/** The similarity floor used when {@link SearchConfig.fuzzyThreshold} is not given. */\nexport const DEFAULT_FUZZY_THRESHOLD = 0.3;\n\n/**\n * Sort keys a query computes rather than reads from a column.\n *\n * `orderBy` is otherwise typed against the row — `keyof M` — which is exactly\n * right for a column and exactly wrong for relevance: `_score` is produced by\n * the query, so it appears in no generated row type and a project with a\n * generated SDK could not name it. The runtime accepted it, the docs told\n * people to use it, and the types rejected it.\n *\n * Kept as a named union rather than a loose `string` so the other half of the\n * guarantee survives: a typo'd column is still a compile error, and remains a\n * 400 at runtime rather than a silently unsorted list.\n *\n * `_distance` is deliberately not here. A vector search orders by distance on\n * its own and overrides `orderBy` outright, so naming it would imply a choice\n * the caller does not have.\n *\n * @group Search\n */\nexport type ComputedSortField = typeof RELEVANCE_SORT_FIELD;\n\n/**\n * The relevance sort key. Valid only on a collection that declares a\n * {@link SearchConfig} *and* on a query that carries a search string; anywhere\n * else it is an unknown field and the request is refused.\n */\nexport const RELEVANCE_SORT_FIELD = \"_score\";\n\n/**\n * One field that matched, and the text around the hit.\n *\n * Returned per row as `_matches` when a query asks for it — see the `explain`\n * option on `.search()`. Answers the question a ranked list otherwise leaves\n * open: *why is this row here?* A candidate surfacing for \"iso 14001\" because\n * of a certification is a different result from one surfacing because the\n * string appears in a paragraph about something else, and the score alone\n * cannot tell them apart.\n *\n * @group Search\n */\nexport interface SearchMatch {\n /**\n * The declared field path that matched, exactly as written in\n * {@link SearchConfig.fields} — e.g. `\"questionnaire.certifications\"`.\n * Map it to a label for display; the path is stable, a label is yours.\n */\n field: string;\n\n /**\n * The matching text, with each hit wrapped in `<mark>…</mark>`.\n *\n * Built by Postgres's `ts_headline` over the same normalized text that was\n * indexed. With {@link SearchConfig.unaccent} on that means the snippet\n * reads with accents folded — `Auditoria` rather than `Auditoría`. That is\n * deliberate: `ts_headline` over the *original* text cannot find a hit the\n * unaccented query produced, so it returns the text with nothing marked at\n * all. A readable snippet that highlights beats a prettier one that\n * silently does not.\n *\n * Contains markup by construction. Render it as HTML or strip the tags —\n * do not display it raw, and do not trust it as plain text.\n */\n snippet: string;\n}\n","import type { AnyCollectionConfig } from \"./collections\";\nimport type { Properties } from \"./properties\";\n\n/**\n * @group Models\n */\nexport type OnAction = \"cascade\" | \"restrict\" | \"no action\" | \"set null\" | \"set default\";\n\n/**\n * The key a junction row's own columns are carried under, in both directions.\n *\n * A read that includes a `manyToMany` relation serves each related row with its\n * link's columns nested here — `{ id: 5, name: \"ts\", _pivot: { role: \"owner\" } }`\n * — and a membership write may name the same key on an element to state what\n * the link should hold. One constant because the two have to be the same word:\n * a wire name that differs between the read and the write it round-trips\n * through is a shape no client can echo back.\n *\n * Leading underscore, like `_matches`: it reads as metadata about the row\n * rather than as one of its columns. A payload property may not be named\n * `_pivot` either — `checkJunctionPayload` refuses it — so the key means one\n * thing wherever it appears.\n *\n * @group Models\n */\nexport const JUNCTION_PIVOT_KEY = \"_pivot\";\n\n/**\n * What kind of link a relation is.\n *\n * The discriminant. Every other field a relation carries belongs to exactly one\n * of these, which is the point: a relation used to be a single open interface\n * where `cardinality`, `direction`, `localKey`, `foreignKeyOnTarget`, `through`\n * and `joinPath` were all optional and any combination typechecked. Which link\n * you meant then had to be *inferred* from which fields you happened to set,\n * and the inference was ~200 lines that guessed, fell back on naming\n * conventions, and swallowed its own failures.\n *\n * Most of that guessing produced bugs rather than convenience. A `many`\n * relation carrying a `localKey` — a combination the old type permitted — made\n * the write path stamp the parent's own foreign key onto the child row. Under\n * these kinds that state cannot be written down.\n *\n * @group Models\n */\nexport type RelationKind = \"belongsTo\" | \"hasOne\" | \"hasMany\" | \"manyToMany\" | \"via\";\n\n/** Fields every relation carries, whatever its kind. @group Models */\nexport interface RelationBase {\n /**\n * The name this link is addressed by: the key in `include`, the tab in the\n * admin panel, and the path segment of a nested URL.\n *\n * Defaults to the declaring property's key, or to the target's slug for an\n * entry in `relations`.\n */\n relationName?: string;\n\n /** The collection on the other end. */\n target: () => AnyCollectionConfig;\n\n /**\n * What the database does to this side's foreign key when the target row's\n * key changes. Emitted as the constraint's `ON UPDATE`.\n *\n * Unset means no clause, which Postgres reads as `NO ACTION`. Set\n * `\"cascade\"` when the target's key is a natural key that can be edited —\n * a slug, a SKU — so the pointers follow it.\n *\n * Only a `belongsTo` puts the key on this table, so this is the only kind\n * where the clause is written here; on the other kinds it describes the\n * constraint the target's own column carries.\n */\n onUpdate?: OnAction;\n /**\n * What the database does to this side's rows when the target row is\n * deleted. Emitted as the constraint's `ON DELETE`.\n *\n * Defaults, when unset, to `\"set null\"` for an optional relation and\n * **`\"restrict\"`** for a required one. `NOT NULL` says a child cannot exist\n * without a parent; it does not say deleting the parent should delete the\n * child. Ask for `\"cascade\"` when that is what you mean — it is the one\n * value that destroys rows you did not name.\n *\n * A `manyToMany` is the exception: its junction rows default to\n * `\"cascade\"`, because the row deleted there is the link and not the target.\n */\n onDelete?: OnAction;\n\n /**\n * Presentation overrides applied when this relation is rendered as a tab.\n *\n * Whether the link is *required* is not here: it is\n * `validation: { required: true }` on the declaring property, the same key\n * every other field uses. A relation carried its own copy until 0.18, and\n * the two disagreed by construction — the DDL generator read the property\n * (so the column was `NOT NULL`) while codegen read the relation (so the\n * generated `Insert` type made it optional), and a `create()` that\n * typechecked failed at the database.\n */\n overrides?: Partial<AnyCollectionConfig>;\n}\n\n/**\n * This collection holds the foreign key. One target row per source row.\n *\n * ```ts\n * author: { kind: \"belongsTo\", target: () => authors, localKey: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface BelongsToRelation extends RelationBase {\n kind: \"belongsTo\";\n /**\n * Column on **this** collection's table holding the target's key.\n * Defaults to `<relationName>_id`.\n */\n localKey?: string;\n}\n\n/**\n * The target holds the foreign key, and at most one target row points back.\n *\n * ```ts\n * profile: { kind: \"hasOne\", target: () => profiles, foreignKeyOnTarget: \"user_id\" }\n * ```\n * @group Models\n */\nexport interface HasOneRelation extends RelationBase {\n kind: \"hasOne\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * Set it when the two sides are joined on a natural key rather than on the\n * row id — an external identity id, a SKU, a tenant slug. See\n * {@link HasManyRelation.sourceKey}, which this mirrors.\n */\n sourceKey?: string;\n}\n\n/**\n * The target holds the foreign key, and many target rows point back. The\n * children belong to this parent alone — deleting one deletes a row.\n *\n * ```ts\n * posts: { kind: \"hasMany\", target: () => posts, foreignKeyOnTarget: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface HasManyRelation extends RelationBase {\n kind: \"hasMany\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * The mirror of `localKey` on {@link BelongsToRelation}: that one names the\n * column this side reads from, this one names the column the other side\n * points at. Without it the pair can only be joined on the row id, which\n * makes a natural-key link — `auth_user_id ↔ auth_user_id`, a SKU, a tenant\n * slug — inexpressible as `hasMany`, and it has to drop to the read-only\n * `via`.\n *\n * The column must be unique: the link addresses one source row per value,\n * and Postgres will not accept a foreign key against a non-unique column.\n *\n * ```ts\n * applications: {\n * kind: \"hasMany\",\n * target: () => talentApplications,\n * sourceKey: \"auth_user_id\",\n * foreignKeyOnTarget: \"auth_user_id\"\n * }\n * ```\n */\n sourceKey?: string;\n}\n\n/**\n * Both sides hold many, through a junction table. The target rows are shared,\n * so this collection owns the *link* and not the row: removing one removes a\n * junction row and leaves the target alone.\n *\n * Declared the same way from either side — there is no owning and inverse\n * version. Swap `sourceColumn` and `targetColumn` to describe the other\n * direction.\n *\n * ```ts\n * tags: { kind: \"manyToMany\", target: () => tags }\n * ```\n * @group Models\n */\nexport interface ManyToManyRelation extends RelationBase {\n kind: \"manyToMany\";\n /**\n * The junction table and its two key columns. Every part defaults: the\n * table to both table names sorted and joined, the columns to\n * `<collection>_id` and `<relationName>_id`.\n */\n through?: {\n table?: string;\n /** Junction column holding **this** collection's key. */\n sourceColumn?: string;\n /** Junction column holding the **target's** key. */\n targetColumn?: string;\n /**\n * Extra columns the junction row carries, declared exactly like a\n * collection's properties.\n *\n * A membership is often not only a membership. \"This user is in that\n * organisation\" is really \"…as an `owner`, since March\"; \"this tag is\n * on that post\" is really \"…in third place\". Until this existed the\n * junction was two key columns and nothing else, so the role and the\n * position had to become a collection of their own — which is a\n * different data model, a different set of policies and a different\n * URL, for what is still one link.\n *\n * The properties are read by the same planner that reads a\n * collection's, so a payload column gets the type, `NOT NULL`,\n * `DEFAULT`, `UNIQUE` and enum type it would get on a table. What it\n * does **not** get is `indexes` (declared per collection, and no\n * collection declares a junction), `search`, `vector`, or anything a\n * relation would put on it — a payload property may not be a\n * `relation`, a `reference` or a `vector`, and config validation\n * refuses one that is.\n *\n * On the wire the values travel under {@link JUNCTION_PIVOT_KEY}: a\n * read serves `{ …target, _pivot: { role } }`, and a membership write\n * accepts `{ id, _pivot: { role } }` beside the bare ids.\n *\n * ```ts\n * members: {\n * kind: \"manyToMany\",\n * target: () => users,\n * through: {\n * table: \"org_members\",\n * properties: {\n * role: { type: \"string\", enum: [\"owner\", \"admin\", \"member\"],\n * defaultValue: \"member\", validation: { required: true } },\n * joinedAt: { type: \"date\", autoValue: \"on_create\" }\n * }\n * }\n * }\n * ```\n *\n * Both sides of the same junction may declare it, and both must agree:\n * `resolveJunctionSpecs` refuses two declarations of the same payload\n * key that do not describe the same column, because only one of them\n * could ever be created.\n */\n properties?: Properties;\n };\n}\n\n/**\n * An explicit chain of joins, for links the four shapes above cannot express:\n * multi-hop paths, composite keys, or a join whose condition is not a plain\n * foreign key.\n *\n * Read-only. Rebase will not infer how to write through an arbitrary join\n * chain, and guessing is what this type exists to stop.\n *\n * ```ts\n * permissions: {\n * kind: \"via\",\n * target: () => permissions,\n * cardinality: \"many\",\n * joinPath: [\n * { table: \"user_roles\", on: { from: \"id\", to: \"user_id\" } },\n * { table: \"role_permissions\", on: { from: \"role_id\", to: \"role_id\" } },\n * { table: \"permissions\", on: { from: \"permission_id\", to: \"id\" } }\n * ]\n * }\n * ```\n * @group Models\n */\nexport interface ViaRelation extends RelationBase {\n kind: \"via\";\n /** Whether the chain yields one row or many. Cannot be derived from a join chain. */\n cardinality: \"one\" | \"many\";\n /**\n * The joins, in order, from this collection's table to the target's.\n *\n * Each step names a table and the columns to join it on; the last step's\n * table is the target. Read-only, because Rebase will not work out how to\n * write through an arbitrary chain, and guessing is what this kind exists to\n * stop.\n */\n joinPath: JoinStep[];\n}\n\n/**\n * A link from one collection to another, as authored.\n *\n * A closed union: pick the kind that describes the link and the type offers\n * exactly the fields that kind needs. See {@link ResolvedRelation} for the form\n * the runtime works with, which has every default filled in.\n *\n * @group Models\n */\nexport type Relation =\n | BelongsToRelation\n | HasOneRelation\n | HasManyRelation\n | ManyToManyRelation\n | ViaRelation;\n\n/**\n * A relation with every default filled in — the form the runtime works with.\n *\n * The authored {@link Relation} and this are deliberately different types.\n * They used to be one, which meant no reader could tell which fields had been\n * supplied and which had been guessed, and so every consumer re-derived what it\n * needed with its own chain of `if (through) … else if (localKey) …` fallbacks.\n * Those chains disagreed with each other; that disagreement is what produced\n * silently wrong reads and corrupt writes.\n *\n * Here each variant carries exactly its own fields, all required. A consumer\n * switches on `kind` and gets what it needs without a fallback, and the cases\n * it forgot are a compile error rather than a wrong answer at runtime.\n *\n * @group Models\n */\nexport type ResolvedRelation =\n | ResolvedBelongsTo\n | ResolvedHasOne\n | ResolvedHasMany\n | ResolvedManyToMany\n | ResolvedVia;\n\n/** Fields present on every resolved relation. @group Models */\nexport interface ResolvedRelationBase {\n /** Always set: defaulted during resolution if the author omitted it. */\n relationName: string;\n /**\n * The collection on the other end.\n *\n * Still a thunk — a relation between two collections that import each other\n * has to be — but normalised: resolution unwraps the module namespace the\n * author's `() => import(…)` may hand back, so every consumer gets the\n * config and not a `{ default: … }` wrapper.\n */\n target: () => AnyCollectionConfig;\n /** The target's slug, resolved once so consumers need not call `target()`. */\n targetSlug: string;\n /** As authored — see {@link RelationBase.onUpdate}. Defaults are not filled in. */\n onUpdate?: OnAction;\n /**\n * As authored — see {@link RelationBase.onDelete}. `undefined` here means\n * the author said nothing, and the DDL generator picks the default; it does\n * **not** mean \"no action\".\n */\n onDelete?: OnAction;\n /** Presentation overrides applied when this relation is rendered as a tab. */\n overrides?: Partial<AnyCollectionConfig>;\n /**\n * Whether one row or many come back. Derived from `kind` — kept because it\n * is what most consumers actually branch on, and because `via` is the one\n * kind where it is authored rather than implied.\n */\n cardinality: \"one\" | \"many\";\n /**\n * Whether Rebase knows how to write through this link. False only for\n * {@link ResolvedVia}, whose join chain it will not invent a write for.\n */\n writable: boolean;\n /**\n * Whether the target rows are shared with other parents. True for\n * many-to-many and for multi-hop `via`: what the parent owns is the link,\n * so removing one must not delete the row.\n */\n shared: boolean;\n}\n\n/** @group Models */\nexport interface ResolvedBelongsTo extends ResolvedRelationBase {\n kind: \"belongsTo\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on this collection's table. */\n localKey: string;\n}\n\n/** @group Models */\nexport interface ResolvedHasOne extends ResolvedRelationBase {\n kind: \"hasOne\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /** @see ResolvedHasMany.sourceKey */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedHasMany extends ResolvedRelationBase {\n kind: \"hasMany\";\n cardinality: \"many\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /**\n * Column on the source's table that `foreignKeyOnTarget` points at, or\n * `undefined` for the source's primary key.\n *\n * The one optional field on a resolved relation, and deliberately so. Every\n * other default is filled in here because it can be: a table name and a\n * column name are derivable from the relation and its two endpoints alone.\n * The primary key is not — this driver resolves it from `isId`, then the\n * Drizzle schema, then a column named `id`, and the middle tier does not\n * exist at resolution time.\n *\n * So `undefined` is a sentinel with exactly one meaning, not a field a\n * consumer is invited to guess at. Read it through `sourceKeyField()`,\n * which is the only place that turns it into a column name.\n */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedManyToMany extends ResolvedRelationBase {\n kind: \"manyToMany\";\n cardinality: \"many\";\n writable: true;\n shared: true;\n /**\n * The junction table and its two key columns, with every default filled in:\n * the table from both table names sorted and joined, the columns from each\n * endpoint's slug.\n */\n through: {\n table: string;\n sourceColumn: string;\n targetColumn: string;\n /**\n * The payload columns as authored, or `{}` when there are none —\n * never `undefined`, so a consumer reads one shape.\n * See {@link ManyToManyRelation.through}.\n */\n properties: Properties;\n };\n}\n\n/** @group Models */\nexport interface ResolvedVia extends ResolvedRelationBase {\n kind: \"via\";\n writable: false;\n /** The chain as authored — see {@link ViaRelation.joinPath}. Nothing to default. */\n joinPath: JoinStep[];\n}\n\n// ── Narrowing helpers ────────────────────────────────────────────────\n//\n// Consumers that only care about one axis — \"does this list many rows\",\n// \"is there a column on the target\" — should ask that question rather\n// than enumerate kinds, so adding a kind later does not silently skip them.\n\n/** Relations whose target row carries this collection's key. @group Models */\nexport type ResolvedForeignKeyOnTarget = ResolvedHasOne | ResolvedHasMany;\n\n/** @group Models */\nexport function hasForeignKeyOnTarget(relation: ResolvedRelation): relation is ResolvedForeignKeyOnTarget {\n return relation.kind === \"hasOne\" || relation.kind === \"hasMany\";\n}\n\n/** @group Models */\nexport function isManyToMany(relation: ResolvedRelation): relation is ResolvedManyToMany {\n return relation.kind === \"manyToMany\";\n}\n\n/** @group Models */\nexport function isToMany(relation: ResolvedRelation): boolean {\n return relation.cardinality === \"many\";\n}\n\n/**\n * Defines a single, explicit step in a multi-join path.\n *\n * Each step represents one JOIN operation in the sequence. The `from` columns\n * refer to the previous table in the chain (or the source table for the first step),\n * and the `to` columns refer to the current table being joined.\n *\n * @example Single column join:\n * ```typescript\n * {\n * table: \"authors\",\n * on: {\n * from: \"author_id\", // Column from previous table (e.g., posts.author_id)\n * to: \"id\" // Column from current table (authors.id)\n * }\n * }\n * ```\n *\n * @example Multi-column composite key join:\n * ```typescript\n * {\n * table: \"order_items\",\n * on: {\n * from: [\"order_id\", \"store_id\"], // Multiple columns from previous table\n * to: [\"order_id\", \"store_id\"] // Corresponding columns in current table\n * }\n * }\n * ```\n */\nexport interface JoinStep {\n /**\n * The database table name to join TO in this step.\n * This is the table you're joining into, not the table you're joining from.\n *\n * @example \"authors\", \"user_roles\", \"product_categories\"\n */\n table: string;\n\n /**\n * The join condition for this step. Defines how the previous table\n * connects to the current table.\n *\n * - `from`: Column name(s) on the PREVIOUS table in the join chain\n * - `to`: Column name(s) on the CURRENT table (specified in `table`)\n *\n * For the first step, `from` refers to the source collection's table.\n * For subsequent steps, `from` refers to the table from the previous step.\n *\n * Both `from` and `to` support:\n * - Single column: `\"user_id\"`\n * - Multiple columns: `[\"company_id\", \"region_id\"]` for composite keys\n *\n * When using arrays, both `from` and `to` must have the same length,\n * and columns are matched by position (index 0 with index 0, etc.).\n */\n on: {\n from: string | string[];\n to: string | string[];\n };\n}\n","/**\n * Structured, engine-agnostic policy expressions.\n *\n * A {@link PolicyExpression} is the single source of truth for a row-level\n * security condition. It is compiled to Postgres `USING`/`WITH CHECK` SQL\n * (authoritative enforcement) and independently evaluated in JavaScript (to\n * drive the admin UI, and — in future — to enforce on engines without native\n * RLS such as MongoDB). Because both the SQL and the JS decision derive from\n * the *same* expression, the UI matches database enforcement by construction —\n * no drift between two hand-written implementations.\n *\n * The only escape hatch that cannot be evaluated client-side is the\n * {@link RawPolicyExpression} node (`{ kind: \"raw\" }`): it preserves full\n * PostgreSQL power but, being arbitrary SQL, is treated as *unknown* by the\n * JavaScript evaluator (never silently allowed) and reflected exactly in the UI\n * via server-computed capability flags.\n *\n * @group Models\n */\nexport type PolicyExpression =\n | TruePolicyExpression\n | FalsePolicyExpression\n | AndPolicyExpression\n | OrPolicyExpression\n | NotPolicyExpression\n | ComparePolicyExpression\n | RolesOverlapPolicyExpression\n | RolesContainPolicyExpression\n | AuthenticatedPolicyExpression\n | RegisteredPolicyExpression\n | ServerContextPolicyExpression\n | ExistsInPolicyExpression\n | RawPolicyExpression;\n\n/**\n * The id a request without a logged-in user reports as `rebase.uid()`.\n *\n * A user-context request always sets `app.uid`: blank would read back as\n * `NULL`, and `NULL` is how the trusted server context is recognised, so an\n * anonymous visitor would be promoted to server privileges. The driver\n * therefore substitutes this sentinel at the single chokepoint where the GUC\n * is set.\n *\n * The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a\n * tautology on the user path** — it is true for anonymous visitors too. Use\n * {@link policy.authenticated} to mean \"signed in\", and\n * {@link policy.serverContext} to mean \"the trusted server context\". Do not\n * hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one\n * literal is not enough.\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_ID = \"anonymous\";\n\n/**\n * Every uid that has ever meant \"nobody is signed in\" — newest first.\n *\n * There are two because there were two. The types, the policy compiler, the\n * JavaScript evaluator and the linter were all built on\n * {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated\n * callers as `'anon'` — so `policy.authenticated()`, which compiled to\n * `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The\n * sanctioned way to write \"signed in\" granted to everyone, and the linter\n * flagged the spelling that actually worked as a foreign convention.\n *\n * The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here\n * because policies outlive the server that generated them: a database still\n * holding policies from before the fix, or a project whose server has not been\n * upgraded yet, must not become a grant in either direction. Compile against\n * this list, not against a single literal.\n *\n * No real user id is ever one of these, so a match is always \"not signed in\".\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_IDS: readonly string[] = [ANONYMOUS_USER_ID, \"anon\"];\n\n/**\n * Whether a uid stands for \"no one is signed in\", in any spelling rebase has\n * used. `null`/`undefined` is the trusted server context, not an anonymous\n * caller, and is therefore **not** anonymous — see {@link ANONYMOUS_USER_ID}.\n *\n * @group Models\n */\nexport function isAnonymousUid(uid: string | null | undefined): boolean {\n return typeof uid === \"string\" && ANONYMOUS_USER_IDS.includes(uid);\n}\n\n/** Always allows. Compiles to `true`. @group Models */\nexport interface TruePolicyExpression {\n kind: \"true\";\n}\n\n/** Always denies. Compiles to `false`. @group Models */\nexport interface FalsePolicyExpression {\n kind: \"false\";\n}\n\n/** Logical AND — every operand must pass. @group Models */\nexport interface AndPolicyExpression {\n kind: \"and\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical OR — at least one operand must pass. @group Models */\nexport interface OrPolicyExpression {\n kind: \"or\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical negation. @group Models */\nexport interface NotPolicyExpression {\n kind: \"not\";\n operand: PolicyExpression;\n}\n\n/** Comparison operators available to {@link ComparePolicyExpression}. @group Models */\nexport type PolicyCompareOperator = \"eq\" | \"neq\" | \"lt\" | \"lte\" | \"gt\" | \"gte\";\n\n/**\n * Compares two operands, e.g. `owner_id = rebase.uid()`.\n * @group Models\n */\nexport interface ComparePolicyExpression {\n kind: \"compare\";\n op: PolicyCompareOperator;\n left: PolicyOperand;\n right: PolicyOperand;\n}\n\n/**\n * True when the user holds *at least one* of the given application roles.\n * Compiles to `string_to_array(rebase.roles(), ',') && ARRAY[...]`.\n * @group Models\n */\nexport interface RolesOverlapPolicyExpression {\n kind: \"rolesOverlap\";\n roles: readonly string[];\n}\n\n/**\n * True when the user holds *all* of the given application roles.\n * Compiles to `string_to_array(rebase.roles(), ',') @> ARRAY[...]`.\n * @group Models\n */\nexport interface RolesContainPolicyExpression {\n kind: \"rolesContain\";\n roles: readonly string[];\n}\n\n/**\n * True when a signed-in user is making the request. Compiles to\n * `rebase.uid() IS NOT NULL AND rebase.uid() <> 'anonymous'`.\n *\n * Both halves are load-bearing. `IS NOT NULL` excludes the server context;\n * the {@link ANONYMOUS_USER_ID} comparison excludes anonymous visitors, who\n * *do* carry a non-null `rebase.uid()`. Checking only `IS NOT NULL` grants to\n * everyone — see {@link ANONYMOUS_USER_ID}.\n *\n * `policy.not(policy.authenticated())` therefore means \"anonymous visitor or\n * the server context\". To single out the server context, use\n * {@link ServerContextPolicyExpression}.\n * @group Models\n */\nexport interface AuthenticatedPolicyExpression {\n kind: \"authenticated\";\n}\n\n/**\n * True for a signed-in caller who has an ACCOUNT — not a guest.\n *\n * The distinction {@link AuthenticatedPolicyExpression} cannot make. Anonymous\n * SIGN-IN (`POST /auth/anonymous`) mints a real user row with a real uid and a\n * real session, so such a caller is \"authenticated\" by every test that looks at\n * `rebase.uid()`: same shape, same default role, indistinguishable inside a\n * policy. On a deployment with anonymous sign-in enabled, every rule meaning\n * \"a signed-in person\" therefore also meant \"anybody at all\", since pressing\n * Continue as guest needs no email, no password and no agreement to anything.\n *\n * Note the two senses of \"anonymous\", which is the reason this was easy to\n * miss. {@link ANONYMOUS_USER_ID} is the sentinel for a request carrying NO\n * session, and `authenticated()` already excludes it. A guest is the other\n * thing: a session with nobody behind it. This node excludes both.\n *\n * Compiles to `authenticated() AND NOT rebase.is_anonymous()`.\n *\n * Use it wherever a rule is about a person who could be held responsible for\n * something — writing a review, joining an organization, spending money. Use\n * `authenticated()` where a guest is genuinely welcome, which is what\n * anonymous sign-in is for: a cart before checkout, a draft before signup.\n * @group Models\n */\nexport interface RegisteredPolicyExpression {\n kind: \"registered\";\n}\n\n/**\n * True only in the trusted **server context** — the built-in flows that run\n * without a user (signup, migrations, `dataAsAdmin`) set no user GUC, so\n * `rebase.uid()` is `NULL` for them and only for them. Compiles to\n * `rebase.uid() IS NULL`.\n *\n * This is what lets the owner connection satisfy a policy even under FORCE RLS.\n * It is deliberately a primitive rather than `not(authenticated())`: the two\n * meant the same thing while `authenticated` ignored {@link ANONYMOUS_USER_ID},\n * and conflating them is what turns a server-only grant into an anonymous one.\n *\n * The JavaScript evaluator always returns `false` for this node — a client is\n * never the server context.\n * @group Models\n */\nexport interface ServerContextPolicyExpression {\n kind: \"serverContext\";\n}\n\n/**\n * Membership / relational access: true when at least one row exists in another\n * collection (a join/membership table) matching `where`. This is what lets you\n * scope reads to \"rows whose team the caller belongs to\" without an N+1\n * per-row lookup — it compiles to a single correlated `EXISTS` subquery.\n *\n * Inside `where`, {@link FieldPolicyOperand} (`policy.field`) references a column\n * of the joined collection, while {@link OuterFieldPolicyOperand}\n * (`policy.outerField`) references a column of the row being checked (the outer\n * table under RLS). Combine with {@link AuthUidPolicyOperand} to correlate to\n * the caller.\n *\n * @example\n * ```ts\n * // documents visible only to members of the document's team:\n * policy.existsIn({\n * collection: \"team_members\",\n * where: policy.and(\n * policy.compare(policy.field(\"team_id\"), \"eq\", policy.outerField(\"team_id\")),\n * policy.compare(policy.field(\"user_id\"), \"eq\", policy.authUid()),\n * ),\n * })\n * // → EXISTS (SELECT 1 FROM team_members _ex0\n * // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id = rebase.uid())\n * ```\n *\n * Postgres-authoritative: like {@link RawPolicyExpression}, the JavaScript\n * evaluator treats it as *unknown* (it cannot run a subquery client-side), so\n * enforcement is always the database's.\n * @group Models\n */\nexport interface ExistsInPolicyExpression {\n kind: \"existsIn\";\n /** Slug of the collection to search (the join / membership table). */\n collection: string;\n /** Condition evaluated against the joined collection's rows. */\n where: PolicyExpression;\n}\n\n/**\n * A raw PostgreSQL boolean expression — the full-power escape hatch.\n *\n * Columns can be referenced as `{column_name}`. This is Postgres-only and\n * **server-authoritative**: the JavaScript evaluator cannot evaluate arbitrary\n * SQL, so it treats this node as *unknown* rather than guessing.\n * @group Models\n */\nexport interface RawPolicyExpression {\n kind: \"raw\";\n sql: string;\n}\n\n/**\n * An operand referenced by a {@link ComparePolicyExpression}.\n * @group Models\n */\nexport type PolicyOperand =\n | FieldPolicyOperand\n | OuterFieldPolicyOperand\n | LiteralPolicyOperand\n | AuthUidPolicyOperand\n | AuthRolesPolicyOperand\n | AuthClaimPolicyOperand;\n\n/** A column value on the row being evaluated. @group Models */\nexport interface FieldPolicyOperand {\n kind: \"field\";\n /** The property/column name (resolved to its DB column when compiled). */\n name: string;\n}\n\n/**\n * A column value on the *outer* row when used inside {@link ExistsInPolicyExpression}\n * — i.e. the row the RLS policy is being evaluated for, referenced from within the\n * subquery. Outside an `existsIn` it is equivalent to {@link FieldPolicyOperand}.\n * @group Models\n */\nexport interface OuterFieldPolicyOperand {\n kind: \"outerField\";\n /** The property/column name on the outer collection. */\n name: string;\n}\n\n/** A constant value. @group Models */\nexport interface LiteralPolicyOperand {\n kind: \"literal\";\n value: string | number | boolean | null;\n}\n\n/** The current user's id — compiles to `rebase.uid()`. @group Models */\nexport interface AuthUidPolicyOperand {\n kind: \"authUid\";\n}\n\n/**\n * The current user's roles as an array — compiles to\n * `string_to_array(rebase.roles(), ',')`.\n * @group Models\n */\nexport interface AuthRolesPolicyOperand {\n kind: \"authRoles\";\n}\n\n/**\n * A named claim on the caller's session token — compiles to\n * `NULLIF(rebase.jwt() ->> '<name>', '')`.\n *\n * The operand multi-tenancy is built on, and the reason it is an operand rather\n * than a {@link RawPolicyExpression}: a claim arrives as **text**, and the\n * column it is compared against usually is not. `org_id = rebase.jwt() ->>\n * 'org_id'` on a `uuid` column is not a policy that denies — it is\n * `CREATE POLICY` failing with \"operator does not exist: uuid = text\", and a\n * table left with RLS enabled and no policy denies every row. Casting the\n * *column* to text instead compiles, but takes the index off the one predicate\n * that is ANDed into every read of the table.\n *\n * As an operand the compiler can see both sides: it casts the claim to the\n * column's type, guarded so a malformed claim denies rather than raising\n * `invalid input syntax` on every query, and the column keeps its index.\n *\n * An absent claim, and a claim set to the empty string, are both NULL — and a\n * comparison against NULL is never true, so a caller carrying no claim sees no\n * rows rather than all of them.\n *\n * Only *custom* claims are reachable. `uid`, `roles`, `aal` and `isAnonymous`\n * are identity claims written after the custom ones when a token is minted,\n * precisely so a claims hook cannot assert them; they have their own operands\n * ({@link AuthUidPolicyOperand}, {@link AuthRolesPolicyOperand}) and naming one\n * here is refused.\n *\n * Postgres-authoritative: the JavaScript evaluator reports *unknown* rather\n * than reproducing Postgres's cast semantics (uuid case folding, numeric\n * widening) a second time and getting them subtly wrong.\n * @group Models\n */\nexport interface AuthClaimPolicyOperand {\n kind: \"authClaim\";\n /** The claim's name on the token, e.g. `\"org_id\"`. */\n name: string;\n}\n\n// ── Constructor helpers ──────────────────────────────────────────────\n// Small, dependency-free builders so callers (and the desugaring in\n// `@rebasepro/common`) can assemble expressions without object-literal noise.\n\n/** @group Models */\nexport const policy = {\n true: (): TruePolicyExpression => ({ kind: \"true\" }),\n false: (): FalsePolicyExpression => ({ kind: \"false\" }),\n and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: \"and\",\noperands: operands as PolicyExpression[] }),\n or: (...operands: readonly PolicyExpression[]): OrPolicyExpression => ({ kind: \"or\",\noperands: operands as PolicyExpression[] }),\n not: (operand: PolicyExpression): NotPolicyExpression => ({ kind: \"not\",\noperand }),\n compare: (left: PolicyOperand, op: PolicyCompareOperator, right: PolicyOperand): ComparePolicyExpression =>\n ({ kind: \"compare\",\nop,\nleft,\nright }),\n rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: \"rolesOverlap\",\nroles: roles as string[] }),\n rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: \"rolesContain\",\nroles: roles as string[] }),\n authenticated: (): AuthenticatedPolicyExpression => ({ kind: \"authenticated\" }),\n registered: (): RegisteredPolicyExpression => ({ kind: \"registered\" }),\n serverContext: (): ServerContextPolicyExpression => ({ kind: \"serverContext\" }),\n existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>\n ({ kind: \"existsIn\",\ncollection: args.collection,\nwhere: args.where }),\n raw: (sql: string): RawPolicyExpression => ({ kind: \"raw\",\nsql }),\n field: (name: string): FieldPolicyOperand => ({ kind: \"field\",\nname }),\n outerField: (name: string): OuterFieldPolicyOperand => ({ kind: \"outerField\",\nname }),\n literal: (value: string | number | boolean | null): LiteralPolicyOperand => ({ kind: \"literal\",\nvalue }),\n authUid: (): AuthUidPolicyOperand => ({ kind: \"authUid\" }),\n authRoles: (): AuthRolesPolicyOperand => ({ kind: \"authRoles\" }),\n authClaim: (name: string): AuthClaimPolicyOperand => ({ kind: \"authClaim\",\nname })\n};\n","/**\n * The SQL helper functions RLS policies call, and the schema they live in.\n *\n * ## One schema, and it is ours\n *\n * Rebase creates exactly one schema in a project's database: `rebase`. These\n * three functions live in it alongside the framework's own tables, and that is\n * the whole contract — a reader can look at a database and know precisely which\n * namespace belongs to the framework and that nothing else was touched.\n *\n * It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called\n * `auth`, which is Supabase's name, chosen so that a developer who had written\n * Supabase RLS would recognise `auth.uid()`. The familiarity was real but the\n * name was not Rebase's to take, and taking it had a concrete cost: pointing\n * Rebase at a database that already had a Supabase `auth` schema meant\n * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's\n * `RETURNS uuid`, which Postgres rejects outright —\n *\n * ERROR: cannot change return type of existing function\n * HINT: Use DROP FUNCTION auth.uid() first.\n *\n * — and the failure landed inside a catch-all that logged a warning and carried\n * on, leaving a database with auth tables, no helper functions, and policies\n * calling functions that did not exist. Under `rebase db migrate` the same\n * statements aborted the migration instead.\n *\n * `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`\n * schema untouched and gains a `rebase` one, which is what a gradual migration\n * needs.\n *\n * ## Why functions at all, rather than inlining `current_setting`\n *\n * Because the indirection has already been spent once. `uid()` resolves\n * `app.uid` and falls back to the pre-rename `app.user_id`, so that during a\n * rolling deploy — old and new pods serving one database — both eras resolve\n * the principal. That was a single `CREATE OR REPLACE`. Inlined into policy\n * bodies it would have been a rewrite of every policy on every table.\n *\n * ## Why the name is not configurable\n *\n * A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so\n * these strings are written into every policy in every database Rebase has\n * provisioned. Everything that reads policies back — the SQL-to-policy parser\n * behind the admin UI, the drift checker, `rls-check` — would have to know the\n * configured value to recognise its own output. One frozen name is the feature.\n */\n\n/** The schema Rebase owns. The only schema Rebase creates. */\nexport const REBASE_SCHEMA = \"rebase\";\n\n/**\n * The principal of the current request, as text, or NULL in the server context.\n *\n * Never NULL for a user request — an anonymous one carries\n * {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for\n * the trusted server plane and `IS NOT NULL` a tautology.\n */\nexport const RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;\n\n/** The request's roles as a comma-separated string, for `string_to_array`. */\nexport const RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;\n\n/**\n * Whether the caller is a GUEST — signed in through anonymous sign-in rather\n * than with an account.\n *\n * A different question from {@link ANONYMOUS_USER_ID}, and the two are easy to\n * confuse: that sentinel means \"no session at all\", while this means \"a session\n * with nobody behind it\". Anonymous sign-in mints a real user row with a real\n * uid, so before this reached the database the two kinds of caller were one\n * principal inside every policy.\n */\nexport const RLS_IS_ANONYMOUS_SQL = `${REBASE_SCHEMA}.is_anonymous()`;\n\n/** The request's JWT claims as `jsonb`, or `{}`. */\nexport const RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;\n\n/**\n * The pre-1.0 spellings, for recognising policies and hand-written SQL that\n * predate the move.\n *\n * Kept because policies outlive the server that wrote them: a database migrated\n * by an older release still holds `auth.uid()` in its policy bodies until the\n * next push or boot recompiles them, and anything that reads policies back has\n * to recognise both eras or report the framework's own output as foreign drift.\n * Also used to give a project whose `securityRules` contain raw `auth.uid()` a\n * message naming the replacement, instead of a parse failure.\n */\nexport const LEGACY_RLS_SCHEMA = \"auth\";\nexport const LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;\nexport const LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;\nexport const LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;\n\n/**\n * Rewrites the pre-1.0 function calls in a fragment of policy SQL.\n *\n * Deliberately anchored on a word boundary and the schema qualifier, so a column\n * called `auth_uid` or a table named `auth` is left alone.\n */\nexport function rewriteLegacyRlsFunctions(sql: string): string {\n return sql.replace(\n /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/gi,\n (_match, fn: string) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`\n );\n}\n\n/** Whether a fragment of SQL still calls the pre-1.0 functions. */\nexport function usesLegacyRlsFunctions(sql: string): boolean {\n return /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/i.test(sql);\n}\n","/**\n * First-class multi-tenancy: one declaration, every layer.\n *\n * A tenant-scoped collection was expert work. It took four separate,\n * hand-written pieces that nothing checked against each other — a column, an\n * `existsIn` or raw RLS rule, a value stamped on every insert by a callback,\n * and an index somebody had to remember. Miss the index and the table scans;\n * miss the stamp and the row is invisible the moment it is written; miss the\n * rule and every tenant reads every other tenant's rows, which is the failure\n * nothing surfaces until it is a disclosure.\n *\n * {@link CollectionTenantConfig} is the one place that says \"this collection\n * belongs to a tenant\", and the four pieces are derived from it:\n *\n * - the column is `NOT NULL` and gets a btree index (`planSchema`);\n * - a **restrictive** RLS policy is injected for every operation, so it\n * composes with (rather than replaces) whatever `securityRules` the\n * collection declares — tenancy narrows, it never grants;\n * - the write path stamps the caller's tenant on create, refuses a write that\n * names another tenant, and refuses an update that moves a row between\n * tenants;\n * - the OpenAPI document marks the field so a generated client can see it.\n *\n * @see CollectionTenantConfig\n * @group Models\n */\n\n/**\n * The caller's tenant comes from a claim on their session token.\n *\n * The single-tenant-per-user shape: an identity provider (or Rebase's own\n * custom-claims hook) puts the organization on the token, and every request\n * carries it. Compiles to a comparison against `rebase.jwt() ->> '<claim>'`,\n * which is the same value a hand-written rule would read — so the generated\n * policy and anything an author writes beside it agree by construction.\n *\n * @group Models\n */\nexport interface TenantClaimSource {\n /**\n * The claim's name on the access token, e.g. `\"org_id\"`.\n *\n * Custom claims survive verification and reach RLS as `rebase.jwt()`; the\n * identity claims (`uid`, `roles`, `aal`, `isAnonymous`) are written after\n * them when a token is minted and cannot be shadowed, so naming one of\n * those here is refused rather than quietly reading the identity.\n */\n claim: string;\n}\n\n/**\n * The caller's tenants come from rows of a membership collection.\n *\n * The many-tenants-per-user shape — a `memberships` table with a user column\n * and a tenant column, which is how a person belongs to three organizations at\n * once. Compiles to a correlated `EXISTS` over that table (`policy.existsIn`),\n * so the database answers \"is the caller a member of this row's tenant?\" in the\n * same query rather than in an N+1 of lookups.\n *\n * Nothing is put on the token, so nothing has to be re-minted when somebody\n * joins or leaves a tenant — the next statement already sees the new row.\n *\n * @group Models\n */\nexport interface TenantMembershipSource {\n membership: {\n /** Slug of the collection holding the memberships. */\n collection: string;\n /** The property on it that holds the user id (compared to `rebase.uid()`). */\n userField: string;\n /** The property on it that holds the tenant id. */\n tenantField: string;\n };\n}\n\n/** Where the caller's tenant comes from. @group Models */\nexport type TenantSource = TenantClaimSource | TenantMembershipSource;\n\n/** Narrow a {@link TenantSource} to its claim form. @group Models */\nexport function isTenantClaimSource(source: TenantSource): source is TenantClaimSource {\n return typeof (source as TenantClaimSource).claim === \"string\";\n}\n\n/** Narrow a {@link TenantSource} to its membership form. @group Models */\nexport function isTenantMembershipSource(source: TenantSource): source is TenantMembershipSource {\n return typeof (source as TenantMembershipSource).membership === \"object\"\n && (source as TenantMembershipSource).membership !== null;\n}\n\n/**\n * The roles tenancy does not apply to, when the collection names none.\n *\n * `admin`, mirroring the security baseline every collection already carries\n * (`<table>_default_admin_read` / `_write`): the Studio, `dataAsAdmin` and a\n * support operator all run with it, and a tenancy rule that locked them out\n * would make the admin panel show an empty table on a collection full of rows.\n *\n * @group Models\n */\nexport const DEFAULT_TENANT_BYPASS_ROLES: readonly string[] = [\"admin\"];\n\n/**\n * Declare a collection tenant-scoped.\n *\n * ```ts\n * export const posts = buildCollection({\n * slug: \"posts\",\n * properties: {\n * orgId: { type: \"string\", validation: { required: true } },\n * title: { type: \"string\" }\n * },\n * tenant: { field: \"orgId\", from: { claim: \"org_id\" } }\n * });\n * ```\n *\n * The property has to exist — this says what a column *means*, it does not\n * conjure one into existence, exactly like `softDelete`. A config naming a\n * property the collection does not declare is refused at boot rather than at\n * the first read.\n *\n * ## What it composes with\n *\n * The injected policy is **restrictive**, so it is ANDed with every permissive\n * policy on the table: `securityRules`, `ownerField`, the injected admin\n * baseline. That is the only composition that is safe by construction — a\n * permissive tenancy policy would OR with the author's rules and a single\n * `access: \"public\"` rule would take the whole tenancy boundary off.\n *\n * Postgres-only. RLS is what enforces it, and an engine without row-level\n * security cannot be given this guarantee by an application-layer filter that\n * a raw query goes around.\n *\n * @group Models\n */\nexport interface CollectionTenantConfig<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * The property holding the tenant id.\n *\n * A `string` or `number` property, or a `reference` / `belongsTo` relation\n * to the tenants collection — in which case the foreign key the relation\n * already declares is the column, and no second one is created.\n *\n * The column is made `NOT NULL` and indexed: a nullable tenant column is a\n * row that belongs to nobody and is therefore invisible to everybody, and\n * an unindexed one turns every RLS-filtered read into a sequential scan.\n */\n field: Extract<keyof M, string> | string;\n\n /** Where the caller's tenant comes from. */\n from: TenantSource;\n\n /**\n * Roles that see and write across every tenant.\n *\n * Defaults to {@link DEFAULT_TENANT_BYPASS_ROLES}. An empty array means\n * \"nobody bypasses\" — the trusted server context still does, because it is\n * what runs migrations and the auth flows, and a policy that excluded it\n * would break the boot rather than protect a tenant.\n */\n bypassRoles?: readonly string[];\n}\n","import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { OrderByTuple } from \"./filter-operators\";\nimport type { LogicalCondition } from \"../controllers/data\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\nimport type { SchemaEditingAdmin } from \"./schema_editing\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /**\n * An `or(...)`/`and(...)` group, alongside `filter`.\n *\n * Counted as well as fetched, or `total` describes a different set of rows\n * from the one that was served — the same reason `filter` is here.\n */\n logical?: LogicalCondition;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * Declared here because a subscription is a query, and every field a query\n * has this one needs too. It was missing, so the type-checked boundary\n * dropped it: the client sent the group, nothing rejected it, and the\n * subscription re-fetched with the group gone — pushing every row the\n * caller's policies allowed rather than the ones they asked for. The same\n * defect `FetchCollectionProps.logical` documents, one layer up.\n */\n logical?: LogicalCondition;\n /**\n * Where the subscription's page starts. Missing for the same reason, with\n * a quieter symptom: a subscriber watching page two was pushed page one,\n * and a `collection_update` frame carries no window for it to notice with.\n */\n offset?: number;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched. */\n searchExplain?: boolean;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void\n ): void;\n\n /**\n * Subscribe to single entity changes\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `rebase.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>\n & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;\n\n/**\n * Type guard: can this admin plan a live schema change?\n *\n * Planning is engine-specific — it renders DDL, a Drizzle schema and the\n * declarative SQL artifacts — so the implementation lives in the driver\n * package. The server detects the capability structurally, exactly as it does\n * for SQL, rather than importing an engine it is supposed to know nothing\n * about.\n *\n * @group Admin\n */\nexport function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {\n return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === \"function\";\n}\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Ask the database whether it is there, before anything else touches it.\n *\n * Boot's first database call is not `initializeDriver` — it is the schema\n * provisioning that runs ahead of it, and a driver's connection diagnosis\n * therefore never got the chance to run. A stopped database produced\n * `Failed query: [redacted]` and a stack through drizzle internals: no host,\n * no port, no `ECONNREFUSED`, and no hint about starting the thing.\n *\n * Implementations MUST issue the cheapest round trip they have (`SELECT 1`),\n * MUST throw an error whose message names the host, the port and the\n * driver's own reason, and MAY log a fuller diagnosis first. They MUST NOT\n * throw for a reachable database that merely answered something unexpected —\n * the caller treats a throw as fatal.\n *\n * `driverResult` is optional for the same reason as\n * {@link ensureCollectionSchema}: this runs before `initializeDriver`, so an\n * adapter that was constructed with its own connection has to fall back to\n * it.\n */\n verifyConnection?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n *\n * `driverResult` is optional: this runs before `initializeDriver`, and only\n * the bundle path has a pre-init stand-in to pass. An adapter built by an\n * application already holds its own connection and MUST use it when this is\n * `undefined` — dereferencing it unconditionally works for managed tenants\n * and breaks every app that builds its own adapter.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Apply the collections' row-level-security policies, additively and\n * idempotently — the companion to {@link ensureCollectionSchema}.\n *\n * That method creates the tables; a table with RLS disabled and no policies\n * is not servable, because authenticated requests run as a restricted role:\n * a read with no `SELECT` policy returns nothing (a public collection\n * answers 401) and a write with no `INSERT`/`UPDATE` policy is denied. The\n * `db push` CLI applies these from the same collections, but it cannot reach\n * a managed tenant's in-cluster database — the runtime, already connected,\n * is the only thing that can.\n *\n * MUST be idempotent (re-run on every boot) and MUST NOT be destructive.\n * Runs after auth initialization, because the generated policies call the\n * `auth.*` helper functions and `CREATE POLICY` validates they exist.\n */\n ensureCollectionPolicies?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Create the RLS helper functions on this source's database. See\n * `DatabaseAdapter.ensureRlsRuntime`; needed on every source that is not\n * the default, whose helpers arrive with the auth tables.\n */\n ensureRlsRuntime?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Re-check, after the schema exists, that requests will actually be\n * constrained by the database's own authorization.\n *\n * A driver that isolates user requests by switching to a restricted role has\n * to decide at connect time whether the switch is needed — and on a fresh\n * database that question is asked before there is anything to answer with.\n * The process then creates the schema, becomes its owner, and an owner is\n * exempt from the policies on what it owns. So the answer that was true when\n * the driver initialized can be false by the time it serves a request.\n *\n * This is where a driver asks again. It runs once, after collection tables,\n * auth tables and policies are all in place, and it MUST fail rather than\n * serve when the answer changed and cannot be acted on: booting anyway\n * produces exactly the unenforced server this exists to prevent.\n *\n * Optional, because it is only meaningful for drivers whose isolation\n * depends on state the schema affects. A driver with nothing to re-check\n * omits it.\n */\n finalizeSecurityPosture?(driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Read the collections schema version this database was last provisioned\n * from, or `null` when nothing has ever stamped it.\n *\n * The companion to {@link stampCollectionsSchemaVersion}: one process writes\n * what it applied, every other process compares itself to it. This is what\n * lets a split deployment — several processes over one database, only one of\n * which provisions — notice that a unit is serving against a schema it was\n * not built for. That failure is otherwise silent in both directions: a\n * column that does not exist is a SQL error on one route, and a policy that\n * was never applied is a 200 with no rows.\n *\n * `null` is not an error and MUST NOT be treated as one — every database\n * provisioned before the stamp existed reads this way, and so does every\n * fresh one until its first provisioning boot finishes.\n */\n readCollectionsSchemaVersion?(\n driverResult?: InitializedDriver\n ): Promise<string | null>;\n\n /**\n * Record the collections schema version this process just applied.\n *\n * Called only by the process that provisions, and only after both\n * {@link ensureCollectionSchema} and {@link ensureCollectionPolicies} have\n * run — a stamp written before the policies would claim a schema that is\n * only half in place, and the half that is missing is the one that fails\n * without an error.\n */\n stampCollectionsSchemaVersion?(\n version: string,\n driverResult?: InitializedDriver\n ): Promise<void>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Role management service (optional, roles are now simple strings). */\n roleService?: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n","/**\n * The vocabulary a live schema change is described in.\n *\n * Declared here, and nowhere else, because two packages that must not import\n * each other both need it: `@rebasepro/server-postgres` decides what a change\n * means and renders the files it needs, while `@rebasepro/server` commits those\n * files and serves the routes. Neither can reach the other — the server is\n * engine-agnostic by design — so the shared kernel holds the shapes and the\n * driver is detected structurally through {@link SchemaEditingAdmin}.\n *\n * Nothing here executes anything. These are the nouns.\n */\n\n/**\n * What a change will do to a live database.\n *\n * - `safe` — the boot-time ensure path expresses it, and the result matches the\n * configuration.\n * - `diverges` — the ensure path applies *something*, but the database will not\n * match what the configuration declares, and nothing reports it. This is the\n * category worth having: adding a required property to a populated table\n * yields a nullable column, and adding a value to an existing enum yields\n * nothing at all. Both read as success.\n * - `needs-migration` — the ensure path cannot express it. Dropping anything,\n * changing a type, moving a primary key.\n */\nexport type SchemaChangeVerdict = \"safe\" | \"diverges\" | \"needs-migration\";\n\nexport type SchemaChangeKind =\n | \"add-collection\"\n | \"remove-collection\"\n | \"add-property\"\n | \"remove-property\"\n | \"change-property-type\"\n | \"rename-column\"\n | \"add-enum-value\"\n | \"remove-enum-value\"\n | \"change-required\"\n | \"change-primary-key\";\n\nexport interface SchemaChange {\n kind: SchemaChangeKind;\n verdict: SchemaChangeVerdict;\n /** Collection slug. */\n collection: string;\n /** Property name, where the change is to one. */\n property?: string;\n /** One line, specific: what changed and what it will do. */\n detail: string;\n /** What to do instead, when the verdict is not `safe`. */\n remedy?: string;\n}\n\nexport interface ClassifiedSchemaChanges {\n changes: SchemaChange[];\n /** The worst verdict present, or `safe` for an empty diff. */\n verdict: SchemaChangeVerdict;\n /** True only when every change is `safe` — the one case an editor may apply. */\n applicable: boolean;\n}\n\n/**\n * Where a project's generated schema artifacts live, relative to the **project**\n * root — which is the repository root only when the project is the whole\n * repository.\n *\n * Here rather than in the Postgres package because it is a contract, not an\n * engine detail: `@rebasepro/server` has to derive these for a project in a\n * subdirectory, and it cannot import a driver to do it.\n */\nexport interface SchemaCommitPaths {\n /** Drizzle schema, imported by the backend. */\n schemaFile: string;\n /** Declarative DDL, what `db push` applies and Atlas diffs against. */\n ddlFile: string;\n policiesFile: string;\n searchFile: string;\n /** Vector columns and ANN indexes — like search, applied by Rebase not Atlas. */\n vectorFile: string;\n /**\n * `autoValue: \"on_update\"` triggers and the function they share. Atlas's\n * free tier will not parse a desired state containing a function, so this\n * is applied by Rebase like search and vector.\n */\n triggersFile: string;\n}\n\nexport const DEFAULT_COMMIT_PATHS: SchemaCommitPaths = {\n schemaFile: \"backend/src/schema.generated.ts\",\n ddlFile: \"drizzle/schema.sql\",\n policiesFile: \"drizzle/policies.sql\",\n searchFile: \"drizzle/search.sql\",\n vectorFile: \"drizzle/vector.sql\",\n triggersFile: \"drizzle/triggers.sql\"\n};\n\n/** One file the commit writes, as content rather than as a path on a disk. */\nexport interface SchemaChangeFile {\n path: string;\n contents: string;\n}\n\n/**\n * Everything a change needs written and run.\n *\n * Computed without touching a disk or a network. The database is *read* — what\n * a change means depends on what is already there, and a plan that guessed\n * would be guessing about whether the statements it returns will be accepted.\n */\nexport interface SchemaChangePlan {\n /** Every file the commit writes — collection source and generated artifacts. */\n files: SchemaChangeFile[];\n /** The additive DDL this change adds, in dependency order. */\n statements: string[];\n classified: ClassifiedSchemaChanges;\n /** A commit message describing the change rather than announcing one. */\n message: string;\n /**\n * Constraints the configuration asks for that these statements do not\n * carry, and why.\n *\n * Almost always empty. When it is not, it is the part the person confirming\n * needs to read: the change will apply, and the database will still not\n * enforce something the configuration says — a required property over a\n * table that already holds rows with no value for it. Optional so a plan\n * from an engine that does not distinguish these cases stays valid.\n */\n withheldConstraints?: WithheldSchemaConstraint[];\n}\n\n/** A constraint a plan asks for and does not apply. */\nexport interface WithheldSchemaConstraint {\n /** `schema.table.column`. */\n target: string;\n kind: \"not-null\";\n /** What is in the way, naming the obstacle rather than the rule. */\n reason: string;\n /** What would make it applicable. */\n remedy: string;\n}\n\n/**\n * An admin that can plan a schema change.\n *\n * Planning only. Applying is `executeSql`, which every SQL admin already has,\n * and committing belongs to whatever holds the repository — keeping those three\n * apart is what lets the same plan be committed locally on a developer's machine\n * and through a GitHub App from a cloud tenant.\n *\n * @group Admin\n */\nexport interface SchemaEditingAdmin {\n /**\n * Decide what the change means and render everything it needs.\n *\n * Rejects when the change is not applicable, carrying the classification so\n * a caller can say which change was the problem.\n */\n planSchemaChange(\n before: unknown[],\n after: unknown[],\n options?: { paths?: Partial<SchemaCommitPaths> }\n ): Promise<SchemaChangePlan>;\n}\n","/**\n * The cross-instance transport for channel broadcast and presence, and the\n * contract anyone implementing one has to meet.\n *\n * These types live in `@rebasepro/types` rather than in the Postgres adapter on\n * purpose: a transport package should depend on the contract, not on the\n * database driver that happens to ship the default implementation. A\n * `@rebasepro/channel-bus-<something>` package needs this file and nothing else.\n *\n * Why a transport exists at all: entity/collection realtime already spans\n * instances (CDC, or per-mutation LISTEN/NOTIFY). Channel broadcast and presence\n * did not — they fanned out from per-process maps, so two clients served by\n * different replicas could not see each other, and nothing errored. The bus is\n * the missing hop, and deliberately *only* that hop: which local clients receive\n * a frame stays in the realtime service, so a transport never has to know what a\n * subscription, a WebSocket or a presence roster is.\n */\n\n/**\n * A frame in flight between instances.\n *\n * `sid` identifies the publishing instance. The realtime service drops frames\n * carrying its own `sid` on arrival — local fan-out already happened before the\n * publish — so a transport that echoes a publisher's own messages back to it is\n * still correct, merely wasteful.\n *\n * Keys are spelled out rather than abbreviated. The one shipped transport with a\n * size limit has a pointer path for anything that would approach it, so shaving\n * bytes off key names buys nothing worth the opacity.\n */\nexport type ChannelBusFrame =\n /** A broadcast carrying its payload. */\n | {\n kind: \"broadcast\";\n sid: string;\n channel: string;\n event: string;\n /** Originating client, echoed so receivers can skip it if it is theirs. */\n from?: string;\n /** Sequence number, present only on retained channels. */\n seq?: number;\n payload: unknown;\n }\n /**\n * A broadcast too large for the transport to carry inline: the body is\n * already durable in `rebase.channel_messages`, so the frame carries only\n * its address and each receiver reads it back. Only ever emitted for\n * retained channels, and only by a transport with a finite\n * {@link ChannelBus.maxFrameBytes}.\n */\n | {\n kind: \"broadcast_ref\";\n sid: string;\n channel: string;\n from?: string;\n seq: number;\n }\n /** A presence join/leave/update, small by construction. */\n | {\n kind: \"presence_diff\";\n sid: string;\n channel: string;\n joins: Record<string, Record<string, unknown>>;\n leaves: Record<string, Record<string, unknown>>;\n };\n\n/** Receives frames published by *other* instances. */\nexport type ChannelBusHandler = (frame: ChannelBusFrame) => void | Promise<void>;\n\n/**\n * A cross-instance transport.\n *\n * ## What an implementation must guarantee\n *\n * - **`start()` rejects if the transport is unusable.** The caller falls back to\n * in-process delivery when it does. Resolving while disconnected produces a\n * cluster that believes it is connected and silently is not, which is the\n * exact failure this whole mechanism exists to remove.\n * - **`publish()` reaches every *other* instance, or rejects.** Delivery back to\n * the publisher is permitted but pointless (see {@link ChannelBusFrame.sid}).\n * - **`stop()` is idempotent** and releases everything, including anything\n * holding the event loop open.\n * - **A malformed message never throws out of the transport.** Parsing happens\n * inside the implementation; drop and log what you cannot understand, so one\n * bad frame cannot take the listener down.\n *\n * ## What it does *not* have to guarantee\n *\n * - **Ordering.** Retained channels carry `seq`, and the client SDK orders by\n * it. Unsequenced broadcasts are cursor-grade traffic where order is not\n * meaningful.\n * - **Durability.** A frame lost in transit is a missed live update; retained\n * channels repair themselves through the client's `channel_history` replay.\n * - **Exactly-once.** Duplicates are tolerated — retained frames are deduped by\n * `seq`, and presence diffs are idempotent by construction.\n */\nexport interface ChannelBus {\n /**\n * Identifies the transport in logs and in `getChannelBusKind()`. Use your\n * own name; the framework only compares against `\"memory\"` to decide\n * whether publishing is worth attempting at all.\n */\n readonly kind: string;\n\n /**\n * Largest frame this transport will carry, in bytes of encoded JSON, or\n * `Infinity` when there is no meaningful ceiling.\n *\n * A broadcast that exceeds it is published as a `broadcast_ref` pointer when\n * the channel is retained, and refused with an error to the sender when it\n * is not. Implementations with no limit should return `Infinity` rather than\n * a large number, so the pointer path is never taken needlessly.\n */\n readonly maxFrameBytes: number;\n\n /** Connect and begin delivering remote frames to `handler`. */\n start(handler: ChannelBusHandler): Promise<void>;\n\n /** Publish a frame to the other instances. */\n publish(frame: ChannelBusFrame): Promise<void>;\n\n /** Disconnect and release resources. Idempotent. */\n stop(): Promise<void>;\n}\n\n/**\n * Which transport to use, for the two that ship with the Postgres adapter.\n *\n * To use one that does not ship here — a Redis package, or your own class —\n * pass the {@link ChannelBus} instance itself instead of a config object.\n *\n * There are deliberately only two built in, and neither adds a service to a\n * deployment. Rebase deploys as Postgres + backend + frontend; a bus that\n * required a message broker would put a second stateful service into every\n * `docker-compose.yml` the CLI scaffolds, for a feature most applications never\n * use. Measured across two backend instances against one Postgres container,\n * the Postgres bus carried ~10k cross-instance messages/second with no losses,\n * and stayed flat out to eight instances — comfortably past what live-cursor\n * collaboration generates. The extension point below is the answer for anyone\n * who does outgrow it.\n */\nexport type ChannelBusConfig =\n /**\n * In-process only — the historical behaviour. Broadcast and presence reach\n * the clients connected to *this* instance and no further.\n */\n | { type: \"memory\" }\n /**\n * Postgres LISTEN/NOTIFY, reusing infrastructure the deployment already has.\n *\n * `pg_notify` caps a payload at 8000 bytes, so a broadcast larger than that\n * is delivered cross-instance only on a *retained* channel, where the\n * notification carries a pointer (`seq`) instead of the message and each\n * receiver reads the body back from `rebase.channel_messages`. An oversized\n * broadcast on an ephemeral channel is refused rather than silently\n * delivered to half the cluster.\n *\n * NOTE: `LISTEN` needs a session-mode connection. Behind PgBouncer in\n * transaction mode this must point at the database directly\n * (`DATABASE_DIRECT_URL`), not at the pooler.\n */\n | {\n type: \"postgres\";\n /** Direct connection for the LISTEN client. Defaults to `DATABASE_DIRECT_URL`. */\n connectionString?: string;\n /**\n * How long to coalesce outgoing frames into a single notification, in\n * milliseconds. Defaults to 10.\n *\n * A notify is a query on your primary database, so under load this is\n * the difference between one query per message and one per window. The\n * window is leading-edge: a frame arriving when none is open goes out\n * immediately, so an idle channel pays no added latency and only a\n * sustained stream is batched.\n *\n * Set to 0 to disable coalescing and send every frame on its own.\n */\n batchWindowMs?: number;\n };\n\n/**\n * What `realtime.bus` accepts: a built-in transport by name, or any\n * {@link ChannelBus} instance.\n *\n * ```typescript\n * realtime: { bus: { type: \"postgres\" } } // shipped\n * realtime: { bus: new MyRedisChannelBus(url) } // a separate package, or your own\n * ```\n */\nexport type ChannelBusSetting = ChannelBusConfig | ChannelBus;\n\n/**\n * Whether `setting` is an already-constructed transport rather than a request\n * for a built-in one.\n *\n * Structural rather than nominal so that an instance from a *different copy* of\n * `@rebasepro/types` — an entirely normal outcome of a separately versioned\n * transport package — is still recognised.\n */\nexport function isChannelBusInstance(setting: ChannelBusSetting | undefined): setting is ChannelBus {\n return typeof (setting as ChannelBus | undefined)?.publish === \"function\";\n}\n","/**\n * The resource graph: one declaration site for every named thing a project needs.\n *\n * ## The rule\n *\n * **Every named resource is declared with a constructor in config code.** A\n * database, a bucket, a topic and whatever kind comes next are all spelled the\n * same way, so \"where do I declare my second one\" has one answer instead of one\n * answer per kind.\n *\n * ```ts\n * export const main = database(\"main\");\n * export const media = bucket(\"media\", { transport: \"direct\" });\n * export const signups = topic<SignupEvent>(\"signups\");\n * ```\n *\n * ## Declaration is not binding\n *\n * A declaration says a resource *exists* and what shape it has. It never says\n * how to reach it — that is a property of the environment, not of the project,\n * and it differs between a laptop, a self-hosted box and a tenant in the cloud.\n * Binding lives in `@rebasepro/server`'s boot path, where each kind registers\n * the resolver that reads its environment variables, keyed off the logical\n * name declared here.\n *\n * This split is the whole point. Before it, storage topology was hand-written\n * into `rebase.json` while database topology lived in TypeScript, and the\n * boundary between them was a fact about what the control plane could read\n * before a build — a platform implementation detail that a developer had no way\n * to derive. Worse, storage could be declared in *both* places, and the merge\n * silently kept the JSON's engine and discarded the code's.\n *\n * ## Why a registry rather than a fixed union\n *\n * Kinds register themselves. Adding pub/sub, a cache or a search index must not\n * require editing a manifest schema, a validator and three switch statements —\n * that cost is exactly why the last two kinds ended up in different homes.\n */\n\n/** How a client reaches a resource. */\nexport type ResourceTransport =\n /** Through the backend. The default, and the only one that needs no client SDK. */\n | \"server\"\n /** A provider SDK talks to the resource directly; the backend is not in the path. */\n | \"direct\";\n\n/**\n * A resource kind, as registered.\n *\n * `engines` is an allowlist rather than documentation. An unrecognised engine\n * used to be a free string that passed every check and failed later, further\n * from the typo that caused it — `\"s2\"` for `\"s3\"` reached the runtime. Anything\n * genuinely outside the list is spelled `custom:<id>`, which says so at the call\n * site instead of looking like a typo.\n */\nexport interface ResourceKindSpec {\n /** The kind's name, as it appears in a declaration and in the graph. */\n kind: string;\n /**\n * Which definition of this kind this is. Bump it whenever anything else in\n * the spec changes.\n *\n * Two copies of this package can meet in one process — a published driver\n * inlines it into its dist, and the runtime image ships its own — and the\n * registry is shared between them on purpose. Without a revision the only\n * thing the registry can do with two specs that differ is refuse, and a\n * refusal at driver load is a pod that never boots: every bundle built with\n * a driver older than the change dies on the first image that carries it.\n * With one, the higher revision wins whichever copy loads first, and the\n * older copy is told so. Missing means 0, which is what every copy shipped\n * before revisions existed reports.\n *\n * Only copies that know about revisions honour them. A copy published\n * BEFORE they existed still compares the whole literal and throws, so a\n * kind that has shipped in such a copy cannot change its literal at all —\n * not even to add this field. Correct those kinds with `amendResourceKind`.\n */\n revision?: number;\n /** Engines this kind ships with. `custom:<id>` is always additionally valid. */\n engines: readonly string[];\n /** Used when a declaration names none. */\n defaultEngine: string;\n /**\n * Environment variable base names this kind binds from, in the order a\n * binder should try them. A resource keyed `analytics` reads\n * `<BASE>__ANALYTICS`; the default-keyed resource reads `<BASE>` unsuffixed,\n * so a single-resource project configured the obvious way declares nothing.\n */\n envBases: readonly string[];\n /**\n * The subset of `envBases` that matters for a given engine.\n *\n * The binder reads every base and takes whichever is set — harmless, and it\n * keeps binding tolerant. A GENERATOR cannot be that relaxed: `rebase eject\n * infra` writing S3_BUCKET, GCS_BUCKET, STORAGE_BUCKET and\n * STORAGE_PUBLIC_URL for a `local` bucket hands somebody four variables of\n * which three are noise, and a config file full of irrelevant keys is one\n * nobody reads carefully.\n *\n * Keyed by engine; an engine with no entry falls back to all of them, which\n * is the honest answer for one this package has never heard of.\n */\n envBasesByEngine?: Readonly<Record<string, readonly string[]>>;\n /** Option keys this kind accepts beyond the common ones, for validation. */\n optionKeys?: readonly string[];\n /**\n * Whether a project implicitly has one of these even when it declares\n * nothing. True for databases — a backend without one is not a backend —\n * and false for topics, where zero is the normal number.\n */\n implicitDefault?: boolean;\n}\n\n/** The key a resource takes when a project declares only one of its kind. */\nexport const DEFAULT_RESOURCE_KEY = \"(default)\";\n\n/** A declared resource, as it appears in the graph. */\nexport interface ResourceDeclaration {\n kind: string;\n /** Unique within its kind. What a binder looks up and what an env suffix is built from. */\n key: string;\n engine: string;\n transport: ResourceTransport;\n label?: string;\n /** Kind-specific options, validated against the kind's `optionKeys`. */\n options: Readonly<Record<string, unknown>>;\n /**\n * What in the project reaches this resource, as `<what>:<name>` — a\n * `collection:posts` routed to a database, a `property:posts.cover` stored\n * in a bucket, a `function:report` importing a handle.\n *\n * Recorded by the derive step, never by a constructor: a declaration says\n * a resource exists, and only a reader that has evaluated the rest of the\n * project can say who uses it. It is the map a host needs to split a\n * monolith into units later, and the map a console needs to answer \"what\n * breaks if I remove this\". Absent when nothing was recorded, which is\n * different from an empty list.\n */\n usedBy?: readonly string[];\n}\n\n/**\n * The value a constructor returns.\n *\n * Carries its own declaration so config code can hold it and pass it around,\n * and stringifies to its key so it drops into the places that still take one.\n * Collections name a data source by string today; a handle works there without\n * the collection API having to change, which keeps this a config redesign\n * rather than a rewrite of the data layer.\n */\nexport interface ResourceHandle extends ResourceDeclaration {\n toString(): string;\n}\n\nconst BRAND = Symbol.for(\"@rebasepro/types.resource\");\n\n/** Whether a value is a resource handle rather than a plain string key. */\nexport function isResourceHandle(value: unknown): value is ResourceHandle {\n return typeof value === \"object\" && value !== null && BRAND in value;\n}\n\n/**\n * A reference to a resource where a key is expected: the handle a constructor\n * returned, or the key spelled as a string.\n *\n * The handle is the point. `dataSource: analytics` is the same name spelled\n * once — rename the export and every use follows, jump-to-definition lands on\n * the declaration, and the derive step can record who uses what. The string\n * form stays because a key has to survive serialisation: the runtime and the\n * admin UI read collections as plain data, where a handle cannot travel.\n */\nexport type ResourceRef = string | ResourceHandle;\n\n/** The key a resource reference names, whether it is a handle or already a key. */\nexport function resourceKeyOf(ref: ResourceRef): string {\n return isResourceHandle(ref) ? ref.key : ref;\n}\n\n/**\n * Replace every resource handle inside a value with its key, deeply.\n *\n * Applied where authored config becomes data: `defineCollection`, the\n * collection loaders, the derive step. Past that point a collection is plain\n * data that serialises, compares with `===` and reaches the admin UI over the\n * wire, so a handle must not survive into it. Plain objects and arrays are\n * walked; anything else — a function, a Date, a class instance — is a leaf and\n * is returned as it is, which is what keeps callbacks and validators intact.\n */\nexport function resolveResourceRefs<T>(value: T): T {\n if (isResourceHandle(value)) return value.key as T;\n // Identity-preserving: a value with no handle inside comes back as the\n // same object, not a copy. Collections point at each other through\n // `target: () => authors`, and a loader that cloned every collection would\n // leave those closures returning the originals while everything else\n // held the copies. A collection that `defineCollection` already\n // normalised passes through here untouched.\n if (Array.isArray(value)) {\n let changed = false;\n const out = value.map(item => {\n const next = resolveResourceRefs(item);\n if (next !== item) changed = true;\n return next;\n });\n return (changed ? out : value) as T;\n }\n if (value !== null && typeof value === \"object\") {\n const proto = Object.getPrototypeOf(value);\n if (proto === Object.prototype || proto === null) {\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n const next = resolveResourceRefs(v);\n if (next !== v) changed = true;\n out[k] = next;\n }\n return (changed ? out : value) as T;\n }\n }\n return value;\n}\n\n/**\n * The process-wide registry.\n *\n * Keyed off `globalThis` through a shared symbol rather than held in a module\n * local, because a module local is per *copy* of this package. A project that\n * ends up with two copies of `@rebasepro/types` — which a partially-linked\n * `node_modules` produces, and which has already caused a phantom\n * \"JWT secret not configured\" bug in this repo — would otherwise register into\n * one registry and read from the other, and see an empty graph with nothing\n * anywhere to explain it.\n *\n * `declarations` is that shared map, and stays shared: it is what a project\n * writes and what every copy has to be able to read.\n *\n * `kinds` is NOT, and the distinction is the whole point of `KINDS_KEY` below.\n */\ninterface Registry {\n /** Kinds this copy and its peers agree on — the versioned map. */\n kinds: Map<string, ResourceKindSpec>;\n /** Kinds written by a copy that predates the versioned map. Read-only here. */\n legacyKinds: Map<string, ResourceKindSpec>;\n declarations: Map<string, ResourceDeclaration>;\n}\n\nconst GLOBAL_KEY = Symbol.for(\"@rebasepro/types.resourceRegistry\");\n\n/**\n * Where kinds live, versioned — and why the version is in the symbol.\n *\n * Sharing one kinds map across copies means the copy that registers SECOND is\n * the one that runs the comparison. That copy is whatever the bundle happens to\n * carry, which for a driver is a build of this package frozen at its release —\n * so the rule enforced is the rule that shipped THEN, not the one written here.\n *\n * `revision` (390bb03cd, applied to `database` in 346df48e2) was supposed to\n * settle a disagreement between two copies, and it settles it only when the\n * copy doing the arithmetic knows what `revision` is. 0.17.0–0.17.3 do not:\n * they deep-equal the spec and throw. The runtime registers at import and a\n * driver is imported after it, so the old copy is always second, always the\n * judge, and always throws — verified by the bundle corpus on 2026-09-07,\n * which reported v0.17.3's message verbatim (\"Two packages cannot define the\n * same kind.\", no revision clause) while the runtime it ran on was 0.18.\n *\n * So copies that understand `revision` keep their kinds here, under a symbol\n * no released copy looks at, and the legacy map is left to whoever still wants\n * it. An old copy then registers into a map nobody contests, finds no existing\n * entry, and cannot throw — in any load order, which is what the previous fix\n * only claimed. Bumping this suffix again is how a future change to the\n * REGISTRATION PROTOCOL is made; a change to a kind's own definition is still\n * `revision`, among peers that share this map.\n */\nconst KINDS_KEY = Symbol.for(\"@rebasepro/types.resourceKinds.v2\");\n\nfunction registry(): Registry {\n const g = globalThis as Record<symbol, unknown>;\n let shared = g[GLOBAL_KEY] as { kinds: Map<string, ResourceKindSpec>; declarations: Map<string, ResourceDeclaration> } | undefined;\n if (!shared) {\n // `kinds` is created but never written by this copy: an older copy's\n // own `registry()` returns this object as-is once it exists, and would\n // throw on `undefined.get` if the property were absent.\n shared = { kinds: new Map(), declarations: new Map() };\n g[GLOBAL_KEY] = shared;\n }\n let kinds = g[KINDS_KEY] as Map<string, ResourceKindSpec> | undefined;\n if (!kinds) {\n kinds = new Map();\n g[KINDS_KEY] = kinds;\n }\n return { kinds, legacyKinds: shared.kinds, declarations: shared.declarations };\n}\n\n/**\n * Every kind visible to this copy: the versioned map, plus anything only a\n * legacy copy registered.\n *\n * The fallback is not for Rebase's own kinds — this copy defines all of those —\n * but for a third-party driver built against an older `@rebasepro/types` that\n * registers a kind of its own. Dropping it would make that kind invisible and\n * turn `declareResource` into \"unknown resource kind\" for something genuinely\n * registered.\n */\nfunction visibleKinds(): Map<string, ResourceKindSpec> {\n const { kinds, legacyKinds } = registry();\n if (legacyKinds.size === 0) return kinds;\n const merged = new Map(legacyKinds);\n for (const [k, v] of kinds) merged.set(k, v);\n return merged;\n}\n\n/**\n * Corrections this copy applies on top of a registered kind.\n *\n * Deliberately a module local — per COPY of this package — where the registry\n * above is deliberately shared. A published driver inlines this package into\n * its dist, and the copy it carries compares the shared registry's entry for a\n * kind against its own literal by `JSON.stringify` and throws if they differ\n * (see `registerResourceKind` before revisions existed). That code is in the\n * field and cannot be changed, so the registered literal of any kind that has\n * ever shipped is frozen: change one enumerable key and every bundle built with\n * an older driver dies at driver load on the next image. What a kind actually\n * binds can still be corrected — here, read through `resourceKind()` and\n * everything built on it, invisible to the older copy, which keeps binding the\n * way it did when it was published.\n */\ntype KindAmendment = Partial<Pick<ResourceKindSpec, \"envBases\" | \"envBasesByEngine\" | \"optionKeys\">>;\nconst amendments = new Map<string, KindAmendment>();\n\n/**\n * Correct a registered kind without touching its registered literal.\n *\n * Use this, never an edit to the literal, for a kind that has shipped in a\n * published package. The amendment applies to reads through this copy only.\n */\nexport function amendResourceKind(kind: string, amendment: KindAmendment): void {\n amendments.set(kind, { ...amendments.get(kind), ...amendment });\n}\n\n/** A registered kind as this copy sees it: the shared literal plus this copy's amendments. */\nfunction effectiveKind(spec: ResourceKindSpec): ResourceKindSpec {\n const amendment = amendments.get(spec.kind);\n return amendment ? { ...spec, ...amendment } : spec;\n}\n\n/** `kind:key`, the graph's primary key. */\nfunction declarationId(kind: string, key: string): string {\n return `${kind}:${key}`;\n}\n\n/**\n * Register a resource kind.\n *\n * Idempotent for an identical spec. When a spec for the same kind is already\n * registered and differs, the `revision` decides: the higher one is kept and\n * the other copy is warned about, in either load order. Two different specs at\n * the SAME revision are a genuine conflict — two packages defining one kind, or\n * a change that forgot to bump — and still throw.\n *\n * Both copies in that comparison are peers on `KINDS_KEY`, which is what makes\n * the rule enforceable: a copy old enough not to know `revision` writes to the\n * legacy map instead and never reaches this function's arithmetic. Registering\n * a kind an older copy already put in the legacy map is therefore not a\n * conflict — it is the ordinary case, and `visibleKinds` prefers this one.\n */\nexport function registerResourceKind(spec: ResourceKindSpec): void {\n const kinds = registry().kinds;\n const existing = kinds.get(spec.kind);\n if (!existing) {\n kinds.set(spec.kind, spec);\n return;\n }\n if (JSON.stringify(existing) === JSON.stringify(spec)) return;\n\n const have = existing.revision ?? 0;\n const incoming = spec.revision ?? 0;\n if (have === incoming) {\n throw new Error(\n `Resource kind \"${spec.kind}\" is already registered with a different definition at revision ${have}. ` +\n \"Two packages cannot define the same kind; a newer definition of the same kind must carry a higher `revision`.\"\n );\n }\n const [kept, dropped] = incoming > have ? [spec, existing] : [existing, spec];\n if (kept === spec) kinds.set(spec.kind, spec);\n // No logger below @rebasepro/server, and this runs in browsers too.\n console.warn(\n `[resources] Resource kind \"${spec.kind}\" is registered twice, at revisions ${dropped.revision ?? 0} and ` +\n `${kept.revision ?? 0}; keeping revision ${kept.revision ?? 0}. The older copy is usually @rebasepro/types ` +\n \"inlined in a driver built before the kind changed — rebuild the project with a current driver to remove it.\"\n );\n}\n\n/** Every registered kind, for validators and for `rebase doctor`. */\nexport function resourceKinds(): ResourceKindSpec[] {\n return [...visibleKinds().values()].map(effectiveKind);\n}\n\n/** One registered kind, or undefined. */\nexport function resourceKind(kind: string): ResourceKindSpec | undefined {\n const spec = visibleKinds().get(kind);\n return spec && effectiveKind(spec);\n}\n\n/** Options every kind accepts. */\nexport interface DeclareOptions {\n engine?: string;\n transport?: ResourceTransport;\n label?: string;\n [option: string]: unknown;\n}\n\nconst COMMON_OPTION_KEYS = [\"engine\", \"transport\", \"label\"] as const;\n\n/** Whether an engine is one the kind knows, or an explicit `custom:` opt-out. */\nexport function isValidEngine(spec: ResourceKindSpec, engine: string): boolean {\n return engine.startsWith(\"custom:\") || spec.engines.includes(engine);\n}\n\n/**\n * Declare a resource. The primitive every kind's constructor is built from.\n *\n * Redeclaring the same `kind:key` with a *different* shape throws rather than\n * merging. Merging is what the old storage path did, and it silently discarded\n * one of the two engines — a declaration accepted and then ignored, which is\n * the failure this whole model exists to remove. Redeclaring it identically is\n * fine: a config module evaluated twice must not be an error.\n */\nexport function declareResource(\n kind: string,\n key: string = DEFAULT_RESOURCE_KEY,\n options: DeclareOptions = {}\n): ResourceHandle {\n const spec = resourceKind(kind);\n if (!spec) {\n const known = [...visibleKinds().keys()].sort().join(\", \") || \"none\";\n throw new Error(\n `Unknown resource kind \"${kind}\". Registered kinds: ${known}. ` +\n \"Call registerResourceKind() before declaring one.\"\n );\n }\n\n if (!key || typeof key !== \"string\" || key.trim() === \"\") {\n throw new Error(`A ${kind} needs a non-empty key.`);\n }\n\n const engine = options.engine ?? spec.defaultEngine;\n if (!isValidEngine(spec, engine)) {\n throw new Error(\n `Unknown ${kind} engine \"${engine}\" for \"${key}\". ` +\n `Known engines: ${spec.engines.join(\", \")}. ` +\n `An engine this build does not ship is spelled \"custom:${engine}\", ` +\n \"which says so at the call site rather than failing later.\"\n );\n }\n\n const allowed = new Set<string>([...COMMON_OPTION_KEYS, ...(spec.optionKeys ?? [])]);\n const unknown = Object.keys(options).filter(k => !allowed.has(k));\n if (unknown.length > 0) {\n throw new Error(\n `Unknown option(s) on ${kind} \"${key}\": ${unknown.join(\", \")}. ` +\n `A ${kind} accepts: ${[...allowed].sort().join(\", \")}.`\n );\n }\n\n const extra: Record<string, unknown> = {};\n for (const k of spec.optionKeys ?? []) {\n if (options[k] !== undefined) extra[k] = options[k];\n }\n\n const declaration: ResourceDeclaration = {\n kind,\n key,\n engine,\n transport: options.transport ?? \"server\",\n ...(options.label !== undefined ? { label: options.label } : {}),\n options: Object.freeze(extra)\n };\n\n const id = declarationId(kind, key);\n const previous = registry().declarations.get(id);\n if (previous) {\n if (JSON.stringify(previous) !== JSON.stringify(declaration)) {\n throw new Error(\n `${kind} \"${key}\" is declared twice with different configuration. ` +\n \"Declare it once and export it — two declarations of one resource is \" +\n \"the ambiguity this model exists to remove, so it is refused rather \" +\n \"than merged.\"\n );\n }\n } else {\n registry().declarations.set(id, declaration);\n }\n\n const handle = {\n ...declaration,\n toString() { return key; },\n [BRAND]: true as const\n };\n return handle as ResourceHandle;\n}\n\n/** Every declared resource, in declaration order, optionally filtered by kind. */\nexport function declaredResources(kind?: string): ResourceDeclaration[] {\n const all = [...registry().declarations.values()];\n return kind ? all.filter(r => r.kind === kind) : all;\n}\n\n/**\n * Forget every declaration, keeping registered kinds.\n *\n * For tests and for a CLI that evaluates more than one project in a process.\n * Kinds survive because they are registered by module import, which will not\n * happen a second time.\n */\nexport function resetDeclaredResources(): void {\n registry().declarations.clear();\n}\n\n/**\n * The env-var suffix a resource's bindings use: `__ANALYTICS` for `analytics`,\n * and nothing at all for the default-keyed one.\n *\n * The default takes no suffix so that a project with one database configured\n * through plain `DATABASE_URL` keeps working having declared nothing — the\n * overwhelmingly common project must not have to say so.\n */\nexport function resourceEnvSuffix(key: string): string {\n if (key === DEFAULT_RESOURCE_KEY) return \"\";\n return `__${key.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\")}`;\n}\n\n/**\n * Two resources of a kind whose keys differ but whose env suffixes do not.\n *\n * `media-files` and `media_files` both become `__MEDIA_FILES`, so one would\n * silently read the other's configuration. Returned rather than thrown so the\n * caller can report it with the rest of a validation pass.\n */\nexport function findEnvSuffixCollision(keys: readonly string[]): { a: string; b: string; suffix: string } | null {\n const seen = new Map<string, string>();\n for (const key of keys) {\n const suffix = resourceEnvSuffix(key);\n const previous = seen.get(suffix);\n if (previous !== undefined && previous !== key) return { a: previous, b: key, suffix };\n seen.set(suffix, key);\n }\n return null;\n}\n\n/**\n * The whole graph, as recorded in a manifest and read by a host.\n *\n * `version` is the graph format, not the project's. A host reading a graph it\n * does not understand must say so rather than provision half of it.\n */\nexport interface ResourceGraph {\n version: 1;\n resources: ResourceDeclaration[];\n}\n\n/** The current graph format version. */\nexport const RESOURCE_GRAPH_VERSION = 1 as const;\n\n/**\n * Build a graph from the current declarations, sorted for a stable diff.\n *\n * `usedBy` maps a `kind:key` id to the things that reach it. The derive step\n * supplies it after evaluating collections; a runtime building the graph at\n * boot passes nothing and gets declarations alone, which is all it binds from.\n */\nexport function buildResourceGraph(usedBy?: ReadonlyMap<string, readonly string[]>): ResourceGraph {\n const resources = declaredResources().slice().sort(\n (a, b) => a.kind.localeCompare(b.kind) || a.key.localeCompare(b.key)\n ).map(r => {\n const users = usedBy?.get(declarationId(r.kind, r.key));\n return users && users.length > 0 ? { ...r, usedBy: [...users].sort() } : r;\n });\n return { version: RESOURCE_GRAPH_VERSION, resources };\n}\n\n/** `kind:key`, the id `usedBy` maps are keyed by. Exported for the derive step. */\nexport function resourceId(kind: string, key: string): string {\n return declarationId(kind, key);\n}\n\n/**\n * The environment variables worth writing for a resource, given its engine.\n *\n * Falls back to every base the kind reads when the engine is unknown — a\n * `custom:` engine gets the full list rather than an empty one, because\n * guessing narrow would silently omit the variable it actually needs.\n */\nexport function envBasesForResource(declaration: ResourceDeclaration): readonly string[] {\n const spec = resourceKind(declaration.kind);\n if (!spec) return [];\n return spec.envBasesByEngine?.[declaration.engine] ?? spec.envBases;\n}\n","/**\n * Describes a named storage backend — a place files live.\n *\n * Declared once and shared front + back: the frontend uses it to decide\n * transport (HTTP proxy vs direct SDK), the backend uses the same `key`\n * to resolve a StorageController, and collection properties reference\n * a definition by its `key` via `StorageConfig.storageSource`.\n *\n * This mirrors the {@link DataSourceDefinition} pattern used for databases.\n *\n * @group Models\n */\n\n/**\n * The default storage source key, used when a property does not specify\n * a `storageSource`. Shared by the frontend and backend registries so\n * both agree on \"the default storage backend\".\n * @group Models\n */\nexport const DEFAULT_STORAGE_SOURCE_KEY = \"(default)\";\n\n/**\n * How the *frontend* reaches a storage backend.\n *\n * - `\"server\"` — through the Rebase backend REST API (`/api/storage`).\n * The backend holds the actual `StorageController` and routes by\n * storage-source key. This is the default and covers Local, S3, GCS,\n * and any other server-mediated engine.\n * - `\"direct\"` — straight from the client to the external backend via\n * its own SDK (e.g. Firebase Storage via `@firebase/storage`).\n * The Rebase backend is **not** in the upload/download path.\n *\n * @group Models\n */\nexport type StorageSourceTransport = \"server\" | \"direct\";\n\n/**\n * Declarative definition of a storage source — a named place files live.\n *\n * Declared once and shared front and back: the frontend uses it to decide\n * transport (client HTTP proxy vs direct provider SDK), the backend uses\n * the same `key` to resolve a `StorageController`, and collection\n * properties reference a definition by its `key` via\n * `StorageConfig.storageSource`.\n *\n * @group Models\n */\nexport interface StorageSourceDefinition {\n /**\n * Unique identifier for this storage source. Collection properties\n * point at it via `StorageConfig.storageSource`.\n * Defaults to {@link DEFAULT_STORAGE_SOURCE_KEY}.\n */\n key: string;\n\n /**\n * The engine backing this storage source (e.g. `\"local\"`, `\"s3\"`,\n * `\"gcs\"`, `\"firebase\"`, `\"azure\"`, or a custom id).\n */\n engine: string;\n\n /**\n * The credential set this source signs with, when several sources share one.\n *\n * ## What it is for\n *\n * Every binding a bucket needs is read per key — `S3_BUCKET__MEDIA`,\n * `S3_ACCESS_KEY_ID__MEDIA`, and so on. That is right for the bucket *name*,\n * which is different for every source by definition, and wrong for the\n * credentials, which usually are not: fifteen buckets on one MinIO install\n * meant fifteen copies of the same endpoint, access key and secret — ninety\n * variables where eighteen would do, and one key rotation became fifteen\n * paired edits where a single missed one fails at upload time with an opaque\n * signing error.\n *\n * Naming an account here lets the *account-scoped* bindings fall back to\n * `<BASE>__<ACCOUNT>` when no per-key value is set. The bucket name never\n * falls back: it is what distinguishes one source from another.\n *\n * ## Why it does not fall back to the bare variable\n *\n * A source with no `account` reads only its own suffixed names, exactly as\n * before — so every project that predates this is wire-identical. The\n * unsuffixed `S3_ACCESS_KEY_ID` belongs to the *default* source, and letting\n * a named bucket inherit it would mean a typo'd key silently signs with\n * another source's credentials. Two forms, both explicit, opt-in.\n */\n account?: string;\n\n /**\n * How the frontend reaches this storage. Defaults to `\"server\"`.\n *\n * When `\"direct\"`, the client uses a provider-specific SDK\n * (e.g. `@firebase/storage`) and the backend does not proxy\n * upload/download traffic for this source.\n */\n transport: StorageSourceTransport;\n\n /**\n * Serve unqualified uploads — a storage property naming no `storageSource`\n * — from this source.\n *\n * Declared, never inferred. A project with named buckets and no default\n * used to have one chosen for it by declaration order, with a warning, and\n * the choice differed between development and production because the\n * synthesized local default is dropped in production and the promotion was\n * not. Where the files land is the author's decision; boot now fails\n * without one.\n */\n default?: boolean;\n\n /** Human-readable label for the UI (e.g. \"Firebase Storage\", \"S3 Media\"). */\n label?: string;\n}\n\n/**\n * A resolved storage source: the single source of truth that the frontend\n * router and backend registry both derive from.\n *\n * @group Models\n */\nexport interface ResolvedStorageSource {\n /** Storage source key (routing key, shared front + back). */\n key: string;\n /** Engine backing the source. */\n engine: string;\n /** Frontend transport. */\n transport: StorageSourceTransport;\n /** Human-readable label. */\n label?: string;\n}\n\n/**\n * The environment-variable suffix for a storage or data source key.\n *\n * `\"\"` for the default source — so a single-bucket project keeps configuring\n * plain `S3_BUCKET` — and `__<KEY>` for every named one, uppercased with\n * non-alphanumerics collapsed to underscores: `media-cdn` → `S3_BUCKET__MEDIA_CDN`.\n *\n * The rule derives the variable name from the declared key rather than\n * discovering keys by scanning the environment. Scanning would have to guess how\n * `S3_BUCKET__MEDIA_CDN` splits into a key; deriving cannot be ambiguous, and a\n * typo surfaces as a missing source at boot instead of a silently ignored\n * variable.\n *\n * It lives in this package, with no dependencies, because four things must agree\n * on it exactly: the CLI (validating a build), the runtime (reading its own\n * environment), the control plane (writing a tenant's Secret), and the docs. A\n * second implementation of a naming convention is a second chance to disagree.\n *\n * @group Models\n */\nexport function storageEnvSuffix(key: string, defaultKey: string = DEFAULT_STORAGE_SOURCE_KEY): string {\n if (!key || key === defaultKey) return \"\";\n const normalized = key\n .replace(/[^A-Za-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .toUpperCase();\n if (!normalized) {\n throw new Error(\n `Source key \"${key}\" cannot be turned into an environment variable name. ` +\n \"Use a key containing at least one letter or digit.\"\n );\n }\n return `__${normalized}`;\n}\n\n/**\n * Two distinct keys that collapse onto the same variable name, or `null`.\n *\n * `media-cdn` and `media_cdn` are different source keys but the same suffix, so\n * without this one of them silently reads the other's configuration. Returns the\n * offending pair rather than throwing, so each caller can raise it in its own\n * idiom — a `BundleError` at boot, a build failure in the CLI, a rejected deploy\n * in a control plane.\n *\n * @group Models\n */\nexport function findStorageSuffixCollision(\n keys: string[],\n defaultKey: string = DEFAULT_STORAGE_SOURCE_KEY\n): { a: string; b: string; suffix: string } | null {\n const seen = new Map<string, string>();\n for (const key of keys) {\n const suffix = storageEnvSuffix(key, defaultKey);\n const existing = seen.get(suffix);\n if (existing !== undefined && existing !== key) {\n return { a: existing, b: key, suffix };\n }\n seen.set(suffix, key);\n }\n return null;\n}\n","/**\n * The kinds Rebase ships, and the constructors a project declares them with.\n *\n * Each kind is registered rather than hardcoded, so a fourth one arrives\n * without editing a manifest schema, a validator and a switch statement. That\n * cost is precisely why databases and buckets ended up declared in different\n * files with different rules — the cheapest thing to do was always to bolt the\n * new kind onto whichever home was nearest.\n *\n * A kind owns its engine list. `custom:<id>` is always accepted, so a build\n * that ships an engine this package has never heard of says so at the call site\n * instead of looking like a typo of one that exists.\n */\nimport {\n DEFAULT_RESOURCE_KEY,\n declareResource,\n declaredResources,\n amendResourceKind,\n registerResourceKind,\n type DeclareOptions,\n type ResourceDeclaration,\n type ResourceHandle\n} from \"./resources\";\nimport { DEFAULT_DATA_SOURCE_KEY, type DataSourceDefinition } from \"./data_source\";\nimport { DEFAULT_STORAGE_SOURCE_KEY, type StorageSourceDefinition } from \"./storage_source\";\n\n// ── database ─────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n // There is no single frozen literal for this kind, which is why it carries a\n // revision. 0.17.0 and 0.17.1 shipped `optionKeys: [\"databaseId\",\n // \"migrations\"]`; 0.17.2 added \"extensions\" to the literal itself, before the\n // rule against that existed. So two different objects are inlined in drivers\n // that are in the field, and no choice of literal can equal both: a runtime\n // that matched one threw `already registered with a different definition` at\n // the other and refused to boot. Two tenants crash-looped for six and a half\n // days on exactly that.\n //\n // `revision` is what resolves it. An older copy — whichever literal it\n // carries — is at revision 0, loses to this one, and warns instead of\n // throwing. Corrections still go in the amendment below; this number moves\n // only when the literal itself has to, and every published copy predating the\n // move is thereby handled.\n revision: 1,\n kind: \"database\",\n engines: [\"postgres\", \"mongodb\", \"firestore\", \"sqlite\"],\n defaultEngine: \"postgres\",\n envBases: [\"DATABASE_URL\", \"REBASE_DRIVER\", \"REBASE_DB_POOL_MAX\"],\n optionKeys: [\"databaseId\", \"migrations\", \"extensions\"],\n implicitDefault: true\n});\n// What a database actually binds from. The 0.17.3 list named two variables\n// the resolver never read and omitted five it does (25f1a97e3).\namendResourceKind(\"database\", {\n envBases: [\n \"DATABASE_URL\",\n \"DATABASE_READ_URL\",\n \"ADMIN_CONNECTION_STRING\",\n \"REBASE_DRIVER\",\n \"DB_POOL_MAX\",\n \"DB_POOL_IDLE_TIMEOUT\",\n \"DB_POOL_CONNECT_TIMEOUT\"\n ]\n});\n\n/** Options a database accepts beyond the common ones. */\nexport interface DatabaseOptions extends DeclareOptions {\n /**\n * The physical database or schema within the engine, when it differs from\n * the engine's own default. Threaded to drivers as `databaseId`.\n */\n databaseId?: string;\n /** Directory of migration files, relative to the config directory. */\n migrations?: string;\n /**\n * Server extensions Rebase may install on this database.\n *\n * A permission, not a request: naming one grants leave to run\n * `CREATE EXTENSION IF NOT EXISTS <name>`, and Rebase issues it only when\n * something in the schema actually needs it. Naming an extension nothing\n * needs installs nothing.\n *\n * It has to be said out loud because installing an extension is a decision\n * with a deployment behind it — the image has to ship the library, the role\n * has to be allowed to install it, and a managed provider has to have it on\n * an allow-list. Rebase cannot see any of that from inside the connection,\n * so the answer comes from whoever chose the database.\n *\n * Today `vector` is the one that matters: a `{ type: \"vector\" }` property\n * compiles to a `VECTOR(n)` column, which does not exist until pgvector is\n * installed. Without this, Rebase creates the column and lets Postgres\n * refuse, naming the option.\n *\n * ```ts\n * export const main = database({ extensions: [\"vector\"] });\n * ```\n *\n * `pg_trgm` and `unaccent` are not on this list and need no permission: a\n * `search` block installs them unasked, because they are contrib modules\n * present in every Postgres distribution. pgvector is a separate build that\n * a stock `postgres:18` does not carry.\n */\n extensions?: string[];\n}\n\n/** A database handle. Collections point at it via `dataSource`. */\nexport type DatabaseHandle = ResourceHandle;\n\n/**\n * Declare a database.\n *\n * ```ts\n * export const main = database(); // the default one\n * export const analytics = database(\"analytics\"); // reads DATABASE_URL__ANALYTICS\n * export const withPgv = database({ extensions: [\"vector\"] }); // the default one, configured\n * ```\n *\n * The third form exists because the default database has no name to pass, and\n * the alternative was `database(\"(default)\", { … })` — writing out an internal\n * sentinel to reach the options. A key is a string and options are an object,\n * so the two can never be confused for one another.\n */\nexport function database(options?: DatabaseOptions): DatabaseHandle;\nexport function database(key?: string, options?: DatabaseOptions): DatabaseHandle;\nexport function database(\n keyOrOptions: string | DatabaseOptions = DEFAULT_RESOURCE_KEY,\n options: DatabaseOptions = {}\n): DatabaseHandle {\n return typeof keyOrOptions === \"string\"\n ? declareResource(\"database\", keyOrOptions, options)\n : declareResource(\"database\", DEFAULT_RESOURCE_KEY, keyOrOptions);\n}\n\n/**\n * The extensions the project's databases gave Rebase leave to install.\n *\n * A flat union rather than a per-database answer, because the surfaces that ask\n * — `rebase db push` and the boot schema-ensure — drive one connection and\n * generate one `schema.sql` for every collection regardless of `dataSource`.\n * Splitting the permission by data source would be a distinction the rest of\n * that pipeline does not make, and a false precision is worse than none.\n *\n * Empty for a project that declared nothing, which is every project that has\n * not opted in — so this reads as a refusal by default, on purpose.\n */\nexport function declaredDatabaseExtensions(): readonly string[] {\n const names = new Set<string>();\n for (const declaration of declaredResources(\"database\")) {\n const declared = declaration.options.extensions;\n if (!Array.isArray(declared)) continue;\n for (const name of declared) {\n if (typeof name === \"string\" && name.trim()) names.add(name.trim());\n }\n }\n return [...names].sort();\n}\n\n// ── bucket ───────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n // FROZEN at the 0.17.3 literal, for the reason given on `database`.\n kind: \"bucket\",\n engines: [\"local\", \"s3\", \"gcs\", \"azure\", \"firebase\"],\n defaultEngine: \"local\",\n envBases: [\"S3_BUCKET\", \"GCS_BUCKET\", \"STORAGE_BUCKET\", \"STORAGE_PUBLIC_URL\"],\n envBasesByEngine: {\n local: [\"STORAGE_BUCKET\"],\n s3: [\"S3_BUCKET\", \"STORAGE_ENDPOINT\", \"STORAGE_REGION\", \"STORAGE_PUBLIC_URL\"],\n gcs: [\"GCS_BUCKET\", \"STORAGE_PUBLIC_URL\"],\n azure: [\"STORAGE_BUCKET\", \"STORAGE_PUBLIC_URL\"],\n firebase: [\"STORAGE_BUCKET\", \"STORAGE_PUBLIC_URL\"]\n },\n optionKeys: [\"publicRead\", \"prefix\", \"account\"],\n implicitDefault: false\n});\n// What a bucket actually binds from, per engine (25f1a97e3), plus `default`:\n// the registry no longer promotes a lone named bucket, so a project needs a way\n// to say which one serves unqualified uploads. The literal above is frozen, so\n// the new option key arrives here.\namendResourceKind(\"bucket\", {\n optionKeys: [\"publicRead\", \"prefix\", \"account\", \"default\"],\n envBases: [\n \"STORAGE_TYPE\",\n \"STORAGE_PATH\",\n \"S3_BUCKET\",\n \"S3_REGION\",\n \"S3_ACCESS_KEY_ID\",\n \"S3_SECRET_ACCESS_KEY\",\n \"S3_ENDPOINT\",\n \"S3_FORCE_PATH_STYLE\",\n \"GCS_BUCKET\",\n \"GCS_PROJECT_ID\",\n \"GCS_KEY_FILENAME\"\n ],\n envBasesByEngine: {\n local: [\"STORAGE_TYPE\", \"STORAGE_PATH\"],\n s3: [\n \"STORAGE_TYPE\",\n \"S3_BUCKET\",\n \"S3_REGION\",\n \"S3_ACCESS_KEY_ID\",\n \"S3_SECRET_ACCESS_KEY\",\n \"S3_ENDPOINT\",\n \"S3_FORCE_PATH_STYLE\"\n ],\n gcs: [\"STORAGE_TYPE\", \"GCS_BUCKET\", \"GCS_PROJECT_ID\", \"GCS_KEY_FILENAME\"],\n azure: [],\n firebase: []\n }\n});\n\n/** Options a bucket accepts beyond the common ones. */\nexport interface BucketOptions extends DeclareOptions {\n /**\n * Whether objects are world-readable by default.\n *\n * Declared rather than inferred from the engine, because the two have\n * disagreed before: a private object served through a cacheable public URL\n * is a data leak that nothing errors on.\n */\n publicRead?: boolean;\n /** Key prefix within the bucket, for sharing one bucket between sources. */\n prefix?: string;\n /**\n * Serve unqualified uploads — a storage property with no `storageSource` —\n * from this bucket.\n *\n * A project that declares only `bucket(\"media\")` has no default bucket, and\n * the registry used to promote the one it found with a warning. That is a\n * decision about where a user's files land, made by the framework, on the\n * strength of declaration order; it also produced two different\n * destinations either side of a deploy, because the synthesized local\n * default is dropped in production and the promotion is not. So it is now\n * a boot error, and this is one of the two ways to answer it — the other\n * being `bucket()`, which declares the default bucket itself.\n */\n default?: boolean;\n /**\n * The credential set this bucket signs with, when several share one.\n *\n * `bucket(\"media\", { engine: \"s3\", account: \"minio\" })` keeps reading its own\n * `S3_BUCKET__MEDIA` — the bucket name is what distinguishes one source from\n * another and never falls back — while the provider-level variables\n * (`S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_ENDPOINT`, `S3_REGION`,\n * `S3_FORCE_PATH_STYLE`) fall back to `__MINIO` when no per-key value is set.\n *\n * Fifteen buckets on one install go from ninety variables to eighteen, and\n * rotating the key becomes one edit. A per-bucket value still wins, so a\n * single source can move to another provider without breaking the rest off\n * their shared account.\n */\n account?: string;\n}\n\n/** A bucket handle. Storage properties point at it via `storageSource`. */\nexport type BucketHandle = ResourceHandle;\n\n/**\n * Declare a bucket.\n *\n * ```ts\n * export const uploads = bucket({ engine: \"s3\" }); // the default one\n * export const media = bucket(\"media\", { transport: \"direct\" });\n * ```\n *\n * `transport: \"direct\"` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n *\n * The options-only form exists for the same reason `database`'s does: the\n * default bucket has no name to pass, and without it the only way to configure\n * one was `bucket(\"(default)\", { … })` — writing out an internal sentinel to\n * reach the options. Passing options where a key belongs used to throw \"a\n * bucket needs a non-empty key\", which names neither the mistake nor the fix.\n */\nexport function bucket(options?: BucketOptions): BucketHandle;\nexport function bucket(key?: string, options?: BucketOptions): BucketHandle;\nexport function bucket(\n keyOrOptions: string | BucketOptions = DEFAULT_RESOURCE_KEY,\n options: BucketOptions = {}\n): BucketHandle {\n return typeof keyOrOptions === \"string\"\n ? declareResource(\"bucket\", keyOrOptions, options)\n : declareResource(\"bucket\", DEFAULT_RESOURCE_KEY, keyOrOptions);\n}\n\n// ── topic ────────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n // FROZEN at the 0.17.3 literal, for the reason given on `database`.\n kind: \"topic\",\n // `jobs` is the durable local implementation: a topic fans out to one job\n // row per subscription, so each subscriber retries on its own schedule and\n // a failure is a row somebody can look at rather than a lost message.\n engines: [\"jobs\"],\n defaultEngine: \"jobs\",\n envBases: [\"REBASE_TOPIC_URL\"],\n optionKeys: [\"delivery\", \"maxAttempts\"],\n implicitDefault: false\n});\n// Nothing. A topic on the `jobs` engine is rows in the project's own database\n// and binds from no variable of its own. The literal above says\n// `REBASE_TOPIC_URL` — a name nothing in either repository read, which\n// `rebase status` then printed as a variable somebody could set — and it has to\n// keep saying it, because a driver ≤ 0.17.3 compares that object and throws.\n// The gate in `resource-env-bases.test.ts` covers every registered kind through\n// the amended view, so a phantom name fails a build rather than reaching a\n// developer.\namendResourceKind(\"topic\", { envBases: [] });\n\n/**\n * How hard the runtime tries to deliver.\n *\n * Only `at-least-once` is implemented, and it is the honest name for what a\n * retrying queue does: a handler must tolerate seeing the same event twice.\n * `at-most-once` is listed so a future transport can offer it without the\n * option changing shape, and is refused today rather than silently upgraded.\n */\nexport type TopicDelivery = \"at-least-once\" | \"at-most-once\";\n\n/** Options a topic accepts beyond the common ones. */\nexport interface TopicOptions extends DeclareOptions {\n delivery?: TopicDelivery;\n /** Attempts per subscription before a message is left failed. Default 5. */\n maxAttempts?: number;\n}\n\n/**\n * What a subscription does with an event.\n *\n * `attempt` counts from 1. Worth branching on: the first delivery and the\n * fourth are the same call, but the fourth is where it is worth logging loudly.\n */\nexport type TopicHandler<T> = (event: T, context: { attempt: number; topic: string; subscription: string }) => Promise<void> | void;\n\n/** A declared subscription, as recorded in the graph and wired at boot. */\nexport interface TopicSubscription<T = unknown> {\n topic: string;\n name: string;\n handler: TopicHandler<T>;\n maxAttempts?: number;\n}\n\n/**\n * What a topic publishes through.\n *\n * Installed by `@rebasepro/server` at boot. Absent — in the CLI evaluating\n * config to derive the graph, or in a unit test — publishing throws a message\n * naming the cause, rather than resolving and dropping the event. A publish\n * that silently does nothing is the failure mode a queue exists to prevent.\n */\nexport interface TopicRuntime {\n publish(topic: string, event: unknown): Promise<void>;\n}\n\nconst runtimeHolder: { current: TopicRuntime | null } = { current: null };\n\n/** Install the transport topics publish through. Called by the server at boot. */\nexport function setTopicRuntime(runtime: TopicRuntime | null): void {\n runtimeHolder.current = runtime;\n}\n\nconst subscriptions: TopicSubscription[] = [];\n\n/** Every declared subscription, for the worker to wire and the graph to record. */\nexport function declaredSubscriptions(topic?: string): TopicSubscription[] {\n return topic ? subscriptions.filter(s => s.topic === topic) : subscriptions.slice();\n}\n\n/** Forget declared subscriptions. For tests, alongside `resetDeclaredResources`. */\nexport function resetDeclaredSubscriptions(): void {\n subscriptions.length = 0;\n}\n\n/** A topic handle, carrying its payload type. */\nexport interface TopicHandle<T> extends ResourceHandle {\n /**\n * Publish an event.\n *\n * Resolves once the event is durably recorded for every subscription, not\n * once they have run. Enqueued inside a transaction that rolls back, it was\n * never published.\n */\n publish(event: T): Promise<void>;\n /**\n * Declare a subscription.\n *\n * The name is its identity: it is what the job row records, what a retry\n * counts against, and what a second subscription must not collide with.\n */\n subscription(name: string, handler: TopicHandler<T>, options?: { maxAttempts?: number }): void;\n}\n\n/**\n * Declare a topic.\n *\n * ```ts\n * export const signups = topic<{ userId: string }>(\"signups\");\n * signups.subscription(\"send-welcome\", async (event) => { … });\n * await signups.publish({ userId });\n * ```\n */\nexport function topic<T = unknown>(key: string, options: TopicOptions = {}): TopicHandle<T> {\n if (options.delivery === \"at-most-once\") {\n throw new Error(\n `Topic \"${key}\" asks for at-most-once delivery, which no shipped transport implements. ` +\n \"The durable queue behind topics retries, so it is at-least-once and a handler must \" +\n \"tolerate seeing an event twice. Refused rather than quietly given the other guarantee.\"\n );\n }\n const handle = declareResource(\"topic\", key, options);\n\n return {\n ...handle,\n toString() { return key; },\n async publish(event: T): Promise<void> {\n const runtime = runtimeHolder.current;\n if (!runtime) {\n throw new Error(\n `Cannot publish to topic \"${key}\": no topic runtime is installed. ` +\n \"Publishing works inside a running Rebase backend; this looks like config \" +\n \"being evaluated outside one (a build, a script, or a test without a harness).\"\n );\n }\n await runtime.publish(key, event);\n },\n subscription(name: string, handler: TopicHandler<T>, subOptions: { maxAttempts?: number } = {}): void {\n if (!name || name.trim() === \"\") {\n throw new Error(`A subscription on topic \"${key}\" needs a non-empty name.`);\n }\n if (subscriptions.some(s => s.topic === key && s.name === name)) {\n throw new Error(\n `Topic \"${key}\" already has a subscription named \"${name}\". ` +\n \"The name is what a job row records and what a retry counts against, so two \" +\n \"cannot share one.\"\n );\n }\n subscriptions.push({\n topic: key,\n name,\n handler: handler as TopicHandler<unknown>,\n ...(subOptions.maxAttempts !== undefined ? { maxAttempts: subOptions.maxAttempts } : {})\n });\n }\n } as TopicHandle<T>;\n}\n\n// ── cron ─────────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n kind: \"cron\",\n // The in-process scheduler, claiming each slot in `rebase.cron_claims` so\n // several instances of one deployment run a slot once. It is the only\n // engine because it is the only one that exists; an external scheduler\n // (a platform's cron, a Kubernetes CronJob) would be a second engine that\n // triggers the same handler over HTTP, and it can register itself.\n engines: [\"scheduler\"],\n defaultEngine: \"scheduler\",\n // Code, not configuration: a cron binds from no variable. It is in the\n // graph so a host knows a project's schedules BEFORE running it, which is\n // what lets a console show them and a placement decision read them.\n envBases: [],\n optionKeys: [\"schedule\", \"timezone\", \"description\", \"enabled\", \"timeoutSeconds\", \"catchUpWindowSeconds\"],\n implicitDefault: false\n});\n\n/** What a cron declaration records, beyond its handler. */\nexport interface CronResourceOptions extends DeclareOptions {\n /** Five-field cron expression, e.g. `0 3 * * *`. */\n schedule: string;\n /**\n * IANA zone the schedule is read in, e.g. `Europe/Madrid`.\n *\n * Without it the schedule is read in the process's own zone, which is\n * whatever the host happens to be set to — UTC in nearly every container,\n * the developer's own on a laptop. \"3 AM\" then means two different hours\n * either side of a deploy. Naming the zone makes the declaration mean one\n * thing everywhere.\n */\n timezone?: string;\n description?: string;\n enabled?: boolean;\n timeoutSeconds?: number;\n catchUpWindowSeconds?: number;\n}\n\n/**\n * Declare a cron, as the scheduler's `defineCron` does on its way through.\n *\n * Projects do not call this: `defineCron` in `@rebasepro/server` does, so a\n * cron file is both the handler and the declaration — one file, one name, and\n * the graph derived from it says what a host needs to know without evaluating\n * the handler. Exported so the derive step and the scheduler spell the\n * declaration identically.\n */\nexport function declareCron(name: string, options: CronResourceOptions): ResourceHandle {\n if (typeof options.schedule !== \"string\" || options.schedule.trim() === \"\") {\n throw new Error(`Cron \"${name}\" needs a schedule — a five-field cron expression such as \"0 3 * * *\".`);\n }\n return declareResource(\"cron\", name, options);\n}\n\n// ── function ─────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n kind: \"function\",\n // Mounted by this runtime at `/api/functions/<name>`. A host that runs a\n // function elsewhere — an edge runtime, say — is a second engine, and the\n // bundle's `portable` analysis already says which ones could move.\n engines: [\"http\"],\n defaultEngine: \"http\",\n envBases: [],\n optionKeys: [\"portable\", \"requires\", \"file\"],\n implicitDefault: false\n});\n\n/**\n * What a function declaration records.\n *\n * Recorded by the derive step from the bundler's static analysis rather than\n * by evaluating the function module: a function's handler is a Hono app that\n * only needs to exist at request time, and evaluating it at build time would\n * run its module-scope code in a process with none of its environment.\n */\nexport interface FunctionResourceOptions extends DeclareOptions {\n /** Path inside the project, so a host can point at the file. */\n file?: string;\n /** `false` when the source imports a Node built-in or a package that needs one. */\n portable?: boolean;\n /** Why it is not portable — one short phrase per reason. */\n requires?: string[];\n}\n\n/** Declare a function. Called by the derive step, not by projects. */\nexport function declareFunction(name: string, options: FunctionResourceOptions = {}): ResourceHandle {\n return declareResource(\"function\", name, options);\n}\n\n// ── queue ────────────────────────────────────────────────────────────────────\n\nregisterResourceKind({\n kind: \"queue\",\n // Same durable queue topics ride on: a row per job, claimed with\n // `FOR UPDATE SKIP LOCKED`, retried on a backoff, kept when it gives up.\n engines: [\"jobs\"],\n defaultEngine: \"jobs\",\n envBases: [],\n optionKeys: [\"maxAttempts\"],\n implicitDefault: false\n});\n\n/** Options a queue accepts beyond the common ones. */\nexport interface QueueOptions extends DeclareOptions {\n /** Attempts before a job is left failed. Default 5. */\n maxAttempts?: number;\n}\n\n/** What a queue's handler receives. `attempt` counts from 1. */\nexport type QueueHandler<T> = (\n payload: T,\n context: { attempt: number; queue: string; jobId: string }\n) => Promise<void> | void;\n\n/** Per-job options at enqueue time. */\nexport interface QueueEnqueueOptions {\n /** Earliest time the job may run. Defaults to now. */\n runAt?: Date;\n /** Attempts for this job, overriding the queue's. */\n maxAttempts?: number;\n}\n\n/**\n * What a queue enqueues through.\n *\n * Installed by `@rebasepro/server` at boot, alongside the topic runtime.\n * Absent — config evaluated by the CLI, a unit test — enqueueing throws with\n * the cause named, rather than resolving and dropping the job.\n */\nexport interface QueueRuntime {\n enqueue(queue: string, payload: unknown, options?: QueueEnqueueOptions): Promise<{ id: string }>;\n}\n\nconst queueRuntimeHolder: { current: QueueRuntime | null } = { current: null };\n\n/** Install the transport queues enqueue through. Called by the server at boot. */\nexport function setQueueRuntime(runtime: QueueRuntime | null): void {\n queueRuntimeHolder.current = runtime;\n}\n\n/** A queue's handler, as recorded for the worker to wire. */\nexport interface QueueConsumer<T = unknown> {\n queue: string;\n handler: QueueHandler<T>;\n}\n\nconst queueConsumers = new Map<string, QueueConsumer>();\n\n/** Every declared queue handler, for the worker to wire. */\nexport function declaredQueueConsumers(): QueueConsumer[] {\n return [...queueConsumers.values()];\n}\n\n/** Forget declared queue handlers. For tests, alongside `resetDeclaredResources`. */\nexport function resetDeclaredQueueConsumers(): void {\n queueConsumers.clear();\n}\n\n/** A queue handle, carrying its payload type. */\nexport interface QueueHandle<T> extends ResourceHandle {\n /**\n * Put a job on the queue.\n *\n * Resolves once the job is durably recorded, not once it has run. A row\n * insert, so enqueued inside a transaction that rolls back it was never\n * enqueued.\n */\n enqueue(payload: T, options?: QueueEnqueueOptions): Promise<{ id: string }>;\n /**\n * Declare the handler.\n *\n * One per queue: a queue is a work list with one consumer, which is what\n * separates it from a topic. Work that several things must react to is a\n * topic with several subscriptions.\n */\n handler(fn: QueueHandler<T>): void;\n}\n\n/**\n * Declare a queue.\n *\n * ```ts\n * export const thumbnails = queue<{ key: string }>(\"thumbnails\");\n * thumbnails.handler(async ({ key }) => { … });\n * await thumbnails.enqueue({ key }, { runAt: new Date(Date.now() + 60_000) });\n * ```\n *\n * The difference from a topic is the number of consumers: a queue has one, a\n * topic fans out to every subscription. Both ride on the durable job queue, so\n * declaring either turns it on.\n */\nexport function queue<T = unknown>(key: string, options: QueueOptions = {}): QueueHandle<T> {\n const handle = declareResource(\"queue\", key, options);\n\n return {\n ...handle,\n toString() { return key; },\n async enqueue(payload: T, enqueueOptions?: QueueEnqueueOptions): Promise<{ id: string }> {\n const runtime = queueRuntimeHolder.current;\n if (!runtime) {\n throw new Error(\n `Cannot enqueue on queue \"${key}\": no queue runtime is installed. ` +\n \"Enqueueing works inside a running Rebase backend; this looks like config \" +\n \"being evaluated outside one (a build, a script, or a test without a harness).\"\n );\n }\n return runtime.enqueue(key, payload, enqueueOptions);\n },\n handler(fn: QueueHandler<T>): void {\n if (queueConsumers.has(key)) {\n throw new Error(\n `Queue \"${key}\" already has a handler. A queue has exactly one consumer; ` +\n \"work that several things react to is a topic with several subscriptions.\"\n );\n }\n queueConsumers.set(key, { queue: key, handler: fn as QueueHandler<unknown> });\n }\n } as QueueHandle<T>;\n}\n\n// ── Handing declarations to the readers ──────────────────────────────────────\n\n/**\n * One declaration, as the data layer's definition.\n *\n * There is exactly one of these per kind, and everything that needs a\n * definition goes through it — the frontend, the managed runtime's boot path,\n * and an ejected project's own entrypoint. That is not tidiness: the mapping\n * used to exist twice, once here and once in `@rebasepro/server`'s\n * `graphToStorageSources`, and the two disagreed. The server's copy carried a\n * bucket's `account`; this one dropped it, so a bucket declared with shared\n * credentials resolved them on the managed runtime and resolved *nothing* in an\n * ejected backend — the source was skipped and every upload to it answered 501.\n *\n * A field-by-field map is one line away from that failure at all times, so\n * there is now one line to keep right instead of two to keep equal.\n */\nexport function resourceToDataSource(declaration: ResourceDeclaration): DataSourceDefinition {\n return {\n // The graph and the data layer spell \"the unnamed one\" identically\n // today, but they are separate constants and nothing stops them\n // drifting. Mapped explicitly so a divergence is a compile error rather\n // than a default database that silently fails to bind.\n key: declaration.key === DEFAULT_RESOURCE_KEY ? DEFAULT_DATA_SOURCE_KEY : declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n ...(typeof declaration.options.databaseId === \"string\"\n ? { databaseId: declaration.options.databaseId }\n : {}),\n ...(declaration.label !== undefined ? { label: declaration.label } : {})\n };\n}\n\n/** One declaration, as the storage layer's definition. See {@link resourceToDataSource}. */\nexport function resourceToStorageSource(declaration: ResourceDeclaration): StorageSourceDefinition {\n return {\n key: declaration.key === DEFAULT_RESOURCE_KEY ? DEFAULT_STORAGE_SOURCE_KEY : declaration.key,\n engine: declaration.engine,\n transport: declaration.transport,\n // Carried, or the declaration's `account` is accepted at the call site\n // and lost on the way to the reader — a declared option that does\n // nothing, which is the exact failure this whole model exists to remove.\n ...(typeof declaration.options.account === \"string\"\n ? { account: declaration.options.account }\n : {}),\n ...(declaration.options.default === true ? { default: true } : {}),\n ...(declaration.label !== undefined ? { label: declaration.label } : {})\n };\n}\n\n/**\n * The declared databases, as definitions.\n *\n * Both the frontend and a project's own backend entrypoint read this. The\n * frontend needs to know which sources exist and how they are reached — a\n * `direct`-transport source is one the browser talks to itself — and it imports\n * the same config package the backend does. Without these it would mean writing\n * the list a second time, by hand, next to the declarations, which is precisely\n * the two-homes problem this model removed everywhere else.\n *\n * ```tsx\n * import \"../config/resources\"; // registers them\n * import { declaredDataSources, declaredStorageSources } from \"@rebasepro/types\";\n *\n * <Rebase dataSources={declaredDataSources()} storageSources={declaredStorageSources()} />\n * ```\n *\n * The import is what registers them, so a bundler that drops an unused module\n * would leave this empty — hence the side-effect import above rather than a\n * bare re-export.\n */\nexport function declaredDataSources(): DataSourceDefinition[] {\n return declaredResources(\"database\").map(resourceToDataSource);\n}\n\n/** The declared buckets, as definitions. */\nexport function declaredStorageSources(): StorageSourceDefinition[] {\n return declaredResources(\"bucket\").map(resourceToStorageSource);\n}\n","/**\n * How a collection points at a UI component without the backend learning about React.\n *\n * This file is the hinge the BaaS/admin split turns on. `ComponentRef` is named\n * by a property's `admin` block (`admin.Field`, `admin.Preview`, `admin.Filter`)\n * and imported by `properties.ts`, which must stay in the React-free core\n * because every backend subsystem — validation,\n * the drizzle schema generator, the OpenAPI generator, the SDK codegen — reads\n * property definitions. If `ComponentRef` needed `React.ComponentType`, the whole\n * property model would have to move to the admin layer with it.\n *\n * So the React types are described structurally instead of imported. Every form\n * a React component takes is assignable to {@link ComponentLike}:\n *\n * - a function component is `(props: P) => ReactNode`\n * - a class component satisfies the construct signature (`Component` has `render`)\n * - `memo` and `forwardRef` return exotic components, which are callable\n *\n * The cost is that the return type is `unknown` rather than `ReactNode`, so a\n * function that returns something React could not render is accepted here.\n * `@rebasepro/cms-types` re-exports a `ReactComponentRef<P>` narrowed against\n * the real `React.ComponentType` for authoring and for the admin's internals,\n * which restores that check where it can be enforced.\n */\n\n/**\n * Structural stand-in for `React.ComponentType<P>`.\n *\n * Deliberately not `Function` or `unknown`: those would accept anything and the\n * resolver's runtime heuristics ({@link ComponentRef} form 3) would be all that\n * stood between a typo and a blank screen.\n */\nexport type ComponentLike<P = any> =\n | ((props: P) => unknown)\n | (new (props: P, context?: unknown) => { render(): unknown });\n\n/**\n * Internal marker for a lazily-loaded component reference.\n * Created by the Vite transform plugin when converting string paths\n * to deferred `import()` calls. Users should NOT create these manually.\n *\n * @internal\n */\nexport interface LazyComponentRef<P = unknown> {\n readonly __rebaseLazy: true;\n readonly load: () => Promise<{ default: ComponentLike<P> }>;\n}\n\n/**\n * A reference to a UI component that can be provided in three forms:\n *\n * 1. **String path** (recommended for collection configs):\n * ```ts\n * Field: \"../../frontend/src/components/MyField\"\n * ```\n * The Vite plugin transforms this into a `LazyComponentRef` at build time.\n * On the backend, the string stays inert and is never evaluated.\n *\n * 2. **Lazy import function**:\n * ```ts\n * Field: () => import(\"../../frontend/src/components/MyField\")\n * ```\n * Standard ES dynamic import. Backend never calls the function.\n *\n * 3. **Direct component reference** (use only in frontend-only code):\n * ```ts\n * Field: MyFieldComponent\n * ```\n * Importing a component at the top level will pull React into the\n * backend runtime — only safe in code that the backend never imports.\n * `pnpm check:headless` fails on a collection file that does this.\n *\n * @group Types\n */\nexport type ComponentRef<P = any> =\n | string\n | LazyComponentRef<P>\n | (() => Promise<{ default: ComponentLike<P> }>)\n | ComponentLike<P>;\n\n/**\n * Type guard: checks if a value is a `LazyComponentRef` produced by the\n * Vite transform plugin.\n */\nexport function isLazyComponentRef<P = unknown>(ref: unknown): ref is LazyComponentRef<P> {\n return (\n typeof ref === \"object\" &&\n ref !== null &&\n \"__rebaseLazy\" in ref &&\n (ref as Record<string, unknown>).__rebaseLazy === true\n );\n}\n","/**\n * The project manifest (`rebase.json`) and the build artifacts derived from it.\n *\n * Three separate documents live in this file, and keeping them distinct matters:\n *\n * 1. {@link RebaseProjectManifest} — `rebase.json`. **Authored** by the developer,\n * committed to the repository. Declares topology only: which runtime major the\n * project targets, and which apps *this repository* contributes to the project.\n * Schema, security rules, hooks and functions stay in TypeScript under the\n * config package — nothing that needs a type system belongs here.\n *\n * 2. {@link RebaseProjectLink} — the per-checkout link (`.rebase/cloud.json`).\n * **Not committed**, because it is per-developer like a git remote. Says which\n * deployed project this working copy points at, whether that is a Rebase Cloud\n * project or the base URL of a self-hosted backend.\n *\n * 3. {@link RebaseBundleManifest} — `manifest.json` inside a built bundle.\n * **Generated**, never hand-edited. It is the lockfile analogue: the exact\n * contract a built artifact claims to satisfy, which the runtime validates\n * before it boots and a control plane validates before it deploys.\n *\n * A repository declares only the apps it contains. The set of apps belonging to a\n * project is held by the project itself, which is what makes multi-repo projects\n * work: two repositories never need to know about each other, only about the\n * project.\n */\n\nimport type { StorageSourceDefinition } from \"./storage_source\";\nimport type { ResourceGraph } from \"./resources\";\n\n/**\n * Which kind of thing an app is.\n *\n * - `backend` — the collections/hooks/functions that define the project's API.\n * Exactly one per *project* (not per repository); the registry enforces it.\n * - `static` — a pre-built client bundle (SPA, static site), served from the\n * backend process at its declared `path` or from a CDN. The admin panel is\n * one of these: it is an app in the user's repository like any other.\n *\n * That is the whole list. Ownership of the server process is a property of the\n * backend app ({@link RebaseBackendAppConfig.runtime}), not an app type.\n */\nexport type RebaseAppType = \"backend\" | \"static\";\n\n/**\n * The backend app: the project's API surface.\n *\n * Paths are relative to the directory holding `rebase.json`. The defaults match\n * the layout `rebase init` scaffolds, so a stock project may declare simply\n * `{ \"type\": \"backend\", \"runtime\": \"managed\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\n /**\n * Who owns the process this backend runs in.\n *\n * - `managed` — the platform's runtime image boots this project's bundle.\n * You supply collections, functions, crons and schema; Rebase supplies the\n * server.\n * - `custom` — this repository builds its own image and entrypoint. The\n * escape hatch: full control, no managed-runtime guarantees.\n *\n * Independent of *where* it runs. Both run on Rebase Cloud and both\n * self-host — the destination lives in `.rebase/cloud.json`, not here. See\n * `infra/docker/docker-compose.selfhost.yml`, which boots a managed bundle on a\n * developer's own Docker host.\n *\n * This is authored rather than inferred on purpose. It is the single most\n * consequential fact about a deployment, and inferring it is what used to\n * land projects on the custom runtime without anyone choosing it.\n */\n runtime: \"managed\" | \"custom\";\n /** Directory of the config package (collections + index). Default `config`. */\n config?: string;\n /** Directory of server functions. Default `backend/functions`. */\n functions?: string;\n /** Directory of cron job definitions. Default `backend/crons` when present. */\n crons?: string;\n /**\n * Path to the generated Drizzle schema module (tables/enums/relations).\n * Default `backend/src/schema.generated.ts`.\n */\n schema?: string;\n /**\n * Module path (relative to `config`) exporting the auth users collection as\n * its default export. Default `collections/users`.\n */\n usersCollection?: string;\n\n /**\n * `runtime: \"custom\"` only. Dockerfile path relative to the repository root.\n * Default `Dockerfile`.\n */\n dockerfile?: string;\n /**\n * `runtime: \"custom\"` only. Directory handed to `docker build` as the build\n * context, relative to the directory holding `rebase.json`. Default `.`.\n *\n * The one path in this file allowed to point **above** the project. Every\n * other one names something Rebase reads, and those must be inside the\n * project or a bundle cannot carry them; this names something Rebase never\n * opens. In a workspace repository it normally has to be the workspace root\n * (`\"..\"`), because the lockfile and sibling packages a Dockerfile copies do\n * not live beside `rebase.json`.\n *\n * {@link RebaseBackendAppConfig.dockerfile} stays relative to `rebase.json`\n * whatever this is — it names a file in this repository, and moving the\n * context should not rewrite it. `rebase build` re-expresses it against the\n * context when it prints the command, because `docker build -f` resolves\n * against the working directory rather than the context.\n */\n context?: string;\n /** `runtime: \"custom\"` only. Port the container listens on. Default 8080. */\n port?: number;\n}\n\n/**\n * A static client bundle — SPA or static site — built here and served at `path`.\n */\nexport interface RebaseStaticAppConfig {\n type: \"static\";\n /** Package directory containing the client sources. */\n root: string;\n /** Command that produces `output`. Run from the repository root. */\n build?: string;\n /** Directory of built assets, relative to the repository root. */\n output: string;\n /**\n * Public base path this app is served under. Default `/`.\n *\n * Several static apps run in one process, each at its own path — the API at\n * `/api`, a site at `/`, the admin at `/admin` — which is what keeps a\n * self-hosted deployment a single container.\n *\n * **This is a build-time input, not only a serving concern.** An app mounted\n * at `/admin` must be *built* for `/admin` (Vite's `base`), or `index.html`\n * loads and every asset 404s: a blank page with no server error. `rebase\n * build` passes it as `REBASE_APP_BASE` and asserts the emitted HTML honours\n * it. Changing this value requires rebuilding the app.\n */\n path?: string;\n /**\n * Serve `index.html` for unmatched paths under `path` (client-side routing).\n * Default `true` — the overwhelmingly common case for a client app, and a\n * static *site* generator emits real files for its routes anyway.\n */\n spa?: boolean;\n /**\n * Where this app mounts the Rebase CMS, as a URL path — the address you\n * would type to reach it, not a path relative to `path`.\n *\n * The CMS is an ordinary React component in the developer's own app\n * (`<RebaseCMS basePath=\"/admin\">`), so its address is a *client-side\n * route*: nothing on the server, in the bundle, or in the control plane can\n * observe it. A project whose CMS sits at `/admin` inside a frontend that\n * also serves a product at `/` is indistinguishable, from the outside, from\n * one that has no CMS at all — which is exactly how a Rebase Cloud project\n * came to have no discoverable admin URL anywhere in its console.\n *\n * Declaring it is the only way that fact travels. It is carried into the\n * bundle manifest, recorded on the project's app row at deploy, and is what\n * lets the console (and `rebase apps list`) offer a link straight to it.\n *\n * Must be `path` itself or something beneath it, since the app serving that\n * URL is the one that has to answer for it. Absent means this app does not\n * mount the CMS — the common case for a marketing site or a product app.\n *\n * @example \"/\" — the whole app is the CMS, as `rebase init` scaffolds it\n * @example \"/admin\" — the CMS is one route of a larger app\n */\n cms?: string;\n}\n\nexport type RebaseAppConfig = RebaseBackendAppConfig | RebaseStaticAppConfig;\n\n/**\n * Path prefixes the backend owns, which no static app may claim.\n *\n * One process — and, on the platform, one hostname — serves both the API and\n * however many static apps a project has. Mounting is longest-path-first, so an\n * app declaring `/api` would win against the API itself and every request to it\n * would be answered with that app's `index.html`: a 200 carrying HTML where the\n * caller expected JSON, from a project that looks deployed and healthy.\n *\n * Declared here rather than in either enforcer because both must agree. The CLI\n * checks it so a developer finds out while editing `rebase.json`; the control\n * plane checks it again at deploy intake, because the front door's correctness\n * cannot rest on a check that ran in somebody else's CLI — and a repository can\n * be deployed by a CLI older than this rule.\n */\nexport const RESERVED_BACKEND_PREFIXES = [\"/api\", \"/health\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"] as const;\n\n/**\n * Whether `path` collides with a prefix the backend owns.\n *\n * Compares at segment boundaries, so `/api` and `/api/v2` collide while\n * `/apidocs` does not — the same rule the router matches with, because a check\n * that is stricter than the router rejects paths that would have worked, and one\n * that is looser admits paths that will not.\n */\nexport function reservedPrefixFor(path: string): string | undefined {\n const normalized = path.endsWith(\"/\") && path !== \"/\" ? path.slice(0, -1) : path;\n return RESERVED_BACKEND_PREFIXES.find(\n reserved => normalized === reserved || normalized.startsWith(`${reserved}/`)\n );\n}\n\n/**\n * One declared storage source, as authored in `rebase.json`.\n *\n * The key comes from the enclosing record, so this is\n * {@link StorageSourceDefinition} minus its `key` — the same document the\n * runtime registry and the frontend router consume, expressed the way a JSON\n * object naturally expresses \"a set of named things\".\n */\nexport interface RebaseStorageSourceConfig {\n /** Engine backing this source: `local`, `s3`, `gcs`, or a custom id. */\n engine: string;\n /**\n * How the frontend reaches it. Default `server` (proxied through\n * `/api/storage`). `direct` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n */\n transport?: \"server\" | \"direct\";\n /** Human-readable label for the console and the admin UI. */\n label?: string;\n}\n\n/**\n * `rebase.json` — the authored project manifest.\n */\nexport interface RebaseProjectManifest {\n /** JSON Schema URL, for editor completion. Ignored by the tooling. */\n $schema?: string;\n /**\n * The runtime contract **major** this project targets, as a semver range\n * (e.g. `^1`, `~1.4`, or an exact `1.4.2` to pin).\n *\n * The platform upgrades patches and minors underneath a project without\n * asking; it never crosses a major. See {@link RUNTIME_CONTRACT_VERSION}.\n *\n * Named `rebase` rather than `runtime` so that `runtime` means exactly one\n * thing — {@link RebaseBackendAppConfig.runtime}, who owns the process. It\n * reads like `engines` in a `package.json`, which is what it is.\n */\n rebase: string;\n /**\n * Apps this repository contributes, keyed by app name. The key is the app's\n * identity within the project: it is what `rebase deploy <app>` names, what\n * client credentials are issued against, and what a second repository must\n * not collide with.\n */\n apps: Record<string, RebaseAppConfig>;\n /**\n * Buckets are NOT declared here any more.\n *\n * They were, and the runtime merged this block with the declarations in\n * config code — a bucket named in both had one engine kept and the other\n * silently discarded. Two homes for one concept, with a merge to decide\n * between them, is the shape this whole model replaced.\n *\n * `bucket(\"media\", { engine: \"s3\" })` in the project's config declares one\n * now, and `rebase resources --write` generates `rebase.resources.json`,\n * which is what a host reads before a build. A `storage` block left in this\n * file is refused by the validator, by name, with the replacement in the\n * message — not ignored, because a key that still parses and does nothing\n * is the failure this removed.\n */\n /**\n * Repository-wide opt-out from anonymous CLI usage sharing.\n *\n * **Only `false` does anything.** It suppresses sharing for everyone who\n * clones this repository, overriding each developer's own answer — an\n * organisation setting policy for work done on its behalf, the same shape\n * as a committed `.npmrc`.\n *\n * `true` is deliberately ignored, and the CLI says so rather than obeying\n * quietly. This file is committed, so a `true` here would be one developer\n * answering a privacy question for every colleague who later clones the\n * repo — consent by proxy, which is the exact thing opt-in exists to\n * prevent. Individuals answer at `rebase init`, or with `rebase telemetry enable` / `disable`.\n */\n telemetry?: boolean;\n}\n\n/**\n * The per-checkout project link.\n *\n * Deliberately separate from `rebase.json`: the manifest is committed and shared,\n * while the link is per-developer. Keeping them in one file would mean either\n * committing someone's project id or gitignoring the topology.\n */\nexport interface RebaseProjectLink {\n /**\n * A Rebase Cloud project id, or the base URL of any running Rebase backend\n * (`https://api.example.com`). Both are first-class: every command that\n * accepts a project reference accepts either, so a self-hosted project has\n * the same tooling as a cloud one.\n */\n project: string;\n /** Organization slug. Cloud projects only. */\n org?: string;\n /** Explicit API base URL, when it differs from the project's default. */\n apiUrl?: string;\n}\n\n/**\n * Whether a project can run on the managed runtime, and if not, precisely why.\n *\n * The reasons are returned rather than summarised so tooling can print something\n * a developer can act on. \"Not eligible\" is never a dead end — it selects the\n * custom-runtime path, which still deploys.\n */\nexport interface ManagedCompatibility {\n eligible: boolean;\n reasons: string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Bundle\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Version of the bundle *format* itself.\n *\n * Bumped only when the on-disk layout changes in a way an older runtime could\n * not read. A runtime accepts any bundle whose `bundleFormat` is less than or\n * equal to its own — old bundles keep booting on new runtimes, which is the\n * whole point of separating the artifact from the engine.\n *\n * - **1** — `mode: \"cms\" | \"baas\" | \"static\"`, `entry.static` a single directory\n * string, `entry.admin` for a bundled admin panel.\n * - **2** — `kind: \"backend\" | \"static\"`, `entry.static` a list of\n * {@link RebaseBundleStatic}, `entry.admin` removed. A format-1 runtime reading\n * one of these would find no `mode` and an array where it expects a string, so\n * the bump is what turns that into a refusal to boot instead of a bundle that\n * starts and serves nothing.\n */\nexport const BUNDLE_FORMAT_VERSION = 2;\n\n/**\n * The runtime contract major.\n *\n * Distinct from the `@rebasepro/server` package version: the package may release\n * any number of minors and patches while this stays put. It changes only when\n * the bundle/runtime contract breaks compatibility, and a project's\n * `manifest.runtime` range is matched against *this*.\n *\n * ## v2 — resources are declared, not configured\n *\n * `RebaseBackendConfig.dataSources` and `.storageSources` are gone. A project\n * declares its databases and buckets with `database()` / `bucket()` in its\n * config, and the runtime reads those declarations.\n *\n * This had to be a major, and the reason is the managed tier: it moves projects\n * onto new images WITHOUT rebuilding them. A bundle built against v1 exports\n * those keys, and a v2 runtime refuses them at boot — so without this bump, one\n * image rollout would crash-loop every tenant that had ever declared a second\n * database or bucket, in a wave, with the cause in a container log nobody is\n * watching.\n *\n * With the bump, a v1 bundle on a v2 runtime is refused by\n * `assertBundleCompatibility` with the remedy in the message, and the platform\n * keeps it on a v1 image until it is rebuilt. That is the whole purpose of this\n * number.\n *\n * **Release order matters and is not optional.** The control plane is the side\n * that rejects, so it ships FIRST: raise `SUPPORTED_RUNTIME_CONTRACT` in the\n * saas repo (it rejects only `contract >` its own, so it then accepts both),\n * deploy that, and only then release a runtime implementing v2. Shipping the\n * runtime first turns every deploy into a rejected intake blaming the tenant's\n * bundle.\n */\nexport const RUNTIME_CONTRACT_VERSION = 1;\n\n/** Where the runtime finds each part of the bundle. Paths are bundle-relative. */\nexport interface RebaseBundleEntrypoints {\n /** Compiled config package directory (collections live under it). */\n config?: string;\n /** Compiled collections directory, when it differs from `<config>/collections`. */\n collections?: string;\n /** Compiled functions directory. */\n functions?: string;\n /** Compiled crons directory. */\n crons?: string;\n /** Compiled Drizzle schema module. */\n schema?: string;\n /** Module exporting the auth users collection (default export). */\n usersCollection?: string;\n /**\n * Built static apps to serve from this process, in declaration order.\n *\n * A list rather than a single directory because one process serves several\n * apps at different paths — a site at `/` and the admin at `/admin`. The\n * runtime mounts them longest-path-first so the `/`-rooted app's catch-all\n * does not claim its siblings' URLs.\n */\n static?: RebaseBundleStatic[];\n}\n\n/** One built static app inside a bundle. */\nexport interface RebaseBundleStatic {\n /** Public base path, e.g. `/` or `/admin`. */\n path: string;\n /** Bundle-relative directory holding the built assets. */\n dir: string;\n /** Serve `index.html` for unmatched paths under `path`. */\n spa: boolean;\n /**\n * The app's name in `rebase.json`.\n *\n * `dir` is `static/<name>` and has been since folding was written, so this\n * is recoverable by string surgery — which is precisely why it is stated\n * instead. A control plane reconciling app rows against this list has to\n * match them by name, and a consumer that has to re-derive an identifier\n * from a path is one refactor away from matching nothing and registering a\n * duplicate app on every deploy.\n *\n * Optional because bundles built before this field exists do not carry it;\n * a reader that needs a name falls back to the last segment of `dir`.\n */\n name?: string;\n /**\n * Where this app mounts the Rebase CMS, as a URL path.\n *\n * Copied from the app's declaration — see {@link RebaseStaticAppConfig.cms}\n * for why a client-side route has to be declared to be knowable at all.\n */\n cms?: string;\n}\n\n/**\n * A native module found in the dependency closure.\n *\n * Recorded rather than merely counted so a rejection can name the offending\n * package instead of saying \"something here is native\".\n */\nexport interface NativeDependency {\n name: string;\n /** Why it was flagged — a `.node` binary, a gyp build, or an install script. */\n reason: string;\n}\n\n/**\n * `manifest.json` — generated, and the document the runtime and control plane\n * both validate against.\n */\n/**\n * One custom function, as recorded in a built bundle.\n *\n * @see RebaseBundleManifest.functions\n */\nexport interface RebaseBundleFunction {\n /**\n * The filename without its extension — which is also the URL segment it\n * mounts at (`/api/functions/<name>`), the API-key permission that grants\n * it, and the name `REBASE_FUNCTIONS_ONLY` selects by. One identity, used\n * everywhere.\n */\n name: string;\n /** Path inside the bundle, so a host can point at the file. */\n file: string;\n /**\n * `false` when the function's own source imports a Node built-in or a\n * package that needs one.\n *\n * Descriptive, never a gate: nothing refuses to build or deploy on this. It\n * says where this function *could* run, not where it should.\n */\n portable: boolean;\n /**\n * Why it is not portable — one short phrase per reason, deduplicated.\n * Absent when it is.\n */\n requires?: string[];\n}\n\nexport interface RebaseBundleManifest {\n /** @see BUNDLE_FORMAT_VERSION */\n bundleFormat: number;\n runtime: {\n /** The `runtime` range copied from `rebase.json`. */\n range: string;\n /** Exact `@rebasepro/server` version this bundle was built against. */\n builtAgainst: string;\n /** Runtime contract major this bundle requires. */\n contract: number;\n };\n /**\n * Hash of the compiled collection definitions.\n *\n * This is the contract stamp. A generated SDK records the value it was built\n * from, a client sends it back, and a mismatch is what lets the platform say\n * \"this app was built against an older schema\" instead of failing mysteriously\n * at the first request. It covers collections only — a hook edit does not\n * change a client's contract, so it must not invalidate every SDK.\n */\n schemaVersion: string;\n /** Which app in `rebase.json` this bundle was built from. */\n app: string;\n /**\n * What the runtime does with this bundle.\n *\n * - `backend` — boot the full server: database, auth and the data API, plus\n * any static apps in `entry.static`.\n * - `static` — no backend at all: serve `entry.static` and nothing else. No\n * database, no auth, no data sources. This is how a static app runs on the\n * same image as the backend.\n *\n * Replaces an earlier `mode: \"cms\" | \"baas\" | \"static\"`. The cms/baas\n * distinction was never a third kind of thing — it is simply whether\n * `entry.config` is present, so it is derived rather than declared.\n */\n kind: \"backend\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\n /**\n * Every custom function in the bundle, named and classified.\n *\n * Two things are recorded per function, and both are answers a host would\n * otherwise have to get by importing user code:\n *\n * - **What it is called.** That name is the function's identity everywhere —\n * the URL segment it mounts at, the `functions/<name>` API-key\n * permission, the value `REBASE_FUNCTIONS_ONLY` selects by. A host that\n * wants to give one slow function its own replica count currently has to\n * boot the bundle to discover what is in it.\n * - **Whether it needs Node.** Purely descriptive: a function that opens a\n * file or runs raw SQL is a fine function, and every deployment today is\n * a Node process. It is recorded because the question \"which of these\n * could run somewhere else\" has to be answerable from the artifact, and\n * because answering it per-file after the fact — across a codebase\n * already written — is the expensive version of the same question.\n *\n * Absent on a bundle built before this field existed, which is why every\n * consumer must treat it as optional rather than as an empty list.\n */\n functions?: RebaseBundleFunction[];\n hooks: {\n /**\n * Whether the dependency closure contains native code.\n *\n * The managed runtime refuses these: a prebuilt binary cannot be run on\n * an image the platform did not build it for, and the honest failure is\n * at deploy time rather than at 3am in a crash loop.\n */\n native: boolean;\n nativeModules?: NativeDependency[];\n };\n /**\n * What the bundle's config says about storage access control.\n *\n * Storage is not under RLS and its keys share one flat namespace, so a\n * deployment with file storage enabled and no access model serves every\n * user's files to every signed-in user. The runtime refuses to boot in that\n * state — which, on a hosted platform that enables storage from the *console*\n * rather than from the bundle, surfaces as a crash loop the developer cannot\n * read.\n *\n * Recording it here lets a host reject the deploy with the reason instead.\n * Absent on bundles built before this field existed.\n */\n storage?: {\n /** Whether the config package exports a `storageAuthorize` hook. */\n authorize: boolean;\n /**\n * Buckets, on bundles built before {@link RebaseBundleManifest.resources}.\n *\n * No longer written. A host reads `resources`, which carries every kind\n * in one list; this stays declared so a control plane can keep reading\n * the bundles a project shipped before it was rebuilt.\n */\n sources?: StorageSourceDefinition[];\n };\n /**\n * Everything the project declares it needs — databases, buckets, topics,\n * and whatever kind is registered next.\n *\n * Recorded so a host can tell, from the artifact alone and before starting\n * anything, what a deploy will need provisioned. That question used to be\n * answerable for buckets and for nothing else, because buckets were the\n * only kind written into an artifact — which is how a project's databases\n * became invisible to the platform that runs them.\n *\n * Absent on bundles built before this field existed.\n */\n resources?: ResourceGraph;\n deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\n /**\n * The dependency tree ships *inside* the bundle, already installed.\n *\n * Absent or false means the tree is declared but not present, and\n * whoever boots the bundle has to install it. On the managed runtime that\n * install runs in an init container on **every** pod start — the bundle\n * lives on a volume that is wiped each time — and it is the single\n * largest cost in a managed pod's life: 35–55 seconds of a 40–60 second\n * cold start. Since a pod restarts on every eviction, node failure, OOM\n * and runtime rollout, that number is not a startup detail. It is what an\n * outage costs.\n *\n * Vendoring moves the install to build time, where it happens once. It is\n * skipped when the closure contains native code, because a prebuilt\n * binary is only valid for the platform it was built for — see\n * {@link vendorTarget} for what \"the platform\" means here.\n */\n vendored?: boolean;\n /**\n * What {@link vendored} was resolved for, recorded so a mismatch can be\n * refused rather than discovered at import time.\n *\n * Cross-platform vendoring is safe for pure JavaScript and unsafe for\n * anything compiled, and the boundary between them is not always visible\n * in a dependency list: `esbuild` is pure-JS with a *platform-specific\n * optional dependency* holding the actual binary, so an install run on a\n * developer's Mac silently produces a tree that cannot run on the Linux\n * image. The install therefore resolves optional dependencies for the\n * target explicitly rather than for the machine it runs on, and records\n * the answer here.\n */\n vendorTarget?: {\n /** npm `--os`, e.g. `linux`. */\n os: string;\n /** npm `--cpu`, e.g. `x64`. */\n cpu: string;\n /** Node major the tree was resolved for. */\n node: string;\n };\n };\n build: {\n /** `@rebasepro/cli` version that produced this bundle. */\n cli: string;\n /** Node major the bundle was compiled on. */\n node: string;\n /** ISO-8601. */\n createdAt: string;\n };\n}\n\n/** The contract a running backend serves at `GET /api/meta/contract`. */\nexport interface RebaseProjectContract {\n /** Matches {@link RebaseBundleManifest.schemaVersion}. */\n schemaVersion: string;\n runtime: {\n /** `@rebasepro/server` version currently running. */\n version: string;\n contract: number;\n };\n /** Full collection definitions, serialized — the input to SDK generation. */\n collections: unknown[];\n /** Collection slugs, for cheap inspection without parsing the definitions. */\n collectionSlugs: string[];\n generatedAt: string;\n}\n\n/** Header carrying the schema version an SDK was generated from. */\nexport const SCHEMA_VERSION_HEADER = \"x-rebase-schema\";\n","import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import type { VectorSearchParams } from \"./data_driver\";\nimport type { ComputedSortField, SearchMatch } from \"../types/search\";\nimport { Entity, EntityValues } from \"../types/entities\";\nimport { WhereFilterOp, FieldPath, NonColumnFieldPath, FilterValues, NullsPlacement, OrderBySpec, RelationAggregateSort } from \"../types/filter-operators\";\n\n/**\n * The element type of an array column, and the column's own type otherwise.\n *\n * A generated SDK emits an `array` property as `Array<X>` and a to-many\n * relation as `Array<TargetRow>`, so this is what `array-contains` compares\n * against on either.\n */\nexport type ElementOf<T> = T extends readonly (infer E)[] ? E : T;\n\n/**\n * The `id` of a row-shaped element, and `never` for anything else.\n *\n * A to-many relation is emitted as `Array<TargetRow>`, but the filter compilers\n * compare a relation by **id** — `buildRelationFilterPredicate` in\n * `@rebasepro/server-postgres` unwraps a relation value down to its id — so\n * `where(\"tags\", \"array-contains\", tagId)` is the call that works, and the\n * element type alone would refuse it.\n */\nexport type IdOf<E> = E extends { id: infer I } ? I : never;\n\n/**\n * One member of an array column: its element, or — when the element is a row —\n * that row's id, which is what a relation filter is actually compared against.\n */\nexport type WhereElementOf<T> = ElementOf<T> | IdOf<ElementOf<T>>;\n\n/**\n * The value a given operator takes on a column of type `T`.\n *\n * `WhereValue<T>` was one value type for all sixteen operators, which made\n * `array-contains` uncallable from a generated SDK — it is the one operator\n * whose value is an *element* of the column rather than the column's own type,\n * so on `tags: string[]` it wanted a `string[]` and the documented\n * `.where(\"tags\", \"array-contains\", \"featured\")` was a compile error. The\n * spelling that did compile, `[\"featured\"]`, builds `@> ARRAY[$1]` with the\n * whole array bound as the single element and matches nothing: the correct\n * query rejected, the accepted query silently wrong.\n *\n * The branches mirror `buildSingleFilterCondition` in `@rebasepro/server-postgres`:\n *\n * - `array-contains` → one element of the column (or a related row's id).\n * - `in` / `not-in` / `array-contains-any` → a list of elements; a bare element\n * is read as the one-element list, and `null` is a null check.\n * - `like` / `ilike` / `not-like` / `not-ilike` → a SQL pattern. Always a\n * string, including on numeric and date columns, which the driver casts.\n * - `is-null` / `is-not-null` → nothing; the value is ignored everywhere.\n * - everything else → the column's own type, or `null` for a null comparison.\n *\n * Distributes over `Op`, so a caller holding an unnarrowed `WhereFilterOp`\n * (a dynamic filter UI, say) gets the union of every branch and stays as\n * permissive as it was.\n */\nexport type WhereValueFor<Op extends WhereFilterOp, T> =\n Op extends \"array-contains\"\n ? WhereElementOf<T>\n : Op extends \"in\" | \"not-in\" | \"array-contains-any\"\n ? readonly WhereElementOf<T>[] | WhereElementOf<T> | null\n : Op extends \"like\" | \"ilike\" | \"not-like\" | \"not-ilike\"\n ? string\n : Op extends \"is-null\" | \"is-not-null\"\n ? null | undefined\n : T | null;\n\n/**\n * A group of conditions combined with `and`, `or`, or negated with `not`.\n *\n * ## `not`\n *\n * `not` negates the **conjunction** of its `conditions`: `not(a)` is `NOT a`,\n * and `not(a, b)` is `NOT (a AND b)`. One rule, stated here and applied\n * identically by the wire codec (`or(...)`/`and(...)`/`not(...)` in\n * `@rebasepro/common`), the REST `?not=` parameter and every driver compiler,\n * so a negation means the same thing whichever end writes it.\n *\n * Negation is not expressible by inverting the operators inside the group: SQL\n * three-valued logic makes `NOT (a AND b)` and `(NOT a) OR (NOT b)` differ the\n * moment a NULL is involved, and only one of them is what the caller wrote. It\n * compiles to a real `NOT (...)`.\n */\nexport interface LogicalCondition {\n type: \"and\" | \"or\" | \"not\";\n conditions: (FilterCondition | LogicalCondition)[];\n}\n\nexport interface FilterCondition {\n column: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * How one relation is loaded by {@link FindParams.include}.\n *\n * `true` loads the relation whole. The object form narrows it — the same four\n * knobs a top-level query has, applied to the rows *inside* one relation — and\n * `include` nests, so a query can ask for \"each post's five newest published\n * comments, each with its author\" in one request.\n *\n * ```ts\n * include: {\n * comments: {\n * limit: 5,\n * where: { published: [\"==\", true] },\n * orderBy: [\"created_at\", \"desc\"],\n * include: { author: true }\n * }\n * }\n * ```\n *\n * Nesting is bounded at {@link MAX_INCLUDE_DEPTH} hops. Each hop is another\n * batched query, and the bound is what stops one request from walking a\n * self-referencing relation forever.\n *\n * @group Data\n */\nexport interface IncludeOptions {\n /** Rows to load per parent row. Applied per parent, not across the page. */\n limit?: number;\n /** Filter the related rows, in the same dialect as {@link FindParams.where}. */\n where?: FilterValues<string>;\n /** An `and`/`or`/`not` group over the related rows. */\n logical?: LogicalCondition;\n /**\n * Sort the related rows — the tuple form, or the `field:direction[:nulls]`\n * shorthand the REST `?orderBy=` parameter uses.\n *\n * The string is accepted because this whole object travels over a query\n * string, where a tuple is three characters of JSON heavier for no gain.\n */\n orderBy?: OrderBySpec<string> | string;\n /** Columns of the *related* row to return. `id` is always included. */\n fields?: string[];\n /** Relations of the related row to load in turn. */\n include?: IncludeSpec;\n}\n\n/**\n * The relations a read loads, as a list of (possibly dotted) names or as a\n * tree.\n *\n * - `[\"author\", \"comments.author\"]` — a dotted path is the same thing as the\n * nested object form, spelled flat. It is what the REST `?include=` parameter\n * carries, and the two forms compile to the same request.\n * - `[\"*\"]` — every relation, one hop deep. The admin panel's shape.\n * - `{ comments: { limit: 5, include: { author: true } } }` — the parametrised\n * form.\n *\n * A name that is not a relation of the collection is a **400\n * `UNKNOWN_RELATION`**, not a silent omission: a read that quietly drops an\n * `include` answers 200 with the field missing, which is indistinguishable from\n * a row that genuinely has no related row.\n *\n * @group Data\n */\nexport type IncludeSpec = string[] | Record<string, true | IncludeOptions>;\n\n/**\n * Hops an {@link IncludeSpec} may nest. `comments.author` is two.\n *\n * @group Data\n */\nexport const MAX_INCLUDE_DEPTH = 3;\n\n/**\n * Parameters for querying a collection.\n *\n * ## How the filter parameters combine\n *\n * `where`, `logical`, and `searchString` are **independent** and, when more\n * than one is present, are combined with **AND** — every clause must match.\n * Concretely the backend builds:\n *\n * ```text\n * (where filters, AND-ed together)\n * AND (logical group)\n * AND (searchString matches, OR-ed across searchable columns)\n * ```\n *\n * So `where` does **not** conflict with or override `logical` — they stack.\n * If you need `where` fields OR-ed with each other, move them into `logical`\n * instead. There is no way to OR `where` against `logical`; express anything\n * that isn't a plain AND of the three groups inside a single `logical` tree.\n *\n * ## Pagination precedence\n *\n * `limit`/`offset` and `page` describe the same window two ways. If **both\n * `offset` and `page` are provided, `page` wins** — the backend computes\n * `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the\n * explicit `offset`. Pick one style per query.\n *\n * @group Data\n */\nexport interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Maximum number of items to return.\n *\n * Omit it and the backend applies {@link DEFAULT_LIST_LIMIT}, so a read is\n * never unbounded. Provide it and it must be a whole number between 1 and\n * {@link MAX_LIST_LIMIT}: the backend **rejects** anything else with a 400\n * rather than trimming it to fit, because a page quietly smaller than the\n * one you asked for is indistinguishable from having reached the end of the\n * collection. To read past the ceiling, page with `offset` — or let\n * {@link SDKCollectionClient.iterate} / {@link SDKCollectionClient.findAll}\n * do it for you.\n */\n limit?: number;\n /**\n * Number of items to skip. Ignored when {@link FindParams.page} is also\n * set — `page` takes precedence.\n */\n offset?: number;\n /**\n * Page number (1-indexed), alternative to {@link FindParams.offset}.\n * When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.\n */\n page?: number;\n /**\n * Filter conditions keyed by field name.\n * Each value is a `[WhereFilterOp, value]` tuple or an array of tuples\n * for multiple conditions on the same field. Multiple fields, and multiple\n * tuples on one field, are **AND-ed**; also AND-ed with `logical` and\n * `searchString` when present (see the interface docs).\n *\n * @example\n * { status: [\"==\", \"active\"] }\n * { age: [\">=\", 18] }\n * { role: [\"in\", [\"admin\", \"editor\"]] }\n * { age: [[\">=\", 18], [\"<\", 65]] }\n */\n where?: FilterValues<FieldPath<M>>;\n /**\n * Logical grouping conditions (AND/OR). Use this for anything `where`\n * can't express — notably OR-ing conditions. AND-ed with `where` and\n * `searchString` when present (see the interface docs).\n */\n logical?: LogicalCondition;\n /**\n * Sort order as a `[field, direction]` tuple, or a list of them applied in\n * order of significance — the second key breaks ties on the first, and so on.\n *\n * @example orderBy: [\"created_at\", \"desc\"]\n * @example orderBy: [[\"roles\", \"asc\"], [\"created_at\", \"desc\"]]\n */\n orderBy?: OrderBySpec<FieldPath<M> | ComputedSortField>;\n /**\n * Relations to load into the response — see {@link IncludeSpec}.\n *\n * Not checked against `M` here: a relation name comes from the collection's\n * `relations`, not from its columns, so nothing in a *hand-written* row type\n * can validate one. A **generated** `Database` narrows this to the\n * collection's actual relation keys, recursively — see `rebase codegen`.\n *\n * An unknown name is a 400 `UNKNOWN_RELATION`. It used to be ignored.\n */\n include?: IncludeSpec;\n\n /**\n * Columns to return, instead of all of them.\n *\n * A real column projection: only these columns are read from the database,\n * so a query that needs two fields of a wide row does not pay for the rest.\n * `excludeFromApi` still applies — naming such a column here does not\n * un-hide it — and the primary key is always returned, because a row that\n * cannot be addressed cannot be updated, deleted or paged past.\n *\n * A relation named in {@link FindParams.include} is loaded regardless of\n * whether it appears here; use {@link IncludeOptions.fields} to narrow the\n * columns *within* an included relation.\n */\n fields?: string[];\n\n /**\n * Collapse rows that are identical over the columns being returned.\n *\n * `SELECT DISTINCT` over the projection — so it is only meaningful\n * alongside {@link FindParams.fields}, and with the primary key in the\n * projection (which it always is) every row is already distinct. Pair it\n * with `fields` naming the columns you actually want the distinct values of.\n *\n * `meta.total` counts distinct rows too, so a distinct listing's `hasMore`\n * describes the set it is paging.\n */\n distinct?: boolean;\n\n /**\n * Continue from where a previous page ended — keyset (\"seek\") pagination.\n *\n * The value is the opaque `meta.nextCursor` of the previous response. It\n * encodes the sort keys the query was ordered by and the last row's values\n * for them, so a page picks up strictly after the last row served rather\n * than at a row *count* that concurrent writes have already moved.\n *\n * It has to describe the same query: `after` alongside a different\n * `orderBy` is a 400 `CURSOR_ORDER_MISMATCH` rather than a page of rows\n * seeked in an order nobody asked for. Mutually exclusive with `offset` and\n * `page` for the same reason.\n *\n * Multi-key sorts and nullable keys both work — the comparison is built\n * over every key, in order, with the NULL placement the sort declared.\n */\n after?: string;\n /**\n * Text search string, AND-ed with `where`/`logical`. This is the value\n * behind the query builder's `.search()` method.\n *\n * What it compiles to depends on the collection. By default — matching\n * every collection that has not said otherwise — it is a case-insensitive\n * substring match OR-ed across the collection's top-level `string`\n * properties: it does not reach inside `map` or `array` properties, it does\n * not stem or rank, and it cannot use an index.\n *\n * A Postgres collection that declares a `search` block instead gets a\n * ranked full-text match over exactly the fields it named, and rows come\n * back with a {@link FindParams.orderBy}-able `_score`.\n */\n searchString?: string;\n\n /**\n * Nearest-neighbour search over a `vector` property.\n *\n * Postgres only, and only for a collection that declares a property of\n * type `vector`. Rows come back ordered by distance, closest first, each\n * carrying a `_distance`. Combines with `where` and `logical`, which are\n * applied as filters before the ordering — so this is \"the nearest rows\n * that also match\", not \"the nearest rows, then filtered\".\n *\n * Supplying the query vector is the caller's job: rebase stores and\n * searches embeddings, it does not compute them.\n */\n vectorSearch?: VectorSearchParams;\n\n /**\n * Ask each returned row to explain itself: which declared search fields\n * matched, with a highlighted snippet from each. Populates `_matches`.\n *\n * Off by default because it is not free — one `ts_headline` per declared\n * field per returned row, and `ts_headline` re-parses the document rather\n * than reading the index. Fine for a page of results, not for an export.\n *\n * Ignored unless the collection declares a `search` block and the query\n * carries a `searchString`; there is nothing to explain otherwise.\n */\n searchExplain?: boolean;\n}\n\n/**\n * Paginated response from a collection query.\n * @group Data\n */\nexport interface FindResponse<M extends Record<string, unknown> = Record<string, unknown>> {\n /** Array of entities matching the query */\n data: Entity<M>[];\n /** Pagination metadata */\n meta: {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n };\n}\n\n\n\n/**\n * Fluent query builder for the **admin panel** — resolves to `FindResponse<M>`\n * (Snapshot-wrapped rows).\n *\n * @internal App developers should use {@link SDKQueryBuilderInterface}\n * (flat rows, returned by `client.data.*` / `context.data.*`). This\n * Snapshot-flavored variant backs the admin panel internals only.\n *\n * @group Data\n */\nexport interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;\n where(logicalCondition: LogicalCondition): this;\n orderBy(column: (keyof M & string) | ComputedSortField, direction?: \"asc\" | \"desc\"): this;\n limit(count: number): this;\n offset(count: number): this;\n search(searchString: string, options?: { explain?: boolean }): this;\n\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n *\n * Postgres only, over a property declared as `type: \"vector\"`. Each row\n * comes back with a `_distance`. Any `where` on the same query filters\n * before the ordering; distance decides the order.\n *\n * The query embedding is the caller's to produce.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): this;\n include(...relations: string[]): this;\n find(): Promise<FindResponse<M>>;\n listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;\n}\n\n/**\n * A single collection's CRUD accessor for the **admin panel** — every method\n * resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).\n *\n * @internal App developers do **not** use this. The public, symmetric surface\n * is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`\n * in the SDK and `context.data.products` in framework callbacks. This\n * Snapshot-flavored accessor backs the admin panel view-model only.\n *\n * @group Data\n */\nexport interface CollectionAccessor<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Find multiple records with optional filtering, pagination, and sorting.\n */\n find(params?: FindParams<M>): Promise<FindResponse<M>>;\n\n /**\n * Find a single record by its ID.\n */\n findById(id: string | number): Promise<Entity<M> | undefined>;\n\n /**\n * Create a new record.\n * @param data The entity data to create.\n * @param id Optional specific ID to use for the new record.\n * @returns The created entity\n */\n create(data: Partial<EntityValues<M>>, id?: string | number): Promise<Entity<M>>;\n\n /**\n * Create many records in a single transaction.\n *\n * See {@link SDKCollectionClient.createMany}. Optional: not every driver can\n * write in bulk, and callers should fall back to `create` per record.\n */\n createMany?(\n data: Partial<EntityValues<M>>[],\n options?: { upsert?: boolean; onConflict?: readonly string[] }\n ): Promise<Entity<M>[]>;\n\n /**\n * Update an existing record by ID.\n * @returns The updated entity\n */\n update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;\n\n /**\n * Update many records in a single transaction.\n *\n * See {@link SDKCollectionClient.updateMany}. Optional, as `createMany` is.\n */\n updateMany?(updates: { id: string | number; data: Partial<EntityValues<M>> }[]): Promise<Entity<M>[]>;\n\n /**\n * Delete many records in a single transaction.\n *\n * See {@link SDKCollectionClient.deleteMany}. Optional, as `createMany` is.\n */\n deleteMany?(ids: (string | number)[]): Promise<void>;\n\n /**\n * Delete a record by ID.\n */\n delete(id: string | number): Promise<void>;\n\n /**\n * Subscribe to a collection for real-time updates.\n * Optional method, may not be supported by all implementations (like stateless HTTP clients).\n */\n listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;\n\n /**\n * Subscribe to a single record for real-time updates.\n * Optional method.\n */\n listenById?(id: string | number, onUpdate: (entity: Entity<M> | undefined) => void, onError?: (error: Error) => void): () => void;\n\n /**\n * Count the number of records matching the given filter.\n *\n * Optional on this contract because a data source need not support it, and\n * required on `CollectionClient` — the HTTP implementation always has it.\n * So `client.data.posts.count()` compiles in the browser while the same\n * call through a `context.data` accessor needs `count?.()`, which is the\n * one place the two halves of this API are not interchangeable.\n */\n count?(params?: FindParams<M>): Promise<number>;\n\n /**\n * {@link SDKCollectionClient.aggregate}. Optional here for the same reason\n * `count` is: not every data source can compute one, and the SDK wraps an\n * absent implementation in a stub that says so rather than returning a\n * number nothing counted.\n */\n aggregate?(params: AggregateParams<M>): Promise<AggregateRow[]>;\n\n // Fluent Query Builder\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): QueryBuilderInterface<M>;\n where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;\n orderBy(column: (keyof M & string) | ComputedSortField, direction?: \"asc\" | \"desc\"): QueryBuilderInterface<M>;\n limit(count: number): QueryBuilderInterface<M>;\n offset(count: number): QueryBuilderInterface<M>;\n search(searchString: string, options?: { explain?: boolean }): QueryBuilderInterface<M>;\n\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n *\n * Postgres only, over a property declared as `type: \"vector\"`. Each row\n * comes back with a `_distance`. Any `where` on the same query filters\n * before the ordering; distance decides the order.\n *\n * The query embedding is the caller's to produce.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): QueryBuilderInterface<M>;\n include(...relations: string[]): QueryBuilderInterface<M>;\n}\n\n// =============================================================================\n// SDK-facing types — flat rows, no Entity wrapper\n// =============================================================================\n\n/**\n * Pagination metadata returned with collection queries.\n * @group Data\n */\nexport interface PaginationMeta {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n /**\n * The opaque cursor that continues this listing — pass it back as\n * {@link FindParams.after}.\n *\n * Present whenever there is a next page to describe (`hasMore` is true and\n * the page returned at least one row). Absent on the last page, and absent\n * on a query no cursor can describe (relevance ordering, whose scores are\n * computed per query and are not comparable between two of them).\n *\n * Opaque on purpose: it encodes the sort keys *and* the last row's values\n * for them, and a client that parsed it would be depending on an encoding\n * that exists to be changed.\n */\n nextCursor?: string;\n}\n\n/**\n * Paginated response from a collection query (SDK-facing).\n * Returns flat rows instead of Entity-wrapped objects.\n *\n * @example\n * const { data, meta } = await rebase.data.posts.find();\n * console.log(data[0].title); // direct access — no .values\n * console.log(meta.total);\n *\n * @group Data\n */\nexport interface FindResult<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Flat rows matching the query, each carrying whatever the query computed\n * for it — see {@link QueryComputedFields}.\n */\n data: (M & QueryComputedFields)[];\n /** Pagination metadata */\n meta: PaginationMeta;\n}\n\n/**\n * Values a query attaches to a row that are not columns of it.\n *\n * Both are absent unless the query asked for the thing that produces them, so\n * both are optional — and reading one on a query that did not ask returns\n * `undefined` rather than a wrong number.\n *\n * They live here rather than on the row type because a generated row type\n * describes a *table*, and neither of these is in one. Without this, a caller\n * who sorted by relevance could not then read the relevance.\n *\n * A `type` alias, deliberately, not an `interface`. TypeScript grants an\n * implicit index signature to a type alias and withholds it from an interface,\n * so `Row & QueryComputedFields` stops being assignable to\n * `Record<string, unknown>` the moment this becomes an interface. Seven casts\n * in one downstream app broke on exactly that.\n *\n * @group Data\n */\nexport type QueryComputedFields = {\n /**\n * Relevance, when the collection declares a {@link SearchConfig} and the\n * query carried a search string. Higher is better; the scale is not\n * comparable between two different search strings.\n */\n _score?: number;\n /**\n * Which declared fields matched, and the text around each hit. Present only\n * when the query asked for it — `.search(term, { explain: true })` — because\n * it costs a `ts_headline` per field per row.\n */\n _matches?: SearchMatch[];\n /**\n * Distance to the query vector, when the query used\n * {@link FindParams.vectorSearch}. Lower is closer, and the rows are\n * already ordered by it.\n */\n _distance?: number;\n};\n\n/**\n * One aggregate a query asks for.\n *\n * `count` alone counts rows; every other function names a column, and `count`\n * with a column counts its non-NULL values.\n *\n * The result key is derived rather than chosen: `sum(total)` comes back as\n * `sum_total` and a bare `count()` as `count`. Letting a caller name it would\n * mean checking their name is not also a `groupBy` field — a rule nobody would\n * guess, and a silently overwritten value if it went unchecked.\n *\n * @group Data\n */\nexport type AggregateSelect<M extends Record<string, unknown> = Record<string, unknown>> =\n | { fn: \"count\"; field?: Extract<keyof M, string> }\n | { fn: \"sum\" | \"avg\" | \"min\" | \"max\"; field: Extract<keyof M, string> };\n\n/**\n * One row of an aggregate result: the `groupBy` columns, plus one key per\n * {@link AggregateSelect} under its derived alias.\n *\n * `count`, `sum` and `avg` arrive as numbers — Postgres returns bigint and\n * numeric as strings, and they are parsed once at the driver rather than by\n * every caller. `min`/`max` keep the column's own type.\n *\n * @group Data\n */\nexport type AggregateRow = Record<string, unknown>;\n\n/**\n * What {@link SDKCollectionClient.aggregate} takes: the same narrowing a\n * `find()` takes, minus the parts of it that describe a *page* of rows.\n *\n * `limit` survives and means what it means on the REST route — a bound on the\n * number of **groups**, because grouping by a high-cardinality column is a whole\n * table's worth of rows in one response. It is ignored when there is no\n * `groupBy`, since an ungrouped aggregate is one row.\n *\n * `orderBy`, `include`, `after` and the rest are absent on purpose: an\n * aggregate has no rows to sort, no relations to load and no page to continue.\n * They were silently ignored on the REST route; here they do not typecheck.\n *\n * @group Data\n */\nexport interface AggregateParams<M extends Record<string, unknown> = Record<string, unknown>> {\n /** The aggregates to compute. At least one. */\n select: AggregateSelect<M>[];\n /** Columns to group by. Omit for a single row over everything that matches. */\n groupBy?: Extract<keyof M, string>[];\n /** Filter conditions, as {@link FindParams.where}. */\n where?: FilterValues<FieldPath<M>>;\n /** An `and`/`or`/`not` group, AND-ed with `where`. */\n logical?: LogicalCondition;\n /** Text search, AND-ed with the filters. */\n searchString?: string;\n /** Most groups to return. Ignored without `groupBy`. */\n limit?: number;\n}\n\n/**\n * Which column an iteration seeks on, for keyset (\"seek\") pagination.\n *\n * Either the column name on its own — sorted ascending — or the column plus an\n * explicit direction. The column must be **unique** and must be the column the\n * query is ordered by; see {@link PageWalkOptions.cursor}.\n *\n * @group Data\n */\nexport type CursorSpec<M extends Record<string, unknown> = Record<string, unknown>> =\n | (Extract<keyof M, string>)\n | { field: Extract<keyof M, string>; direction?: \"asc\" | \"desc\" };\n\n/**\n * How {@link SDKCollectionClient.iterate} / {@link SDKCollectionClient.findAll}\n * walk a collection, layered on top of the normal `find()` parameters.\n *\n * @group Data\n */\nexport interface PageWalkOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Rows fetched per request. Defaults to 200; values below 1 are clamped up.\n * This is the request size, not a result cap — the iteration keeps going\n * until the server says there is nothing left.\n */\n pageSize?: number;\n /**\n * Paginate by **seeking on a column** instead of by offset.\n *\n * Offset paging — the default — re-counts rows on every request, so a row\n * inserted or deleted *while the iteration runs* shifts the window and the\n * walk silently skips or repeats rows. Seeking is immune to that: each page\n * asks for rows strictly after the last one seen, so concurrent writes\n * before the cursor cannot move it.\n *\n * Prefer this whenever the collection has a unique, sortable column\n * (typically its primary key). The column must be unique — a repeated value\n * at a page boundary either skips rows or stalls, and the iterator throws\n * rather than looping — and the query is ordered by it, so a `cursor` and a\n * conflicting `orderBy` is an error, not a silent override.\n *\n * Implemented with the parameters `find()` already takes (an `orderBy` plus\n * a `>` / `<` filter on the cursor column), so it works on every transport\n * and needs nothing new from the server.\n *\n * @example\n * for await (const job of client.data.jobs.iterate({ cursor: \"id\" })) { … }\n */\n cursor?: CursorSpec<M>;\n /**\n * Hard ceiling on the number of requests one walk may make, so a server\n * that never stops saying `hasMore` cannot spin forever. Defaults to\n * 10 000 pages; hitting it throws.\n */\n maxPages?: number;\n}\n\n/**\n * Parameters accepted by {@link SDKCollectionClient.iterate} — everything\n * `find()` takes except the window itself (`limit`, `offset`, `page`), which\n * the iterator owns, plus the walk options.\n *\n * @group Data\n */\nexport type IterateParams<M extends Record<string, unknown> = Record<string, unknown>> =\n Omit<FindParams<M>, \"limit\" | \"offset\" | \"page\"> & PageWalkOptions<M>;\n\n/**\n * Parameters accepted by {@link SDKCollectionClient.findAll}: the iteration\n * parameters plus the ceiling that keeps a whole collection from being pulled\n * into memory unnoticed.\n *\n * @group Data\n */\nexport type FindAllParams<M extends Record<string, unknown> = Record<string, unknown>> =\n IterateParams<M> & {\n /**\n * Most rows to materialise. Defaults to 10 000. Exceeding it **throws**\n * — a truncated array returned as if it were the whole answer is the\n * kind of quiet wrong that shows up months later in a report. Pass\n * `Infinity` to opt out deliberately, or use `iterate()` to stream.\n */\n maxRows?: number;\n };\n\n/**\n * Fluent Query Builder Interface for the SDK client.\n * Returns `FindResult<M>` (flat rows) instead of `FindResponse<M>` (Entity-wrapped).\n *\n * @group Data\n */\nexport interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;\n /**\n * Filter on a relation path (`author.name`) or a JSON path\n * (`metadata->>tier`).\n *\n * A separate overload because the value cannot be typed: neither addresses\n * a column of `M`, so there is nothing in a generated row type to check\n * against — the driver resolves the path and refuses what it cannot. The\n * key is still constrained to a *path*, so a mistyped column name does not\n * fall through to here and lose its check.\n *\n * `find({ where })` has accepted both all along ({@link FieldPath}); the\n * builder did not, so the documented relation-path filters were compile\n * errors on a typed client.\n */\n where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): this;\n where(logicalCondition: LogicalCondition): this;\n /**\n * Sort by a column, a relation or JSON path, `_score`, or an aggregate over\n * a to-many relation — the same key set {@link FindParams.orderBy} takes.\n */\n orderBy(\n column: FieldPath<M> | ComputedSortField | RelationAggregateSort,\n direction?: \"asc\" | \"desc\",\n nulls?: NullsPlacement\n ): this;\n limit(count: number): this;\n offset(count: number): this;\n search(searchString: string, options?: { explain?: boolean }): this;\n\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n *\n * Postgres only, over a property declared as `type: \"vector\"`. Each row\n * comes back with a `_distance`. Any `where` on the same query filters\n * before the ordering; distance decides the order.\n *\n * The query embedding is the caller's to produce.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): this;\n /**\n * Load relations — names, dotted paths (`\"comments.author\"`), or the\n * parametrised tree. Repeated calls merge rather than replace.\n */\n include(...relations: (string | IncludeSpec)[]): this;\n /**\n * Return only these columns. A real projection: the columns are what is\n * read from the database, not what survives a trim of the response.\n */\n fields(...columns: (FieldPath<M> | string)[]): this;\n /** `SELECT DISTINCT` over the projection — see {@link FindParams.distinct}. */\n distinct(enabled?: boolean): this;\n /** Continue after a previous page's `meta.nextCursor`. */\n after(cursor: string): this;\n find(): Promise<FindResult<M>>;\n /**\n * Aggregate the rows this query matches instead of returning them.\n *\n * The builder's `where`/`logical`/`search` narrow which rows are\n * aggregated; its `orderBy`, `include` and window do not apply and are\n * ignored, exactly as they are on the REST route.\n */\n aggregate(params: Omit<AggregateParams<M>, \"where\" | \"logical\" | \"searchString\">): Promise<AggregateRow[]>;\n\n /**\n * Page through everything this query matches, one row at a time.\n *\n * The same walker {@link SDKCollectionClient.iterate} uses, so the ceiling\n * on `limit` is not a ceiling on what a query can read. `.limit()` set on\n * the builder becomes the **page size** here, not a total.\n */\n iterate(options?: PageWalkOptions<M>): AsyncIterableIterator<M>;\n\n /**\n * Collect everything this query matches into one array.\n *\n * {@link SDKCollectionClient.findAll}'s `maxRows` guard applies: an\n * unbounded collect is a memory hazard, so it stops and says so rather than\n * growing until the process dies.\n */\n findAll(options?: PageWalkOptions<M> & { maxRows?: number }): Promise<M[]>;\n\n count(): Promise<number>;\n listen(onUpdate: (data: FindResult<M>) => void, onError?: (error: Error) => void): () => void;\n}\n\n/**\n * SDK collection client — returns flat rows, no Entity wrapper.\n *\n * This is the public API surface for app developers using\n * `createRebaseClient()`. admin internals use `CollectionAccessor` instead.\n *\n * Type parameters:\n * - `M` — the **Row** shape returned by reads (`find`, `findById`, `listen`).\n * - `I` — the **Insert** shape accepted by {@link create}. Defaults to\n * `Partial<M>`; the generated SDK supplies a dedicated `Insert` type where\n * required columns are required and auto-generated / read-only columns are\n * omitted, so `create({})` on a table with required fields is a compile error.\n * - `U` — the **Update** shape accepted by {@link update}. Defaults to\n * `Partial<M>`; the generated SDK supplies a dedicated `Update` type.\n *\n * @example\n * const { data: posts } = await rebase.data.posts.find();\n * console.log(posts[0].title); // flat access\n * console.log(posts[0].id); // id at top level\n *\n * const post = await rebase.data.posts.findById(1);\n * console.log(post?.title); // no .values needed\n *\n * @group Data\n */\n/**\n * A change expressed as an operation on the column's current value, rather than\n * as the value to store.\n *\n * `{ views: 5 }` says what the number becomes; `{ views: { $inc: 1 } }` says\n * what happens to it. The difference is the read the caller no longer has to\n * make — and the race that read opens. Two requests that each read `4`, add one\n * and write `5` lose an increment between them; `SET views = views + 1` cannot,\n * because the arithmetic happens inside the statement holding the row lock.\n *\n * Exactly one operator per field. `{ views: { $inc: 1, $push: \"x\" } }` is\n * refused rather than applied in an order the caller cannot see.\n *\n * @group Data\n */\n/**\n * The operator names, as a value.\n *\n * A runtime list beside the type because three layers have to *recognise* an\n * operation, not just accept one: the REST validator, the driver that compiles\n * it, and the offline queue that must refuse to apply one locally. Three copies\n * of four strings is three chances for one of them to miss an operator added to\n * the other two, and the failure is silent in the worst direction — an\n * unrecognised marker is written to the column as a JSON document.\n *\n * @group Data\n */\nexport const FIELD_OPERATORS = [\"$inc\", \"$push\", \"$pull\", \"$merge\"] as const;\n\n/**\n * The key of a {@link BatchRef}. Declared here, beside the field operators,\n * because the two share one namespace: a `$`-prefixed key in a write payload is\n * a marker, and every reader of that namespace has to know all of it.\n *\n * @group Data\n */\nexport const BATCH_REF_KEY = \"$ref\";\n\n/**\n * Whether a value is *trying* to be a field operation — including a misspelled\n * one, which is the case worth catching.\n *\n * Any `$`-prefixed key counts, because `{ $increment: 1 }` written to a number\n * column as a JSON document is the failure this exists to prevent. No collection\n * can declare a column whose value legitimately has a key beginning with `$`: a\n * `map` property's sub-keys are declared, and `$` is not valid in the\n * identifiers the DDL generators emit.\n *\n * The one exception is `{ $ref: … }`, the batch's backward reference. It stands\n * where a *value* goes and is resolved to one before the row is written, so it\n * is not an operation on a column — reading it as a misspelled operator refused\n * every `$ref` in a batch with \"unknown field operator '$ref'\".\n *\n * @group Data\n */\nexport function isFieldOperation(value: unknown): boolean {\n if (typeof value !== \"object\" || value === null || Array.isArray(value) || value instanceof Date) {\n return false;\n }\n const keys = Object.keys(value);\n if (keys.length === 1 && keys[0] === BATCH_REF_KEY) return false;\n return keys.some((key) => key.startsWith(\"$\"));\n}\n\n/** True when any value in a write payload is (or is attempting to be) one. @group Data */\nexport function hasFieldOperation(values: Record<string, unknown> | undefined): boolean {\n return !!values && Object.values(values).some(isFieldOperation);\n}\n\nexport type FieldOperation =\n /** Add to a `number` column; negative to subtract. `SET col = col + n`. */\n | { $inc: number }\n /** Append one value, or each of an array of values, to an `array` column. */\n | { $push: unknown }\n /** Remove every occurrence of a value from an `array` column. */\n | { $pull: unknown }\n /** Shallow-merge an object into a `map` column. `SET col = col || …::jsonb`. */\n | { $merge: Record<string, unknown> };\n\n/**\n * The payload {@link SDKCollectionClient.update} accepts: plain values, field\n * operations, or both in one body.\n *\n * @group Data\n */\nexport type UpdateValues<U> = { [K in keyof U]?: U[K] | FieldOperation };\n\n/**\n * Where an upsert looks for the row it might be replacing.\n *\n * The columns must carry a uniqueness guarantee the database can use as an\n * `ON CONFLICT` target — the primary key, a property with\n * `validation.unique`, or the columns of a declared `unique` index. Anything\n * else is refused with a 400 rather than sent to Postgres, which would answer\n * `there is no unique or exclusion constraint matching the ON CONFLICT\n * specification` from inside a transaction that has already done work.\n *\n * @group Data\n */\nexport interface UpsertOptions extends WriteOptions {\n /** Column names forming the conflict target. Defaults to the primary key. */\n onConflict?: readonly string[];\n}\n\n/**\n * A placeholder standing for a value only the server will know: the id of a row\n * an earlier operation in the same batch creates.\n *\n * `{ \"$ref\": \"order.id\" }` reads the field `id` off the result of the operation\n * that named itself `ref: \"order\"`. Without it a batch cannot express the one\n * thing a cross-collection batch exists for — writing a parent and its children\n * together — because the child's foreign key is not knowable until the parent\n * has been inserted, and splitting the two into separate requests is exactly\n * the non-atomic sequence the batch replaces.\n *\n * Only backward references resolve. `ref` names must be unique within a batch,\n * and an operation may not reference itself or anything after it.\n *\n * @group Data\n */\nexport interface BatchRef {\n /** `<ref name>.<field>`, e.g. `order.id`. */\n $ref: string;\n}\n\n/** One entry of a batch request. @group Data */\nexport type BatchOperation<DB = Record<string, unknown>> = {\n [K in Extract<keyof DB, string>]:\n | {\n op: \"create\";\n collection: K;\n values: { [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef } & Record<string, unknown>;\n /** Name this row so a later operation can reference its columns. */\n ref?: string;\n }\n | {\n op: \"upsert\";\n collection: K;\n values: { [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef } & Record<string, unknown>;\n /** See {@link UpsertOptions.onConflict}. Defaults to the primary key. */\n onConflict?: readonly string[];\n ref?: string;\n }\n | {\n op: \"update\";\n collection: K;\n id: string | number | BatchRef;\n values: { [F in keyof UpdateOf<DB[K]>]?: UpdateOf<DB[K]>[F] | FieldOperation | BatchRef } & Record<string, unknown>;\n ref?: string;\n }\n | {\n op: \"delete\";\n collection: K;\n id: string | number | BatchRef;\n ref?: string;\n };\n}[Extract<keyof DB, string>];\n\n/**\n * What `POST /api/data/_batch` answers with.\n *\n * `data` is aligned to `operations`: the written row for a create, upsert or\n * update, and `null` for a delete — so an index into one is an index into the\n * other, whatever the batch mixed.\n *\n * @group Data\n */\nexport interface BatchResult<R = Record<string, unknown>> {\n data: (R | null)[];\n meta: { operations: number };\n}\n\n/**\n * Per-request options for a write.\n * @group Data\n */\nexport interface WriteOptions {\n /**\n * Names this write, so re-sending it is recognised instead of repeated.\n *\n * A client that does not see a response cannot know whether the write\n * committed. Retrying is therefore the only option, and without a key the\n * server has no way to tell a retry from a second, genuinely new write — so\n * it performs it again. On a table with a server-assigned id that is a\n * duplicate row, because the id the client chose was never used.\n *\n * A key names **one** request, not a job. It records the method, the path\n * and the body it was claimed for, so re-sending that exact request replays\n * its answer, while the same key on a different one is refused with\n * `IDEMPOTENCY_KEY_REUSED` (422) rather than silently answered with the\n * first request's result. Pass a fresh key — a uuid — per call; a reusable\n * business id shared by the create and the delete of one import means the\n * second of them never runs.\n *\n * Set by the offline queue on every replay. Honoured for 24 hours and scoped\n * to the authenticated user — an unauthenticated caller has no principal to\n * scope it to, so the key is ignored there. A retry sent while the first\n * attempt is still being answered gets `IDEMPOTENCY_KEY_IN_PROGRESS` (409)\n * and should be sent again. A server that cannot store keys ignores the\n * header rather than refusing the write.\n */\n idempotencyKey?: string;\n\n /**\n * Whether the server should send the written row back.\n *\n * `false` sends `Prefer: return=minimal`, and the write answers `204 No\n * Content` — `200` carrying the ids only, for a batch. The row is the\n * default because it carries what the server decided: a serial id, an\n * `autoValue` timestamp, whatever `beforeSave` rewrote. A caller that\n * needs none of that is paying for a full row serialisation and, on\n * Postgres, a read-back per written row.\n *\n * Reach for it on imports and fire-and-forget writes. The method resolves\n * to `undefined` (or `[]`) when it is set, so a caller cannot accidentally\n * use a row the server never sent.\n */\n returning?: boolean;\n\n /**\n * The version of the row this write was made against, so it is refused if\n * the row has moved on.\n *\n * The `ETag` from the read that produced the row — `etagOf(row)` on a row\n * from `findById`, or the `ETag` response header. A mismatch answers `412`\n * rather than writing, which is the difference between \"update the row I\n * read\" and \"overwrite whatever is there now\". Without it a read, an edit\n * and a write is last-writer-wins over everything the write did not send,\n * and the loser is told nothing.\n *\n * `\"*\"` asserts only that the row exists.\n *\n * Honoured on `update` and `delete`.\n */\n ifMatch?: string;\n}\n\nexport interface SDKCollectionClient<\n M extends Record<string, unknown> = Record<string, unknown>,\n I = Partial<M>,\n U = Partial<M>\n> {\n /**\n * Find multiple records with optional filtering, pagination, and sorting.\n *\n * ## What a list method returns\n *\n * Two shapes, and one rule that tells them apart: **a window is wrapped, a\n * whole answer is not.**\n *\n * - `find()` and `listen()` return {@link FindResult} — `{ data, meta }` —\n * because they hand back *one page*. `meta.total` and `meta.hasMore` are\n * the caller's only way to know there is more, so a bare array would lose\n * the answer to the question the call raises.\n * - `findAll()`, `createMany()` and `updateMany()` return a plain `M[]`,\n * because there is nothing left over to report: the walk finished, or the\n * batch is exactly the rows that were written. A `meta` there would be\n * `{ total: rows.length, hasMore: false }`, which says nothing.\n * - `iterate()` yields rows one at a time and never materialises a list at\n * all.\n *\n * So `data` is not a wrapper the SDK sometimes adds and sometimes forgets —\n * it is where the pagination metadata lives, and it is present exactly when\n * there is some.\n */\n find(params?: FindParams<M>): Promise<FindResult<M>>;\n\n /**\n * Walk every record matching a query, one row at a time, fetching pages as\n * the consumer consumes them.\n *\n * This is the pagination primitive: `find()` returns one window, `iterate()`\n * returns all of them without the caller hand-rolling the\n * `limit` / `offset += ` / \"am I done yet\" loop. Nothing is buffered — rows\n * are yielded as each page arrives, so a million-row walk costs one page of\n * memory. `break` stops the walk and no further requests are made.\n *\n * Termination is driven by the server's `meta.hasMore`, never by comparing\n * a page's length against the requested limit — a final page that happens\n * to be exactly full is indistinguishable that way, and a walk that stops\n * there drops rows. An empty page also ends the walk, and\n * {@link PageWalkOptions.maxPages} bounds a server that never stops saying\n * there is more.\n *\n * ## Consistency\n *\n * By default this pages by **offset**, which is only as stable as the table\n * is still: a row inserted or deleted ahead of the cursor between two\n * requests shifts every later window, so the walk can skip a row or hand\n * back the same one twice. That is inherent to offset paging, not a bug\n * here. On a collection with a unique sortable column, pass\n * {@link PageWalkOptions.cursor} to seek on it instead — the walk then\n * asks for rows strictly after the last one it saw, which concurrent writes\n * cannot perturb.\n *\n * @example\n * for await (const job of client.data.jobs.iterate({\n * where: { status: [\"==\", \"queued\"] },\n * cursor: \"id\",\n * pageSize: 500\n * })) {\n * await handle(job);\n * }\n */\n iterate(params?: IterateParams<M>): AsyncIterableIterator<M>;\n\n /**\n * {@link iterate}, collected into an array.\n *\n * Convenient when the result is known to be small and awkward to stream.\n * Because \"known to be small\" is an assumption and not a fact, the result is\n * capped — 10 000 rows by default — and going over the cap **throws**\n * rather than returning a short array that reads like a complete one. Raise\n * {@link FindAllParams.maxRows} when the data really is bigger, or switch to\n * `iterate()` and stream it.\n *\n * The offset-drift caveat on {@link iterate} applies here too.\n *\n * @throws When more rows match than `maxRows` allows.\n *\n * @example\n * const overdue = await client.data.invoices.findAll({\n * where: { due_at: [\"<\", today] },\n * cursor: \"id\"\n * });\n */\n findAll(params?: FindAllParams<M>): Promise<M[]>;\n\n /**\n * Find a single record by its ID.\n */\n findById(id: string | number): Promise<M | undefined>;\n\n /**\n * Read one record by its ID, or throw if it is not there.\n *\n * The counterpart to {@link findById}, and the one most reads want. A row\n * fetched by an id that came from a link, a route parameter or another row\n * is expected to exist; when it does not, that is the error case, not a\n * value to thread through the rest of the function.\n *\n * `findById` returns `M | undefined`, so every caller had to prove the row\n * existed before touching a field:\n *\n * ```ts\n * const post = await rebase.data.posts.findById(id);\n * post.title; // TS18048: 'post' is possibly 'undefined'\n * const ok = (await rebase.data.posts.findById(id))!.title; // the `!` everyone reaches for\n * ```\n *\n * With `get`, the absent case is an exception with a code you can branch on,\n * and the happy path is typed as present:\n *\n * ```ts\n * const post = await rebase.data.posts.get(id); // M, not M | undefined\n * ```\n *\n * Same split as Prisma's `findUnique` / `findUniqueOrThrow`: two contracts,\n * both wanted, named so the choice is visible at the call site.\n *\n * @throws {RebaseApiError} `NOT_FOUND` (status 404) when no such row exists,\n * or is visible to the caller — row-level security makes a row the caller\n * may not read indistinguishable from one that is not there, deliberately.\n */\n get(id: string | number): Promise<M>;\n\n /**\n * Create a new record.\n * @param data The record data to create (the collection's `Insert` shape).\n * @param id Optional specific id, sent as an `id` column. This is for tables\n * whose key *is* `id`: the value goes in as that column. For a table keyed\n * on anything else (a `sku`, a composite key), there is no `id` column to\n * receive it — put the key in `data` instead, where it belongs among the\n * columns.\n * @returns The created row\n */\n create(data: I, id?: string | number, options?: WriteOptions): Promise<M>;\n\n /**\n * Write many records in a single request and a single transaction.\n *\n * Built for imports and ETL, where one call per row means one HTTP round\n * trip and one transaction per row. Every record still runs the normal\n * pipeline — callbacks, relations, row-level security — and the batch is\n * all-or-nothing: if any record is rejected, none of them land and the\n * error names the offending index.\n *\n * A record carrying its primary key updates that row; one without inserts.\n * With `{ upsert: true }` each record is written as INSERT ... ON CONFLICT\n * DO UPDATE on the primary key instead, which is what makes a re-runnable\n * import idempotent.\n *\n * Batches are capped server-side (1000 rows by default) because one batch\n * holds its locks for the whole transaction — chunk larger jobs.\n *\n * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.\n * A client that never sees the response cannot know whether the batch\n * committed, and without a key the server cannot tell the retry from a\n * second genuine import — so it performs it again, duplicating every row in\n * the batch rather than just one.\n *\n * @returns The written rows, in the order given.\n *\n * @example\n * ```ts\n * for (const chunk of chunks(rows, 1000)) {\n * await client.data.products.createMany(chunk, { upsert: true });\n * }\n * ```\n */\n createMany(data: I[], options?: { upsert?: boolean; onConflict?: readonly string[] } & WriteOptions): Promise<M[]>;\n\n /**\n * Update an existing record by ID.\n * @param data The fields to update (the collection's `Update` shape).\n * @param options Per-request write options — notably `idempotencyKey`.\n * @returns The updated row.\n * @throws {RebaseApiError} with status 404 when the record does not exist.\n *\n * `create`, `createMany`, `updateMany`, `delete` and `deleteMany` all took\n * {@link WriteOptions}; this one did not, so the single-row update was the\n * one write on the surface that could not be made idempotent. A client that\n * never sees the response retries, and without a key the server cannot tell\n * that retry from a second deliberate edit — which on a `PATCH` that\n * increments or appends is a second edit applied.\n */\n update(id: string | number, data: U | UpdateValues<U>, options?: WriteOptions): Promise<M>;\n\n /**\n * Insert the row, or replace the one already occupying its key.\n *\n * `INSERT ... ON CONFLICT DO UPDATE`, in one statement — so unlike a\n * `findById` followed by `create`-or-`update` it cannot lose the race\n * between the two, and unlike `create` it does not fail when the row is\n * already there. That is what makes a re-runnable import idempotent\n * without a key.\n *\n * The conflict target defaults to the primary key. Pass `onConflict` to\n * upsert on a natural key instead — `[\"email\"]`, `[\"tenant_id\", \"slug\"]` —\n * and the columns must carry a uniqueness guarantee the database can use:\n * a property with `validation.unique`, or the columns of a declared\n * `unique` index. Anything else is a 400 rather than a Postgres error\n * raised half-way through a transaction.\n *\n * The `on_create` timestamp of a row that already existed is left alone: a\n * conflict means the row's creation is a fact about the past, and a nightly\n * re-import that reset `createdAt` on everything it touched would take\n * every \"new this week\" query with it.\n *\n * @example\n * ```ts\n * await client.data.users.upsert(\n * { email: \"a@b.c\", name: \"Ada\" },\n * { onConflict: [\"email\"] }\n * );\n * ```\n */\n upsert(data: I, options?: UpsertOptions): Promise<M>;\n\n /**\n * Update many records in a single request and a single transaction.\n *\n * The counterpart to {@link createMany}, and the reason it exists is the\n * same: one call per row means one HTTP round trip and one transaction per\n * row. Every record still runs the normal pipeline — callbacks, relations,\n * row-level security — and the batch is all-or-nothing, so a rejected\n * record leaves none of them written and the error names the offending\n * index.\n *\n * Each entry is `{ id, data }` rather than a flat row carrying its own key.\n * That is deliberate: on a table keyed on something other than `id` — a\n * `sku`, a composite key — a flat row cannot say whether a column is the\n * address or a value to write. Naming the address separately mirrors\n * single-row `update(id, data)` exactly and leaves nothing to infer.\n *\n * An id that matches no row fails the batch with a 404 rather than being\n * skipped, for the same reason `update()` does: silently updating four of\n * five rows is worse than updating none.\n *\n * Batches share `createMany`'s server-side cap (1000 rows by default),\n * because one batch holds its locks for the whole transaction.\n *\n * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.\n * An update replayed in full is naturally idempotent, but one interleaved\n * with another writer's is not — the key is what stops a lost ACK from\n * re-applying a stale batch over newer data.\n *\n * @returns The updated rows, in the order given.\n *\n * @example\n * ```ts\n * await client.data.orders.updateMany([\n * { id: \"o-1\", data: { status: \"shipped\" } },\n * { id: \"o-2\", data: { status: \"shipped\" } }\n * ]);\n * ```\n */\n updateMany(updates: { id: string | number; data: U | UpdateValues<U> }[], options?: WriteOptions): Promise<M[]>;\n\n /**\n * Delete a record by ID.\n * @throws {RebaseApiError} with status 404 when the record does not exist.\n *\n * Takes {@link WriteOptions} like every other write. It did not, so the one\n * mutation that cannot be made safe by repeating it — a delete replayed\n * after the row is gone answers 404, which an offline queue reads as a\n * permanent failure — was also the one that could not carry an\n * `idempotencyKey`.\n */\n delete(id: string | number, options?: WriteOptions): Promise<void>;\n\n /**\n * Delete many records in a single request and a single transaction.\n *\n * Takes ids, not a filter. A filter-shaped bulk delete is a different and\n * far more dangerous operation — the failure mode is an omitted or\n * mistyped condition emptying a table, and it cannot be reviewed at the\n * call site the way an explicit list can. Read first, then pass the ids you\n * meant.\n *\n * `beforeDelete` and `afterDelete` fire per row, exactly as they do for\n * single deletes, and returning `false` from `beforeDelete` fails the batch\n * rather than quietly dropping one row from it. All-or-nothing, so an id\n * that matches no row 404s the whole call.\n *\n * Shares `createMany`'s row cap.\n *\n * @example\n * ```ts\n * const stale = await client.data.sessions.findAll({\n * where: { expires_at: [\"<\", cutoff] }\n * });\n * await client.data.sessions.deleteMany(stale.map(s => s.id as string));\n * ```\n */\n deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;\n\n /**\n * The low-level realtime subscription: raw server pushes, nothing else.\n *\n * **Prefer `observe()`** on a client from `@rebasepro/client`, which wraps\n * this one and is what a UI actually wants — it emits from the local\n * database first when offline is enabled, re-emits on local writes and\n * rollbacks, and de-duplicates emissions so a refresh that changes nothing\n * does not call back. `listen` does none of that; it forwards what the\n * socket sends.\n *\n * Always present. A client that cannot subscribe — one built with\n * `realtime: false`, or on a driver with no `listenCollection` — installs a\n * stub that throws a `RebaseClientError` naming the configuration that\n * would make it work. It used to be optional, which made every call site\n * either write `listen!(…)` or a null check the type system could not tell\n * apart from a real capability question; the answer to *that* question is\n * {@link isUnsupported}, and the answer for ordinary code is to just call\n * it.\n *\n * `observe()` degrades to a single fetch instead of throwing, which is the\n * other reason to reach for it instead.\n */\n listen(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;\n\n /** {@link listen} for a single row. Prefer `observeById()`. */\n listenById(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;\n\n /**\n * Count the number of records matching the given filter.\n *\n * Always present; see {@link listen} for what a transport that cannot serve\n * it does instead.\n */\n count(params?: FindParams<M>): Promise<number>;\n\n /**\n * `count`/`sum`/`avg`/`min`/`max` over the matching rows, optionally\n * grouped — the SDK half of `GET /<collection>/aggregate`.\n *\n * The whole point is not to fetch rows in order to reduce them: \"revenue by\n * status\" over a million orders is one query and one row per status here,\n * and a `findAll()` plus a loop everywhere else — which is wrong under a\n * `limit` and unaffordable without one. It runs through the same\n * request-scoped handle as every other read, so RLS applies to the rows\n * being aggregated.\n *\n * ```ts\n * const rows = await rebase.data.orders.aggregate({\n * select: [{ fn: \"sum\", field: \"total\" }, { fn: \"count\" }],\n * groupBy: [\"status\"],\n * where: { created_at: [\">=\", startOfMonth] }\n * });\n * // [{ status: \"paid\", sum_total: 41822.5, count: 317 }, …]\n * ```\n *\n * Always present; a backend whose driver cannot aggregate answers 501\n * naming the capability rather than an empty result set, which would read\n * as \"nothing matched\".\n */\n aggregate(params: AggregateParams<M>): Promise<AggregateRow[]>;\n\n // Fluent Query Builder\n where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M>;\n /** A relation path (`author.name`) or a JSON path (`metadata->>tier`). */\n where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): SDKQueryBuilderInterface<M>;\n where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;\n orderBy(\n column: FieldPath<M> | ComputedSortField | RelationAggregateSort,\n direction?: \"asc\" | \"desc\",\n nulls?: NullsPlacement\n ): SDKQueryBuilderInterface<M>;\n limit(count: number): SDKQueryBuilderInterface<M>;\n offset(count: number): SDKQueryBuilderInterface<M>;\n search(searchString: string, options?: { explain?: boolean }): SDKQueryBuilderInterface<M>;\n /**\n * Order rows by nearest-neighbour distance to `vector`, closest first.\n * Postgres only, over a `type: \"vector\"` property. See\n * {@link SDKQueryBuilderInterface.vectorSearch}.\n */\n vectorSearch(\n property: string,\n vector: number[],\n options?: { distance?: \"cosine\" | \"l2\" | \"inner_product\"; threshold?: number }\n ): SDKQueryBuilderInterface<M>;\n include(...relations: (string | IncludeSpec)[]): SDKQueryBuilderInterface<M>;\n /** {@link SDKQueryBuilderInterface.fields} */\n fields(...columns: (FieldPath<M> | string)[]): SDKQueryBuilderInterface<M>;\n /** {@link SDKQueryBuilderInterface.distinct} */\n distinct(enabled?: boolean): SDKQueryBuilderInterface<M>;\n /** {@link SDKQueryBuilderInterface.after} */\n after(cursor: string): SDKQueryBuilderInterface<M>;\n}\n\n/**\n * The unified data access object for the **admin panel** (Entity-shaped).\n *\n * Access collections as dynamic properties: `data.products.find(...)`. Each\n * accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the\n * view-model the admin renders. This is what `useData()` / the admin\n * `RebaseContext.data` are backed by.\n *\n * @internal App developers do **not** use this — they use\n * {@link RebaseSdkData} (flat rows), which is what the SDK client and backend\n * `context.data` expose. This Entity-shaped map backs the admin panel only.\n *\n * @group Data\n */\nexport type RebaseData<DB = unknown> = {\n /**\n * Get a collection accessor by slug.\n * Alternative to dynamic property access for cases where\n * the collection name is a variable.\n *\n * @example\n * const accessor = data.collection(\"products\");\n * await accessor.find({ limit: 10 });\n */\n collection<M extends Record<string, unknown> = Record<string, unknown>>(slug: string): CollectionAccessor<M>;\n} & (\n DB extends Record<string, unknown>\n ? { [K in keyof DB]: CollectionAccessor<DB[K] extends { Row: infer R extends Record<string, unknown> } ? R : Record<string, unknown>> }\n : {\n /**\n * Dynamic collection accessor.\n * Access any collection by its slug as a property.\n *\n * The index signature is `CollectionAccessor` alone, for the reason\n * spelled out on {@link RebaseSdkData}: unioning in the `collection`\n * method's own signature is unnecessary across an intersection, and it\n * costs `data.products.find()` — the access this `@example` documents.\n *\n * @example\n * data.products.find({ where: { status: [\"==\", \"published\"] } })\n */\n [collectionSlug: string]: CollectionAccessor;\n }\n);\n\n/**\n * The unified data access object for the **SDK** — flat rows, no Entity wrapper.\n *\n * This is the symmetric developer-facing data API, identical in shape on both\n * sides of the stack:\n * - The frontend SDK client (`client.data.products.find()`)\n * - Backend framework callbacks & scripts (`context.data.products.find()`)\n *\n * Every accessor returns flat rows (the table's columns) via\n * {@link SDKCollectionClient} — access fields directly (`row.title`), never\n * `row.values.title`. The admin uses {@link RebaseData} (Entity) instead.\n *\n * @example\n * // Frontend SDK\n * const { data: posts } = await client.data.posts.find();\n * console.log(posts[0].title); // flat — no .values\n *\n * // Backend callback — identical shape\n * callbacks: {\n * beforeSave: async ({ context }) => {\n * const product = await context.data.products.findById(id);\n * console.log(product?.price); // flat — no .values\n * }\n * }\n *\n * @group Data\n */\n/**\n * Extract the `Row` shape from a generated `Database[slug]` entry, falling\n * back to an open record when the entry is untyped.\n * @group Data\n */\nexport type RowOf<T> = T extends { Row: infer R extends Record<string, unknown> } ? R : Record<string, unknown>;\n\n/**\n * Extract the `Insert` shape from a generated `Database[slug]` entry (the\n * input accepted by `create`), falling back to `Partial<Row>`.\n * @group Data\n */\nexport type InsertOf<T> = T extends { Insert: infer I extends Record<string, unknown> } ? I : Partial<RowOf<T>>;\n\n/**\n * Extract the `Update` shape from a generated `Database[slug]` entry (the\n * input accepted by `update`), falling back to `Partial<Row>`.\n * @group Data\n */\nexport type UpdateOf<T> = T extends { Update: infer U extends Record<string, unknown> } ? U : Partial<RowOf<T>>;\n\n/**\n * Note on the untyped branch below: its index signature is\n * `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.\n *\n * The union looks like it is needed so `collection` — a method on this same\n * object — satisfies the index signature. It is not, because `collection` is\n * declared in a *separate* member of the intersection, and TypeScript only\n * requires named properties to be assignable to an index signature declared\n * alongside them. Including the function arm cost the documented accessor:\n *\n * rebase.dataAsAdmin.projects.find()\n * // ^ Property 'find' does not exist on type\n * // 'SDKCollectionClient | ((slug: string) => …)'\n *\n * Every project without a generated `Database` type lands on this branch, so\n * property-style access — the form used by the `@example` below, by the\n * scaffolded function template, and by the 0.13 migration note — did not\n * compile for any of them. Do not restore the arm; use `collection(slug)` if a\n * caller genuinely needs the by-slug function.\n */\nexport type RebaseSdkData<DB = unknown> = {\n /**\n * Get a flat collection accessor by slug.\n *\n * @example\n * const accessor = data.collection(\"products\");\n * await accessor.find({ limit: 10 });\n */\n collection<M extends Record<string, unknown> = Record<string, unknown>>(slug: string): SDKCollectionClient<M>;\n} & (\n DB extends Record<string, unknown>\n ? { [K in keyof DB]: SDKCollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>> }\n : {\n /**\n * Dynamic flat collection accessor.\n * Access any collection by its slug as a property.\n *\n * @example\n * data.products.find({ where: { status: [\"==\", \"published\"] } })\n */\n [collectionSlug: string]: SDKCollectionClient;\n }\n);\n","import { RebaseApiError } from \"../errors\";\nimport type { CollectionRegistryController } from \"./collection_registry\";\nimport type { EntityStatus, EntityValues } from \"../types/entities\";\nimport type { CollectionConfig, FilterValues } from \"../types/collections\";\nimport type { OrderByTuple } from \"../types/filter-operators\";\nimport type { RebaseCallContext } from \"../call_context\";\nimport type { IncludeSpec, LogicalCondition } from \"./data\";\nimport type { CollectionUpdateMeta } from \"../types/websockets\";\n\n\n/**\n * @internal\n */\nexport interface FetchOneProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n id: string | number;\n databaseId?: string;\n collection?: CollectionConfig<M>\n /**\n * See {@link FetchCollectionProps.withDeleted}. A soft-deleted row is a 404\n * here by default, so `findById` and `find` agree about which rows exist —\n * a row you cannot find in a listing and can still open by id is the kind\n * of inconsistency that makes a feature untrustworthy.\n */\n withDeleted?: boolean | \"only\";\n}\n\n/**\n * @internal\n */\nexport type ListenOneProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchOneProps<M>\n & {\n onUpdate: (row: Record<string, unknown> | null) => void,\n onError?: (error: Error) => void,\n }\n\n/**\n * Configuration for vector similarity search queries.\n * Vector search applies an ORDER BY distance expression and optionally\n * filters results by a distance threshold.\n */\nexport interface VectorSearchParams {\n /** Property name containing the vector column */\n property: string;\n /** Query vector to compare against */\n vector: number[];\n /** Distance function (default: \"cosine\") */\n distance?: \"cosine\" | \"l2\" | \"inner_product\";\n /** Only return results within this distance threshold */\n threshold?: number;\n}\n\n// ── List pagination bounds ────────────────────────────────────────────────\n//\n// Client-driven list reads (REST `GET /<collection>` and the WebSocket\n// `subscribe_collection` message) accept a client-supplied `limit`. Without\n// bounds, an ABSENT limit streams the entire table into memory — a trivial\n// OOM/DoS — and `limit=100000000` (or `limit=0`, historically an unlimited\n// bypass) is honoured verbatim. `resolveClientListLimit` is the single shared\n// enforcement point so every untrusted ingress behaves identically. Trusted\n// server-side callers build fetch options directly and are intentionally NOT\n// bounded here (migrations, admin exports, and CDC refetches may need the full\n// set).\n//\n// A limit the platform will not serve is REFUSED, not quietly shrunk. Clamping\n// answers a request for 100 000 rows with 1 000 of them, and a short page is\n// indistinguishable from \"that is all the data there is\" — which is how a CSV\n// export shipped 50 rows of a 100 000-row collection under a filename that read\n// like the whole thing. `meta.total`/`meta.hasMore` make truncation *detectable*\n// on the REST list response, but only for a caller who thinks to compare what it\n// asked for against what it got, and the WebSocket `collection_update` frame\n// carries neither — so signalling cannot be the answer on every surface and\n// rejecting is. An ABSENT limit still defaults: naming no window is not the same\n// as asking for one that cannot be served.\n\n/** Rows returned for a plain / text-search list read when the client sends no `limit`. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Rows returned for a vector-search list read when the client sends no `limit`. */\nexport const DEFAULT_VECTOR_LIST_LIMIT = 10;\n/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */\nexport const MAX_LIST_LIMIT = 1000;\n\n/** Overridable bounds for {@link resolveClientListLimit}. */\nexport interface ListLimitBounds {\n /** Default page size for plain and text-search reads. */\n defaultLimit?: number;\n /** Default page size for vector-search reads. */\n vectorDefaultLimit?: number;\n /** Largest limit a client may ask for. A larger one is rejected, not clamped. */\n maxLimit?: number;\n}\n\n/**\n * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not\n * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it\n * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.\n *\n * @group Errors\n */\nexport class ListLimitError extends RebaseApiError {\n /** The ceiling that was exceeded — what the caller should page by instead. */\n readonly maxLimit: number;\n\n constructor(message: string, maxLimit: number) {\n super(message, { status: 400, code: \"INVALID_LIMIT\" });\n this.name = \"ListLimitError\";\n this.maxLimit = maxLimit;\n // Keeps `instanceof` working when this is compiled down for an older\n // target, where extending a builtin otherwise loses the prototype.\n Object.setPrototypeOf(this, ListLimitError.prototype);\n }\n}\n\n/**\n * Resolve a client-supplied list `limit` into a safe, always-defined value.\n *\n * - An absent / blank limit falls back to the mode default:\n * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.\n * - A limit that is present must be an integer in `[1, maxLimit]`. Anything\n * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws\n * {@link ListLimitError} rather than being coerced into range, because every\n * coercion answers a question the caller did not ask with a page it cannot\n * tell apart from the whole collection.\n *\n * The return is never `undefined` — no ingress that routes its client limit\n * through this can produce an unbounded read.\n *\n * @throws {ListLimitError} when a present `limit` is not an integer in range.\n */\nexport function resolveClientListLimit(\n rawLimit: number | string | null | undefined,\n opts: ListLimitBounds & { vectorSearch?: boolean } = {}\n): number {\n const maxLimit = opts.maxLimit ?? MAX_LIST_LIMIT;\n if (rawLimit != null && String(rawLimit).trim() !== \"\") {\n // `Number`, not `parseInt`: `parseInt(\"50rows\")` is 50, which silently\n // reads a typo as a window the caller never wrote.\n const parsed = typeof rawLimit === \"number\" ? rawLimit : Number(String(rawLimit).trim());\n if (!Number.isInteger(parsed) || parsed < 1) {\n throw new ListLimitError(\n `Invalid \\`limit\\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`,\n maxLimit\n );\n }\n if (parsed > maxLimit) {\n throw new ListLimitError(\n `\\`limit\\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows ` +\n \"per read and page through the rest with `offset` — answering with a smaller page would be \" +\n \"indistinguishable from there being no more rows.\",\n maxLimit\n );\n }\n return parsed;\n }\n return opts.vectorSearch\n ? (opts.vectorDefaultLimit ?? DEFAULT_VECTOR_LIST_LIMIT)\n : (opts.defaultLimit ?? DEFAULT_LIST_LIMIT);\n}\n\n/**\n * @internal\n */\nexport interface FetchCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n collection?: CollectionConfig<M>;\n filter?: FilterValues<Extract<keyof M, string>>,\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * The REST layer parsed `?or=` into this and then had nowhere to put it, so\n * the group was dropped and the read ran unfiltered — returning every row\n * the caller's policies allowed rather than the ones they asked for.\n */\n logical?: LogicalCondition;\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n /**\n * The sort, in either of two spellings:\n *\n * - a field name, whose direction is the separate `order` below — the\n * original single-column contract, which every existing driver reads;\n * - a list of `[field, direction]` tuples applied in order of significance,\n * which carries a multi-column sort and ignores `order` entirely.\n *\n * `normalizeDriverOrderBy` in `@rebasepro/common` collapses the pair to the\n * list form. A driver that has not been taught the list form should read it\n * through that helper rather than assume a string: handed an array, `String()`\n * would produce a field name like `roles,asc` and the sort would 400 (or,\n * with unknown-field warnings on, silently vanish).\n */\n orderBy?: string | OrderByTuple[];\n searchString?: string;\n /** Ask each row which declared search field matched — populates `_matches`. */\n searchExplain?: boolean;\n /** Direction for the string form of `orderBy`. Ignored when `orderBy` is a list. */\n order?: \"desc\" | \"asc\";\n /** Vector similarity search configuration */\n vectorSearch?: VectorSearchParams;\n /**\n * What to do about rows a soft delete has stamped.\n *\n * Unset (the default) hides them, which is the whole point of the feature:\n * a deleted row is deleted as far as the application is concerned. `true`\n * includes them alongside the live ones — a trash view, an admin audit.\n * `\"only\"` returns nothing but them, which is the trash view proper and is\n * not expressible as a filter, because the field is not part of the\n * caller's vocabulary.\n *\n * Ignored by collections that do not declare {@link\n * PostgresCollectionConfig.softDelete}: there is no stamp to look at, and\n * silently returning nothing for `\"only\"` on such a collection would be a\n * worse answer than ignoring it.\n */\n withDeleted?: boolean | \"only\";\n /**\n * Relations to load — see {@link IncludeSpec}.\n *\n * Absent means *no* relations, the same as it does over REST. It used to be\n * absent from this contract entirely, and the driver's own fetch then loaded\n * every relation of every row unconditionally: `find()` returned a row with\n * a foreign key and `listen()` returned the same row with a nested object\n * where that key was, for the same query.\n */\n include?: IncludeSpec;\n /** Columns to read, as a projection. See `FindParams.fields`. */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n}\n\n/**\n * @internal\n */\nexport type ListenCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchCollectionProps<M> &\n {\n /**\n * Page number (1-indexed), as `FindParams.page`.\n *\n * A subscription could name a `limit` and an `offset` but not a `page`,\n * so a live list on page three had to compute the offset itself — and\n * the two spellings then disagreed about what a page was.\n */\n page?: number;\n onUpdate: (rows: Record<string, unknown>[], meta?: CollectionUpdateMeta) => void;\n onError?: (error: Error) => void;\n };\n\n/**\n * @internal\n */\nexport interface SaveProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n values: Partial<EntityValues<M>>;\n id?: string | number; // can be empty for new entities\n previousValues?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n status: EntityStatus;\n /**\n * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key\n * instead of choosing between insert and update up front.\n *\n * One statement, so it does not lose the race a read-then-write can, and it\n * succeeds whether or not the row is already there — what a re-runnable\n * import needs. Requires every primary key column to be present; without\n * them there is no conflict target and the row is inserted normally.\n */\n upsert?: boolean;\n\n /**\n * The columns the upsert matches a conflict on, instead of the primary key.\n *\n * The key is the only target that always exists, and it is the wrong one\n * for the write an upsert is usually reached for: \"this user, identified by\n * their email, exists with these values\". Keyed on the primary key that is\n * an insert, because the caller does not know the serial id — so the row is\n * duplicated on every run.\n *\n * Only column sets carrying a uniqueness guarantee are legal here; Postgres\n * refuses anything else with 42P10, from inside a transaction. The REST\n * layer checks the target against the collection's declarations first (see\n * `resolveConflictTarget`), so the answer is a 400 naming the available\n * targets rather than a 500 naming a constraint the caller never wrote.\n */\n onConflict?: readonly string[];\n}\n\n/**\n * @internal\n */\nexport interface SaveManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to write. A row carrying its primary key updates (or, with\n * `upsert`, inserts-or-updates) that row; one without inserts.\n */\n rows: Partial<EntityValues<M>>[];\n collection?: CollectionConfig<M>;\n /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */\n upsert?: boolean;\n /** The conflict target for those upserts. See {@link SaveProps.onConflict}. */\n onConflict?: readonly string[];\n}\n\n/**\n * @internal\n */\nexport interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to update, each named by its address.\n *\n * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the\n * values and is insert-shaped — `saveMany` passes `status: \"new\"` and no\n * `id`, so it cannot express \"update exactly this row\". This can, and it is\n * why bulk update is a separate driver method rather than a flag on that one.\n */\n updates: { id: string | number; values: Partial<EntityValues<M>> }[];\n collection?: CollectionConfig<M>;\n}\n\n/**\n * @internal\n */\nexport interface DeleteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n row: { id: string | number; path: string; values?: Partial<EntityValues<M>> };\n collection?: CollectionConfig<M>;\n /**\n * Issue a real `DELETE` on a collection that declares\n * {@link PostgresCollectionConfig.softDelete}.\n *\n * The row and every cascade behind it go. It needs the same permission an\n * ordinary delete does and nothing more: it is the same verb, and a second\n * access-control surface for one operation is a second thing to get wrong.\n * No effect on a collection without soft delete, where every delete is\n * already this one.\n */\n hard?: boolean;\n}\n\n/**\n * @internal\n */\nexport interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n ids: (string | number)[];\n collection?: CollectionConfig<M>;\n /** See {@link DeleteProps.hard}. */\n hard?: boolean;\n}\n\n/**\n * Addressing ONE link of a many-to-many, to set the columns it carries.\n *\n * `path` is the relation on a row — `posts/1/tags` — and `targetId` the row on\n * the far side, so the pair names exactly one junction row. Not a `SaveProps`,\n * because a save at that address means \"write the target row\", and a link's own\n * columns are not the target's: two posts sharing a tag see one tag and two\n * different links.\n *\n * @internal\n */\nexport interface UpdateRelationPivotProps {\n /** The nested relation path, e.g. `posts/1/tags`. */\n path: string;\n /** The far side's key. */\n targetId: string | number;\n /** The junction columns to set, keyed by the property key `through.properties` declares. */\n pivot: Record<string, unknown>;\n}\n\n/**\n * One operation of a {@link DataDriver.batchWrite}.\n *\n * `path` rather than a slug, because a batch entry addresses rows exactly as\n * the single-row props do and a nested path is a legal address there.\n *\n * @internal\n */\nexport interface BatchWriteOperation<M extends Record<string, unknown> = Record<string, unknown>> {\n op: \"create\" | \"update\" | \"upsert\" | \"delete\";\n path: string;\n /** Required for `update` and `delete`. May be a `$ref` marker; see `batchWrite`. */\n id?: unknown;\n values?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n /** See {@link SaveProps.onConflict}. `upsert` only. */\n onConflict?: readonly string[];\n /** Names this operation's result, for a later `$ref`. */\n ref?: string;\n}\n\n/**\n * @internal\n */\nexport interface BatchWriteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n operations: BatchWriteOperation<M>[];\n}\n\nexport type FilterCombinationValidProps = {\n path: string;\n databaseId?: string;\n collection: CollectionConfig;\n filterValues: FilterValues<string>;\n sortBy?: [string, \"asc\" | \"desc\"];\n};\n\n/**\n * The integration SPI for plugging a data backend into Rebase.\n *\n * Implement this interface to connect a custom backend (or use a built-in\n * driver such as the Firestore one) and register it on\n * `<Rebase dataSources>`. Rebase wraps drivers via `buildRebaseData` and\n * routes collections to them by their `dataSource` key.\n *\n * For *consuming* data in application code, use `RebaseData` /\n * `context.data` instead — this interface is only for providing it.\n *\n * @group Datasource\n */\nexport interface DataDriver {\n\n /**\n * Key that identifies this driver\n */\n key?: string;\n\n /**\n * If the driver has been initialised\n */\n initialised?: boolean;\n\n /**\n * Fetch data from a collection\n * @param props\n * @return Promise of flat rows\n */\n fetchCollection<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Listen to a collection in a given path. If you don't implement this method\n * `fetchCollection` will be used instead, with no real time updates.\n * @param props\n * @return Function to cancel subscription\n */\n listenCollection?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenCollectionProps<M>): () => void;\n\n /**\n * Retrieve a single row given a path and a collection\n * @param props\n */\n fetchOne<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Get realtime updates on one row.\n * @param props\n * @return Function to cancel subscription\n */\n listenOne?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenOneProps<M>): () => void;\n\n /**\n * Save a row to the specified path\n * @param props\n */\n save<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;\n\n /**\n * Save many rows as one unit of work.\n *\n * Every row runs the same pipeline as {@link save} — callbacks, relations\n * and row-level security all still apply — but they share a single\n * transaction, so the batch either lands whole or not at all. That, and the\n * single round trip, is what makes importing tens of thousands of rows\n * viable without dropping to raw SQL.\n *\n * Optional: drivers that cannot do this leave it undefined and callers fall\n * back to `save` per row.\n */\n saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Update many rows in one transaction, each addressed by id.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make the\n * batch atomic should not pretend to. The REST layer reports\n * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single\n * writes, which would be neither atomic nor one round trip — the two things\n * a caller reaches for a batch to get.\n */\n updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Delete the row `props.row` addresses.\n *\n * **Resolving means the row is gone because this call removed it.** A\n * delete that matched nothing must reject with a not-found error\n * (`ApiError.notFound`, `statusCode: 404`) rather than resolving quietly.\n *\n * The rule is here rather than in each driver because the two\n * implementations answered differently and each had a test pinning its own\n * habit: Postgres threw, Mongo logged a warning and resolved. Three things\n * decide it in favour of rejecting.\n *\n * The REST layer already says 404 — `DELETE /api/data/<c>/<id>` reads the\n * row before removing it — so a quiet resolve made the driver API disagree\n * with the HTTP API about the same operation, and only in-process\n * `rebase.data` callers could see the difference.\n *\n * A caller cannot tell \"deleted\" from \"there was nothing there\" without it,\n * and those are different facts: one means the caller's model of the data\n * was right, the other that it was stale. Silence hands back the wrong one\n * and the caller carries on.\n *\n * And on a driver with row-level security, \"matched nothing\" is *also* how\n * a policy refusal arrives — Postgres filters `DELETE` through `USING`\n * rather than raising. A driver that resolves on zero rows therefore\n * reports a refused delete as a completed one, which is the defect\n * `explainZeroRowWrite` exists to prevent (see `write-denial.ts`).\n *\n * Conformance for both server drivers lives in\n * `packages/server/test/contract/delete-contract.ts`, run by each driver's\n * own suite against its own database. `packages/firebase`'s Firestore\n * driver does not honour it: `deleteDoc` resolves for a missing document\n * and reporting otherwise would cost a read on every delete. It runs in the\n * browser against Firestore's own semantics rather than behind\n * `rebase.data`, and that exception is stated here rather than left to be\n * discovered.\n */\n delete<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;\n\n /**\n * Delete all entities from a collection.\n * @param path Collection path\n */\n deleteAll?(path: string): Promise<void>;\n\n /**\n * Set the columns ONE many-to-many link carries, leaving the membership\n * alone — `manyToMany`'s `through.properties`.\n *\n * `PATCH /api/data/<c>/<id>/<relation>/<targetId>` with a `_pivot` body\n * reaches this. The membership array cannot express it: sending one element\n * would unlink everything else, and re-sending the whole set to change one\n * value reintroduces the lost update the membership diff exists to avoid.\n *\n * Optional. A driver whose junctions carry nothing but the two keys leaves\n * it undefined, and the REST layer answers `RELATION_PIVOT_UNSUPPORTED`\n * rather than pretending the write landed.\n */\n updateRelationPivot?(props: UpdateRelationPivotProps): Promise<void>;\n\n /**\n * Delete many rows in one transaction, addressed by id.\n *\n * Ids rather than a filter, deliberately — see\n * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.\n */\n deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;\n\n /**\n * Apply a mixed list of writes across collections as one unit of work.\n *\n * The capability `saveMany` and `deleteMany` cannot express between them: a\n * batch that touches two tables. Sent as two requests those can\n * half-succeed, and the recovery — read back, work out which half landed,\n * undo it — is code nobody writes.\n *\n * Every operation runs the pipeline its single-row equivalent runs, in\n * order, in one transaction, under the caller's own role. Operations may\n * carry `{ \"$ref\": \"<name>.<field>\" }` markers in `values` or `id`, which\n * the driver resolves against the rows earlier operations wrote — the\n * driver, because inside the transaction is the only place those rows\n * exist. `@rebasepro/server` exports `resolveBatchRefs` so the resolution\n * is one implementation rather than one per driver.\n *\n * Resolves to one entry per operation, aligned to the input: the written\n * row for a create, update or upsert, and `null` for a delete.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make it\n * atomic must not pretend to. The REST layer answers `BATCH_UNSUPPORTED`\n * rather than falling back to a loop, which would be the non-atomic\n * sequence the caller reached for this to avoid.\n */\n batchWrite?<M extends Record<string, unknown> = Record<string, unknown>>(\n props: BatchWriteProps<M>\n ): Promise<(Record<string, unknown> | null)[]>;\n\n /**\n * Check if the given property is unique in the given collection\n * @param path Collection path\n * @param name of the property\n * @param value\n * @param id\n * @param collection\n * @return `true` if there are no other fields besides the given entity\n */\n checkUniqueField(\n path: string,\n name: string,\n value: unknown,\n id?: string | number,\n collection?: CollectionConfig\n ): Promise<boolean>;\n\n /**\n * Count the number of entities in a collection\n */\n count?<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number>;\n\n /**\n * Check if the given filter combination is valid\n * @param props\n */\n isFilterCombinationValid?(props: Omit<FilterCombinationValidProps, \"collection\"> & {\n databaseId?: string\n }): boolean;\n\n /**\n * Get the object to generate the current time in the driver\n */\n currentTime?: () => unknown;\n\n delegateToCMSModel?: (data: unknown) => unknown;\n\n cmsToDelegateModel?: (data: unknown) => unknown;\n\n initTextSearch?: (props: {\n context: RebaseCallContext,\n path: string,\n databaseId?: string,\n collection: CollectionConfig,\n parentCollectionSlugs?: string[];\n parentEntityIds?: string[];\n }) => Promise<boolean>;\n\n /**\n * Flag to indicate if the driver has requested the initialization of the text search index\n */\n needsInitTextSearch?: boolean;\n\n // ── REST fetch capabilities ─────────────────────────────────────────\n\n /**\n * Optional REST-optimised fetch service. When present, the REST API\n * generator uses these methods instead of the generic `fetchOne` /\n * `fetchCollection` pipeline, enabling include-aware eager-loading.\n */\n restFetchService?: RestFetchService;\n\n // ── Admin capabilities ─────────────────────────────────────────────\n //\n // Admin operations are now modelled as capability-specific interfaces\n // (SQLAdmin, DocumentAdmin, SchemaAdmin) in `@rebasepro/types/backend`.\n //\n // Drivers that support admin features should expose them here.\n // Consumers should use the `isSQLAdmin()`, `isSchemaAdmin()` etc.\n // type guards to safely narrow the type before calling methods.\n\n /**\n * Return the admin capabilities of this driver.\n * @see SQLAdmin\n * @see DocumentAdmin\n * @see SchemaAdmin\n */\n admin?: import(\"../types/backend\").DatabaseAdmin;\n\n}\n\n/**\n * REST-optimised fetch service exposed by drivers that support\n * eager-loading of relations via `include`.\n *\n * The methods return flattened rows — exactly the table's columns, under their\n * own names and with the types the database returned — and included relations\n * inlined as plain nested rows. This is the shape served to app developers\n * through the REST API / SDK client.\n *\n * No synthesized `id`: identity is a primary key, which may be named anything\n * and span several columns, so an address is derived by whoever needs one (see\n * `buildCompositeId`) rather than written into the row on top of the data.\n *\n * @group DataDriver\n */\nexport interface RestFetchService {\n /**\n * Fetch a collection of flattened entities with optional relation includes.\n */\n fetchCollectionForRest(\n collectionPath: string,\n options?: {\n filter?: FilterValues<string>;\n /** An `or(...)`/`and(...)` group, applied alongside `filter`. */\n logical?: LogicalCondition;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n searchString?: string;\n /** Ask each row which declared search fields matched — populates `_matches`. */\n searchExplain?: boolean;\n databaseId?: string;\n vectorSearch?: VectorSearchParams;\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n /**\n * Columns to read. A projection pushed into the SELECT, not a trim\n * of the response — `excludeFromApi` still applies on top, and the\n * primary key is always read whether or not it is named.\n */\n fields?: string[];\n /** `SELECT DISTINCT` over the projection. See `FindParams.distinct`. */\n distinct?: boolean;\n },\n include?: IncludeSpec\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * The opaque cursor that continues a listing after `row`.\n *\n * On the driver rather than the route because deriving it needs the\n * collection's primary key — which may be named anything and span several\n * columns — and that is the driver's knowledge. The route holds the last\n * row and the sort keys and asks for the string.\n *\n * `undefined` where no cursor can describe the page: an ordering with no\n * stored value to compare against (relevance), or a row missing a value for\n * one of the sort keys. The listing then reports no `nextCursor` and the\n * caller pages by offset, which is what it did before cursors existed.\n *\n * Optional: a driver that cannot seek simply never issues one, and\n * `meta.nextCursor` is absent for every read it serves.\n */\n cursorFor?(\n collectionPath: string,\n row: Record<string, unknown>,\n orderBy?: OrderByTuple[]\n ): string | undefined;\n\n /**\n * `count`/`sum`/`avg`/`min`/`max` over the rows a filter selects,\n * optionally grouped.\n *\n * Optional, and the REST route answers 501 where a driver does not\n * implement it — an aggregate is not a thing to approximate, and an empty\n * result set would read as \"nothing matched\".\n *\n * Any implementation **must apply the same row-level authorization as a\n * read**. An aggregate is an efficient way to learn about rows you cannot\n * select, and `count(*)` over a table whose policies would return nothing\n * has to be zero.\n */\n aggregate?(\n collectionPath: string,\n options: {\n aggregates: { fn: \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"; field?: string; alias: string }[];\n groupBy?: string[];\n filter?: FilterValues<string>;\n logical?: LogicalCondition;\n searchString?: string;\n limit?: number;\n /** See {@link FetchCollectionProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch a single flattened entity with optional relation includes.\n */\n fetchOneForRest(\n collectionPath: string,\n id: string | number,\n include?: IncludeSpec,\n databaseId?: string,\n options?: {\n /** See `FetchCollectionProps.fields`. */\n fields?: string[];\n /** See {@link FetchOneProps.withDeleted}. */\n withDeleted?: boolean | \"only\";\n }\n ): Promise<Record<string, unknown> | null>;\n}\n","/**\n * Path prefix that marks an object as **public**. Files stored under this\n * prefix are served without any auth token via a stable, permanent,\n * CDN-cacheable URL (see {@link StorageSource.getSignedUrl}). Shared by the\n * client SDK and the backend so both agree on which objects are public.\n *\n * @group Models\n */\nexport const PUBLIC_STORAGE_PREFIX = \"public/\";\n\n/**\n * True when a storage key/path points at a public object (lives under\n * {@link PUBLIC_STORAGE_PREFIX}). The check is applied to the key *within the\n * bucket* — strip any `bucket/` and `scheme://` prefixes first.\n *\n * @group Models\n */\nexport function isPublicStoragePath(path: string | null | undefined): boolean {\n if (!path) return false;\n let p = path;\n const scheme = p.indexOf(\"://\");\n if (scheme !== -1) p = p.substring(scheme + 3);\n p = p.replace(/^\\/+/, \"\");\n\n // Defense-in-depth: a path containing traversal segments is never public,\n // so an attacker can't reach a private object via `public/../secret`.\n if (p.split(\"/\").some((seg) => seg === \"..\")) return false;\n\n // Public iff the object **key** starts with the public prefix. A single\n // leading `default/` bucket segment is tolerated (the default bucket).\n // A substring match is deliberately NOT used — a private object under a\n // folder literally named `public` (e.g. `reports/public/q3.pdf`) must stay\n // private. Named buckets: pass the key (not `bucket/key`) so the prefix is\n // anchored; otherwise it falls back to a private, token-scoped URL (safe).\n return p.startsWith(PUBLIC_STORAGE_PREFIX) || p.startsWith(`default/${PUBLIC_STORAGE_PREFIX}`);\n}\n\n/**\n * @group Models\n */\nexport interface UploadFileProps {\n file: File,\n key: string,\n metadata?: Record<string, unknown>,\n bucket?: string,\n /**\n * Store this object as **public**: it is placed under\n * {@link PUBLIC_STORAGE_PREFIX} and served via a stable, token-less,\n * permanent URL (safe to persist in a database and cache on a CDN).\n * Defaults to `false` (private, short-lived signed URLs).\n */\n public?: boolean,\n /**\n * Which property this file is being uploaded *for* — the collection's slug\n * and the property path within it (`coverImage`, `meta.avatar`,\n * `gallery` for an array of files).\n *\n * The server reads it to enforce that property's own `storage.maxSize` and\n * `storage.acceptedFiles`, which were declared per property, published in\n * the generated types, rendered by the panel's file picker, and until now\n * enforced by nothing on the server — so a `curl` past the picker put a\n * 40 MB executable in a bucket whose config said \"images, under 200 KB\".\n *\n * Advisory in one direction only. The rules are resolved from the server's\n * own registry by slug, so naming a property can make an upload *stricter*\n * or leave it at the global cap; it can never widen anything.\n *\n * Omitted, the upload is checked against the deployment's global\n * `maxFileSize` exactly as before.\n */\n context?: UploadPropertyContext\n}\n\n/**\n * The property an upload is destined for.\n *\n * @group Models\n */\nexport interface UploadPropertyContext {\n /** The collection's slug, as the server registered it. */\n collection: string;\n /** Dotted path to the property — `coverImage`, `meta.avatar`. */\n property: string;\n}\n\n/**\n * @group Models\n */\nexport interface UploadFileResult {\n /**\n * Storage key including the file name where the file was uploaded.\n */\n key: string;\n /**\n * Bucket where the file was uploaded\n */\n bucket: string;\n\n /**\n * Fully qualified storage URL for the uploaded file.\n *\n * For example: `s3://my-bucket/path/to/file.png`. Every controller in the\n * framework returns one — S3, GCS and local alike — and a caller that stores\n * the reference needs it, so it is part of the result rather than a maybe.\n */\n storageUrl: string;\n}\n\n/**\n * @group Models\n */\nexport interface DownloadConfig {\n /**\n * Temporal url that can be used to download the file\n */\n url: string | null;\n\n metadata?: DownloadMetadata;\n\n fileNotFound?: boolean;\n}\n\n/**\n * The full set of object metadata, including read-only properties.\n * @public\n */\nexport declare interface DownloadMetadata {\n /**\n * The bucket this object is contained in.\n */\n bucket: string;\n /**\n * The full path of this object.\n */\n fullPath: string;\n /**\n * The short name of this object, which is the last component of the full path.\n * For example, if path is 'full/path/image.png', name is 'image.png'.\n */\n name: string;\n /**\n * The size of this object, in bytes.\n */\n size: number;\n /**\n * Type of the uploaded file\n * e.g. \"image/jpeg\"\n */\n contentType: string;\n\n customMetadata: Record<string, unknown>;\n /**\n * Optional short-lived download token (for local/server-mediated storage).\n * Absent for public objects, which need no token.\n */\n token?: string;\n /**\n * Optional remaining lifetime of the token, in seconds.\n */\n tokenExpiresIn?: number;\n /**\n * True when this object is public: it is served without a token via a\n * stable, permanent, CDN-cacheable URL. When set, the client builds a\n * token-less URL and caches it indefinitely.\n */\n public?: boolean;\n}\n\n/**\n * @group Models\n */\nexport interface StorageSource {\n /**\n * Upload an object, specifying a key\n * @param file\n * @param key\n * @param metadata\n * @param bucket\n */\n putObject: ({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps) => Promise<UploadFileResult>;\n\n /**\n * Convert a storage key or URL into a download configuration (signed URL equivalent)\n * @param keyOrUrl\n * @param bucket\n */\n getSignedUrl: (keyOrUrl: string, bucket?: string) => Promise<DownloadConfig>;\n\n /**\n * Get an object from a storage key.\n * It returns null if the object does not exist.\n * @param key\n * @param bucket\n */\n getObject: (key: string, bucket?: string) => Promise<File | null>;\n\n /**\n * Delete an object.\n * @param key\n * @param bucket\n */\n deleteObject: (key: string, bucket?: string) => Promise<void>;\n\n /**\n * List the contents of a prefix.\n * @param prefix\n * @param options\n */\n listObjects: (prefix: string, options?: {\n bucket?: string,\n maxResults?: number,\n pageToken?: string\n }) => Promise<StorageListResult>;\n\n}\n\n/**\n * Result returned by list().\n * @public\n */\nexport declare interface StorageListResult {\n /**\n * References to prefixes (sub-folders). You can call list() on them to\n * get its contents.\n *\n * Folders are implicit based on '/' in the object paths.\n * For example, if a bucket has two objects '/a/b/1' and '/a/b/2', list('/a')\n * will return '/a/b' as a prefix.\n */\n prefixes: StorageReference[];\n /**\n * Objects in this directory.\n * You can call getMetadata() and getDownloadUrl() on them.\n */\n items: StorageReference[];\n /**\n * If set, there might be more results for this list. Use this token to resume the list.\n */\n nextPageToken?: string;\n}\n\n/**\n * Represents a reference to an S3-compatible storage object. Developers can\n * upload, download, and delete objects, as well as get/set object metadata.\n * @public\n */\nexport declare interface StorageReference {\n /**\n * Returns a s3:// URL for this object in the form\n * `s3://<bucket>/<path>/<to>/<object>`\n * @returns The s3:// URL.\n */\n toString(): string;\n\n /**\n * A reference to the root of this object's bucket.\n */\n root: StorageReference;\n /**\n * The name of the bucket containing this reference's object.\n */\n bucket: string;\n /**\n * The full path of this object.\n */\n fullPath: string;\n /**\n * The short name of this object, which is the last component of the full path.\n * For example, if path is 'full/path/image.png', name is 'image.png'.\n */\n name: string;\n\n /**\n * A reference pointing to the parent location of this reference, or null if\n * this reference is the root.\n */\n parent: StorageReference | null;\n}\n"],"mappings":"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"}
|