@rebasepro/types 0.10.1-canary.b1e3dbf → 0.10.1-canary.d8d45b2

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.
@@ -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/collections.ts","../src/types/policy.ts","../src/types/backend.ts","../src/types/channel_bus.ts","../src/types/data_source.ts","../src/types/storage_source.ts","../src/types/component_ref.ts","../src/types/project_manifest.ts","../src/types/collection_contract.ts","../src/types/schema_version.ts","../src/controllers/data_driver.ts","../src/controllers/storage.ts"],"sourcesContent":["/**\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 * Left `undefined` for realtime/WebSocket, network, and client-side\n * logic errors that have no HTTP status.\n */\n status?: number;\n /** Stable, machine-readable error code (e.g. `\"NOT_FOUND\"`, `\"BAD_REQUEST\"`). */\n code?: string;\n /** Structured error payload returned by the server, when present. */\n details?: unknown;\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. */\n readonly code?: string;\n /** Structured error payload from the server, when present. */\n readonly details?: unknown;\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 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 constructor(message: string) {\n super(message);\n this.name = \"RebaseClientError\";\n }\n}\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 * 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 * Canonical sort representation: `[fieldName, direction]`.\n *\n * Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.\n * The colon-string form (`\"field:direction\"`) exists only at the HTTP wire\n * boundary, handled by `serializeOrderBy` / `deserializeOrderBy` in\n * `@rebasepro/common`.\n *\n * Design note: the natural extension for multi-column sort is\n * `OrderByTuple[]` — not implemented yet (server consumes only the first).\n *\n * @group Models\n */\nexport type OrderByTuple<Key extends string = string> = [Key, \"asc\" | \"desc\"];\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 * 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 */\n sort?: OrderByTuple<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 * 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 * 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_TO_CANONICAL as Record<string, WhereFilterOp | undefined>)[op];\n}\n","import React, { Dispatch, SetStateAction } from \"react\";\nimport type { Entity, EntityStatus, EntityValues } from \"./entities\";\nimport type { CollectionCallbacks } from \"./entity_callbacks\";\n\nimport type { EnumValues, Properties, PostgresProperties, FirebaseProperties, MongoProperties } from \"./properties\";\nimport type { ExportConfig } from \"./export_import\";\n\nimport type { User } from \"../users\";\nimport type { RebaseContext } from \"../rebase_context\";\nimport type { Relation } from \"./relations\";\nimport type { EntityCustomView, FormViewConfig } from \"./entity_views\";\nimport type { EntityAction } from \"./entity_actions\";\nimport type { PolicyExpression } from \"./policy\";\nimport type { ComponentRef } from \"./component_ref\";\nimport type { CollectionComponentOverrideMap } from \"./component_overrides\";\nimport type { WhereFilterOp, FilterValues, FilterPreset, OrderByTuple } from \"./filter-operators\";\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 * You can set an alias that will be used internally instead of the collection name.\n * The `slug` value will be used to determine the URL of the collection.\n * Note that you can use this value in reference properties too.\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 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 * Icon for the navigation sidebar or cards.\n */\n icon?: string | React.ReactNode;\n\n /**\n * Navigation group for this collection.\n * Collections sharing the same group name will be visually grouped\n * together in the drawer and home page. If not set, the collection\n * falls into the default \"Views\" group.\n */\n group?: string;\n\n /**\n * Array of entity views that this collection has.\n * Can be an array of `EntityCustomView` or a string representing the key of a global `EntityCustomView`.\n */\n entityViews?: (string | EntityCustomView<Record<string, unknown>>)[];\n\n /**\n * Default preview properties displayed when this collection is referenced to.\n */\n previewProperties?: string[];\n\n /**\n * Properties to display as columns in the list view.\n * If not specified, the list view uses a smart default (Title, Status, Date).\n */\n listProperties?: string[];\n\n /**\n * Title property of the entity. This is the property that will be used\n * as the title in entity related views and references.\n * If not specified, the first property simple text property will be used.\n */\n readonly titleProperty?: Extract<keyof M, string> | (string & {});\n\n /**\n * When editing a entity, you can choose to open the entity in a side dialog\n * or in a full screen dialog. Defaults to `full_screen`.\n */\n openEntityMode?: \"side_panel\" | \"full_screen\" | \"split\" | \"dialog\";\n\n /**\n * Controls what happens when a user clicks on a entity in the collection view.\n * - `\"edit\"` (default): Opens the entity in the edit form.\n * - `\"view\"`: Opens a read-only detail view with an \"Edit\" button.\n */\n defaultEntityAction?: \"view\" | \"edit\";\n\n /**\n * Replace the default entity form with a custom component.\n * The Builder receives the same props as entity view tabs\n * (entity, formContext, collection, etc.) and has full control over the UI.\n *\n * Works in both edit mode and read-only mode (when `defaultEntityAction`\n * is `\"view\"`). In read-only mode, `formContext.readOnly` will be `true`.\n */\n formView?: FormViewConfig;\n\n /**\n * Prevent default actions from being displayed or executed on this collection.\n */\n disableDefaultActions?: (\"edit\" | \"copy\" | \"delete\")[];\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 * 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 = auth.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 /**\n * Order in which the properties are displayed.\n * If you are specifying your collection as code, the order is the same as the\n * one you define in `properties`. Additional columns are added at the\n * end of the list, if the order is not specified.\n *\n * You can use this prop to hide some properties from the table view.\n * Note that if you set this prop, other ways to hide fields, like\n * `hidden` in the property definition, will be ignored.\n * `propertiesOrder` has precedence over `hidden`.\n *\n * Supported entry formats:\n * - For properties, use the property key.\n * - For additional fields, use the field key.\n * - Child collections (Firestore subcollections, or Postgres relations\n * with `many` cardinality) each get a column with id\n * `subcollection:<slug>`, e.g. `subcollection:orders`.\n */\n propertiesOrder?: (Extract<keyof M, string> | (string & {}) | string | `subcollection:${string}`)[];\n\n /**\n * If enabled, content is loaded in batches. If `false` all entities in the\n * collection are loaded. This means that when reaching the end of the\n * collection, the CMS will load more entities.\n * You can specify a number to specify the pagination size (50 by default)\n * Defaults to `true`\n */\n pagination?: boolean | number;\n\n\n selectionEnabled?: boolean;\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 * Pass your own selection controller if you want to control selected\n * entities externally.\n * @see useSelectionController\n */\n selectionController?: SelectionController<M>;\n\n /**\n * Force a filter in this view. If applied, the rest of the filters will\n * be disabled. Filters applied with this prop cannot be changed.\n * e.g. `fixedFilter: { age: [\">\", 18] }`\n * e.g. `fixedFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n readonly fixedFilter?: FilterValues<Extract<keyof M, string> | (string & {})>;\n\n /**\n * Initial filters applied to the collection this collection is related to.\n * Defaults to none. Filters applied with this prop can be changed.\n * e.g. `defaultFilter: { age: [\">\", 18] }`\n * e.g. `defaultFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n readonly defaultFilter?: FilterValues<Extract<keyof M, string> | (string & {})>; // setting FilterValues<M> can break defining collections by code\n\n /**\n * Pre-defined filter presets that appear as quick-access options in the\n * collection toolbar. Each preset applies a set of filters (and\n * optionally a sort order) with a single click.\n *\n * ```ts\n * filterPresets: [\n * {\n * label: \"Shipped this month\",\n * filterValues: {\n * status: [\"==\", \"shipped\"],\n * order_date: [\">=\", new Date(Date.now() - 30 * 86400000)]\n * }\n * }\n * ]\n * ```\n */\n readonly filterPresets?: FilterPreset<Extract<keyof M, string> | (string & {})>[];\n\n /**\n * Default sort applied to this collection.\n * When setting this prop, entities will have a default order\n * applied in the collection.\n * e.g. `sort: [\"order\", \"asc\"]`\n */\n readonly sort?: OrderByTuple<Extract<keyof M, string> | (string & {})>;\n\n /**\n * You can add additional fields to the collection view by implementing\n * an additional field delegate.\n */\n readonly additionalFields?: AdditionalFieldDelegate<M, USER>[];\n\n /**\n * Default size of the rendered collection\n */\n defaultSize?: CollectionSize;\n\n /**\n * Can the elements in this collection be edited inline in the collection\n * view. Even when inline editing is disabled, entities can still be\n * edited in the side panel (subject to `securityRules`).\n */\n inlineEditing?: boolean;\n\n /**\n * Should this collection be hidden from the main navigation panel, if\n * it is at the root level, or in the entity side panel if it's a\n * subcollection.\n * It will still be accessible if you reach the specified path.\n * You can also use this collection as a reference target.\n */\n hideFromNavigation?: boolean;\n\n /**\n * If you want to open custom views or subcollections by default when opening the edit\n * view of a entity, you can specify the path to the view here.\n * The path is relative to the current collection. For example if you have a collection\n * that has a custom view as well as a subcollection that refers to another entity, you can\n * either specify the path to the custom view or the path to the subcollection.\n */\n defaultSelectedView?: string | DefaultSelectedViewBuilder;\n\n /**\n * Should the ID of this collection be hidden from the form view.\n */\n hideIdFromForm?: boolean;\n\n /**\n * Should the ID of this collection be hidden from the grid view.\n */\n hideIdFromCollection?: boolean;\n\n /**\n * If set to true, the form will be auto-saved when the user changes\n * the value of a field.\n * Defaults to false.\n * When a new entity is created, this property can be updated to generated a new ID\n */\n formAutoSave?: boolean;\n\n /**\n *\n */\n exportable?: boolean | ExportConfig<USER>;\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 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 * Width of the side dialog (in pixels) when opening a entity in this collection.\n */\n sideDialogWidth?: number | string;\n\n /**\n * If set to true, the default values of the properties will be applied\n * to the entity every time the entity is updated (not only when created).\n * Defaults to false.\n */\n alwaysApplyDefaultValues?: boolean;\n\n /**\n * If set to true, a tab including the JSON representation of the entity will be included.\n */\n includeJsonView?: boolean;\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` to let unknown keys through to the database, which is what\n * happened before this existed: a typo reached the INSERT and came back as\n * a Postgres error about a column, or — where a column really does exist\n * that the config never declared, populated by a trigger or a default —\n * quietly worked. The second case is the reason for the escape hatch.\n */\n strictWrites?: boolean;\n\n /**\n * Should local changes be backed up in local storage, to prevent data loss on\n * accidental navigations.\n * - `manual_apply`: When the user navigates back to a entity with local changes,\n * they will be prompted to restore the changes.\n * - `auto_apply`: When the user navigates back to a entity with local changes,\n * the changes will be automatically applied.\n * - `false`: Local changes will not be backed up.\n * Defaults to `manual_apply`.\n */\n localChangesBackup?: \"manual_apply\" | \"auto_apply\" | false;\n\n /**\n * Default view mode for displaying this collection.\n * - \"table\": Display entities in a table with inline editing (default)\n * - \"cards\": Display entities as a grid of cards with thumbnails\n * - \"kanban\": Display entities in a Kanban board grouped by a property\n * Defaults to \"table\".\n */\n defaultViewMode?: ViewMode;\n\n /**\n * Which view modes are available for this collection.\n * Possible values: \"table\", \"cards\", \"kanban\".\n * Defaults to all three: [\"table\", \"cards\", \"kanban\"].\n * Note: \"kanban\" will only be available if the collection has at least\n * one string property with `enum` defined, regardless of this setting.\n */\n enabledViews?: ViewMode[];\n\n /**\n * Configuration for Kanban board view mode.\n * When set, the Kanban view mode becomes available.\n */\n kanban?: KanbanConfig<M>;\n\n /**\n * Property key to use for ordering items.\n * Must reference a string/text property. When items are reordered,\n * this property will be updated with lexicographic sort keys\n * (e.g. \"a0\", \"a1\", \"a0V\") using string-based fractional indexing.\n * Used by Kanban view for ordering within columns\n * and can be used for general ordering purposes.\n */\n readonly orderProperty?: Extract<keyof M, string> | (string & {});\n\n /**\n * Actions that can be performed on the entities in this collection.\n */\n entityActions?: EntityAction<M, USER>[];\n\n /**\n * Builder for the collection actions rendered in the toolbar\n */\n Actions?: ComponentRef<CollectionActionsProps>[];\n\n /**\n * The database table name for this collection.\n * Automatically set for PostgreSQL collections.\n * For non-SQL backends, this may be undefined.\n */\n table?: string;\n\n /**\n * Relations defined for this collection.\n * Populated at normalization time from inline relation properties\n * or explicit relation definitions.\n */\n relations?: Relation[];\n\n /**\n * Security rules for this collection (Row Level Security).\n * When defined, the backend enforces access control policies.\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * Collection-scoped component overrides. These take precedence over\n * global overrides set on `<Rebase>`, but only within this collection's\n * views (entity form, detail view, table, empty state, etc.).\n *\n * Only collection-scoped components (like `Entity.Form`, `Collection.EmptyState`,\n * `Collection.Card`, etc.) can be overridden here. App-level components\n * (like `Shell.AppBar`, `HomePage`) can only be overridden at the `<Rebase>` level.\n *\n * @example\n * ```tsx\n * const productsCollection: PostgresCollectionConfig = {\n * name: \"Products\",\n * slug: \"products\",\n * table: \"products\",\n * components: {\n * \"Entity.Form\": { Component: ProductCustomForm },\n * \"Collection.Card\": { Component: ProductCard },\n * },\n * properties: { ... }\n * };\n * ```\n */\n components?: CollectionComponentOverrideMap;\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 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 * - `auth.uid()` — the current user's ID\n * - `auth.roles()` — comma-separated app role IDs\n * - `auth.jwt()` — full JWT claims as JSONB\n */\n securityRules?: readonly SecurityRule[];\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 * Type guard for PostgreSQL collections.\n * Returns true if the collection uses the Postgres engine (or the default engine).\n * @group Models\n */\nexport function isPostgresCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): collection is PostgresCollectionConfig<M, USER> {\n return !collection.engine || collection.engine === \"postgres\";\n}\n\n/**\n * Type guard for Firebase / Firestore collections.\n * @group Models\n */\nexport function isFirebaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): collection is FirebaseCollectionConfig<M, USER> {\n return collection.engine === \"firestore\";\n}\n\n/**\n * Type guard for MongoDB collections.\n * @group Models\n */\nexport function isMongoDBCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): collection is MongoDBCollectionConfig<M, USER> {\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/**\n * Configuration for Kanban board view mode.\n * @group Collections\n */\nexport interface KanbanConfig<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Property key to use for Kanban board columns.\n * Must reference a string property with `enum` values defined.\n * Entities will be grouped into columns based on this property's value.\n * The column order is determined by the order of `enum` values in the property.\n */\n columnProperty: Extract<keyof M, string> | (string & {});\n}\n\n/**\n * View mode for displaying a collection.\n * - \"list\": Simple, clean list view — the classic CMS default\n * - \"table\": Table with inline editing\n * - \"cards\": Grid of visual cards with thumbnails\n * - \"kanban\": Board view grouped by a property\n * @group Collections\n */\nexport type ViewMode = \"list\" | \"table\" | \"cards\" | \"kanban\";\n\n/**\n * Parameter passed to the `Actions` prop in the collection configuration.\n * The component will receive this prop when it is rendered in the collection\n * toolbar.\n *\n * @group Models\n */\nexport interface CollectionActionsProps<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User, EC extends CollectionConfig<M> = CollectionConfig<M>> {\n /**\n * Full collection path of this entity. This is the full path, like\n * `users/1234/addresses`\n */\n path: string;\n\n /**\n * Path of the last collection, like `addresses`\n */\n relativePath: string;\n\n /**\n * Array of the parent path segments like `['users']`\n */\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n\n /**\n * The collection configuration\n */\n collection: EC;\n\n /**\n * Use this controller to get the selected entities and to update the\n * selected entities state.\n */\n selectionController: SelectionController<M>;\n\n /**\n * Use this controller to get the table controller and to update the\n * table controller state.\n */\n tableController: EntityTableController<M>;\n\n /**\n * Context of the app status\n */\n context: RebaseContext<USER>;\n\n /**\n * Count of the entities in this collection.\n * undefined means the count is still loading.\n */\n collectionEntitiesCount?: number;\n\n /**\n * Programmatically open the new-document form for this collection,\n * optionally pre-populating it with initial field values.\n * The form opens in the same mode configured for the collection\n * (side panel, full screen, or split).\n *\n * This is the primary hook for workflows that need to create a document\n * from external data — e.g. fetching content from a URL, importing from\n * a third-party API, or duplicating from another system.\n *\n * @example\n * // Inside a custom CollectionAction component:\n * openNewDocument({ title: \"Fetched title\", body: \"...\" });\n */\n openNewDocument: (defaultValues?: Record<string, unknown>) => void;\n\n}\n\n/**\n * Use this controller to retrieve the selected entities or modify them in\n * an {@link CollectionConfig}\n * @group Models\n */\nexport interface SelectionController<M extends Record<string, unknown> = Record<string, unknown>> {\n selectedEntities: Entity<M>[];\n setSelectedEntities(entities: Entity<M>[]): void;\n setSelectedEntities(action: (prev: Entity<M>[]) => Entity<M>[]): void;\n isEntitySelected(entity: Entity<M>): boolean;\n toggleEntitySelection(entity: Entity<M>, newSelectedState?: boolean): void;\n}\n\n// Canonical filter types — re-exported from the single source-of-truth.\nexport type { WhereFilterOp, FilterValues, WireFilterValues, FilterPreset } from \"./filter-operators\";\n\n\n/**\n * Used to indicate valid filter combinations (e.g. created in Firestore)\n * If the user selects a specific filter/sort combination, the CMS checks if it's\n * valid, otherwise it reverts to the simpler valid case\n * @group Models\n */\nexport type FilterCombination<Key extends string> = Partial<Record<Key, \"asc\" | \"desc\">>;\n\n/**\n * Sizes in which a collection can be rendered\n * @group Models\n */\nexport type CollectionSize = \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\";\n\nexport type AdditionalFieldDelegateProps<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> = {\n entity: Entity<M>,\n context: RebaseContext<USER>\n};\n\n/**\n * Use this interface for adding additional fields to entity collection views and forms.\n * @group Models\n */\nexport interface AdditionalFieldDelegate<M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User> {\n\n /**\n * ID of this column. You can use this id in the `properties` field of the\n * collection in any order you want\n */\n key: string;\n\n /**\n * Header of this column\n */\n name: string;\n\n /**\n * Width of the generated column in pixels\n */\n width?: number;\n\n /**\n * Builder for the custom field\n */\n Builder?(props: { entity: Entity<M>, context: RebaseContext<USER> }): React.ReactNode;\n\n\n /**\n * If this column needs to update dynamically based on other properties,\n * you can define an array of keys as strings with the\n * `dependencies` prop.\n * e.g. [\"name\", \"surname\"]\n * This is a performance optimization, if you don't define dependencies\n * it will be updated in every render.\n */\n dependencies?: NoInfer<Extract<keyof M, string>> | NoInfer<Extract<keyof M, string>>[] | (string & {}) | (string & {})[];\n\n /**\n * Use this prop to define the value of the column as a string or number.\n * This is the value that will be used for exporting the collection.\n * If `Builder` is defined, this prop will be ignored in the collection\n * view.\n * @param entity\n */\n value?(props: {\n entity: Entity<M>,\n context: RebaseContext\n }): string | number | Promise<string | number> | undefined;\n}\n\n\nexport type InferCollectionConfigType<S extends CollectionConfig> = S extends CollectionConfig<infer M> ? M : never;\n\n/**\n * Used in the {@link CollectionConfig#defaultSelectedView} to define the default\n * @group Models\n */\nexport type DefaultSelectedViewBuilder = (params: DefaultSelectedViewParams) => string | undefined;\n\n/**\n * Used in the {@link CollectionConfig#defaultSelectedView} to define the default\n * @group Models\n */\nexport type DefaultSelectedViewParams = {\n status?: EntityStatus;\n entityId?: string | number;\n};\n/**\n * You can use this controller to control the table view of a collection.\n */\nexport type EntityTableController<M extends Record<string, unknown> = Record<string, unknown>> = {\n data: Entity<M>[];\n dataLoading: boolean;\n noMoreToLoad: boolean;\n dataLoadingError?: Error;\n filterValues?: FilterValues<Extract<keyof M, string> | (string & {})>;\n setFilterValues?: (filterValues: FilterValues<Extract<keyof M, string> | (string & {})>) => void;\n sortBy?: [Extract<keyof M, string> | (string & {}), \"asc\" | \"desc\"];\n setSortBy?: (sortBy?: [Extract<keyof M, string> | (string & {}), \"asc\" | \"desc\"]) => void;\n searchString?: string;\n setSearchString?: (searchString?: string) => void;\n clearFilter?: () => void;\n itemCount?: number;\n setItemCount?: (itemCount: number) => void;\n initialScroll?: number;\n onScroll?: (props: {\n scrollDirection: \"forward\" | \"backward\",\n scrollOffset: number,\n scrollUpdateWasRequested: boolean\n }) => void;\n paginationEnabled?: boolean;\n pageSize?: number;\n checkFilterCombination?: (filterValues: FilterValues<string>,\n sortBy?: [string, \"asc\" | \"desc\"]) => boolean;\n popupCell?: SelectedCellProps<M>;\n setPopupCell?: (popupCell?: SelectedCellProps<M>) => void;\n\n onAddColumn?: (column: string) => void;\n}\n\nexport type SelectedCellProps<M extends Record<string, unknown> = Record<string, unknown>> = {\n propertyKey: Extract<keyof M, string> | (string & {});\n cellRect: DOMRect;\n width: number;\n height: number;\n entityPath: string;\n entityId: string | number;\n};\n\n/**\n * SQL operation that a policy applies to.\n * @group Models\n */\nexport type SecurityOperation = \"select\" | \"insert\" | \"update\" | \"delete\" | \"all\";\n\n/**\n * Flexible Row Level Security rule for a collection.\n *\n * Built on PostgreSQL Row Level Security. Rules can range from\n * simple convenience shortcuts to fully custom SQL expressions, giving you the\n * full power of PostgreSQL Row Level Security.\n *\n * The authenticated user's identity is available in raw SQL via:\n * - `auth.uid()` — the user's ID\n * - `auth.roles()` — comma-separated app role IDs\n * - `auth.jwt()` — full JWT claims as JSONB\n *\n * These are set automatically per-transaction by the backend.\n *\n * **How rules combine:** PostgreSQL evaluates all matching policies for an\n * operation. Permissive rules are OR'd together (any one passing is enough).\n * Restrictive rules are AND'd (all must pass). This is standard PostgreSQL RLS behavior.\n *\n * **Mutual exclusivity:** `ownerField`, `access`, structured `condition`, and\n * raw SQL (`using`/`withCheck`) cannot be combined. The type system enforces\n * this — attempting to set conflicting fields will produce a compile-time\n * error.\n *\n * **Which form to reach for:** prefer the structured {@link StructuredSecurityRule}\n * (`condition`/`check`) or the shortcuts (`ownerField`, `access`, `roles`). These\n * are engine-agnostic and evaluated identically by the database and the admin UI,\n * so the UI never shows an action the database will reject. Raw SQL\n * ({@link RawSQLSecurityRule}) keeps full PostgreSQL power but is Postgres-only\n * and server-authoritative (the UI cannot evaluate arbitrary SQL locally).\n *\n * @group Models\n */\nexport type SecurityRule =\n | OwnerSecurityRule\n | PublicSecurityRule\n | StructuredSecurityRule\n | RawSQLSecurityRule\n | RolesOnlySecurityRule;\n\n/**\n * Shared fields for all SecurityRule variants.\n * @group Models\n */\nexport interface SecurityRuleBase {\n /**\n * Optional human-readable name for the policy.\n * If not provided, one will be auto-generated from the table name and operation.\n * Must be unique per table.\n *\n * When using `operations` (array), each generated policy will have the\n * operation name appended, e.g. `\"owner_access_select\"`, `\"owner_access_update\"`.\n */\n name?: string;\n\n /**\n * Which SQL operation this policy applies to.\n * Use this when the policy targets a single operation or all operations.\n *\n * For multiple specific operations, use `operations` (array) instead.\n * If neither is specified, defaults to `\"all\"`.\n *\n * @default \"all\"\n */\n operation?: SecurityOperation;\n\n /**\n * Array of SQL operations this policy applies to.\n * The compiler will generate one PostgreSQL policy per operation, sharing\n * the same configuration.\n *\n * This reduces boilerplate when the same rule applies to multiple (but not all)\n * operations.\n *\n * Takes precedence over `operation` (singular) if both are specified.\n *\n * @example\n * // Same rule for select and update\n * { operations: [\"select\", \"update\"], ownerField: \"user_id\" }\n *\n * @example\n * // Equivalent to operation: \"all\"\n * { operations: [\"all\"], ownerField: \"user_id\" }\n */\n operations?: readonly SecurityOperation[];\n\n /**\n * Whether this policy is `\"permissive\"` (default) or `\"restrictive\"`.\n *\n * - **permissive**: Multiple permissive policies for the same operation are\n * OR'd together — if *any* passes, access is granted.\n * - **restrictive**: Restrictive policies are AND'd with all permissive\n * policies — they act as additional gates that *must* also pass.\n *\n * This is the standard PostgreSQL RLS model.\n *\n * @default \"permissive\"\n */\n mode?: \"permissive\" | \"restrictive\";\n\n /**\n * **Shortcut.** Restrict this rule to users that have one of these\n * application-level roles.\n *\n * **Important:** These are NOT native PostgreSQL database roles — names\n * like `public`, `anon` or `authenticated` belong to {@link pgRoles} and\n * produce a condition no user can satisfy if used here. These are\n * application roles managed by Rebase, stored as an inline `roles TEXT[]`\n * column on the users table, and injected into each transaction as\n * `app.user_roles` — which `auth.roles()` reads.\n *\n * There is no roles registry: a role exists once it is assigned to a user.\n *\n * Generates a safe array-overlap condition — the user passes if they hold\n * *any* of the listed roles:\n * `string_to_array(auth.roles(), ',') && ARRAY['<role1>', '<role2>']`\n *\n * (Note: this is a true set intersection, NOT a regex/substring match, so\n * a role named `admin` never matches `nonadmin` or `superadmin`.)\n *\n * Can be combined with `ownerField`, `access`, `condition`, or raw\n * `using`/`withCheck`. When combined, the role check is AND'd with the\n * other condition.\n *\n * @example\n * // Only admins can delete\n * { operation: \"delete\", roles: [\"admin\"] }\n *\n * @example\n * // Admins have unfiltered read access to all rows\n * { operation: \"select\", roles: [\"admin\"], using: \"true\" }\n */\n roles?: readonly string[];\n\n // ── Advanced: native PostgreSQL role targeting ───────────────────────\n\n /**\n * **Advanced.** Native PostgreSQL database roles the policy applies to.\n *\n * By default, all generated policies target the `public` role (i.e.\n * every database connection). This is correct for most setups where\n * a single database role is used for all connections.\n *\n * **Important:** These are NOT the same as the application-level\n * {@link roles} (admin, editor, viewer, etc.) — those are enforced in the\n * USING/WITH CHECK clauses via `auth.roles()`. This field controls the\n * PostgreSQL `TO` clause in `CREATE POLICY ... TO role_name`.\n *\n * Use this if you have dedicated PostgreSQL roles (e.g. `app_read`,\n * `app_write`) and want policies to target specific ones.\n *\n * @default [\"public\"]\n *\n * @example\n * // Only apply this policy when connected as `app_role`\n * { operation: \"select\", access: \"public\", pgRoles: [\"app_role\"] }\n */\n pgRoles?: readonly string[];\n}\n\n/**\n * Security rule that grants access based on row ownership.\n * Generates a USING/WITH CHECK clause like: `<column> = auth.uid()`\n *\n * Cannot be combined with `using`, `withCheck`, or `access`.\n *\n * @example\n * { operation: \"all\", ownerField: \"user_id\" }\n *\n * @group Models\n */\nexport interface OwnerSecurityRule extends SecurityRuleBase {\n /** The property (column) that stores the owner's user ID. */\n ownerField: string;\n access?: never;\n using?: never;\n withCheck?: never;\n condition?: never;\n check?: never;\n}\n\n/**\n * Security rule that grants unrestricted row access (no row filtering).\n * Generates `USING (true)`.\n *\n * This means \"no row-level filter\", NOT \"anonymous/unauthenticated access\".\n * Authentication is still enforced at the API layer — this only controls which\n * *rows* authenticated users can see.\n *\n * Cannot be combined with `using`, `withCheck`, or `ownerField`.\n *\n * @example\n * // Public read (any authenticated user sees all rows)\n * { operation: \"select\", access: \"public\" }\n *\n * @group Models\n */\nexport interface PublicSecurityRule extends SecurityRuleBase {\n /** Grant unrestricted row access for this operation. */\n access: \"public\";\n ownerField?: never;\n using?: never;\n withCheck?: never;\n condition?: never;\n check?: never;\n}\n\n/**\n * Security rule expressed as a structured, engine-agnostic\n * {@link PolicyExpression}. This is the **recommended** way to write a\n * non-trivial condition: it compiles to PostgreSQL `USING`/`WITH CHECK` SQL\n * *and* is evaluated identically by the admin UI, so the UI can never show an\n * action the database will reject.\n *\n * Cannot be combined with `ownerField`, `access`, or raw `using`/`withCheck`.\n *\n * @example\n * // Owner, or any user holding the `moderator` role\n * {\n * operation: \"update\",\n * condition: policy.or(\n * policy.compare(policy.field(\"user_id\"), \"eq\", policy.authUid()),\n * policy.rolesOverlap([\"moderator\"])\n * )\n * }\n *\n * @group Models\n */\nexport interface StructuredSecurityRule extends SecurityRuleBase {\n /**\n * Structured condition for the `USING` clause — which *existing* rows are\n * visible / can be modified / deleted (SELECT, UPDATE, DELETE).\n */\n condition: PolicyExpression;\n\n /**\n * Structured condition for the `WITH CHECK` clause — which *new/updated*\n * row values are allowed (INSERT, UPDATE). Defaults to `condition` when\n * omitted, mirroring PostgreSQL's own behavior.\n */\n check?: PolicyExpression;\n\n ownerField?: never;\n access?: never;\n using?: never;\n withCheck?: never;\n}\n\n/**\n * Security rule using raw SQL expressions for full PostgreSQL RLS power.\n *\n * **Postgres-only and server-authoritative.** Arbitrary SQL cannot be\n * evaluated by the admin UI, so a rule using this form is treated as *unknown*\n * client-side (never silently allowed) and its effect on visible actions is\n * reflected from the server. For conditions that should also drive the UI\n * precisely, prefer the structured {@link StructuredSecurityRule}.\n *\n * Cannot be combined with `ownerField`, `access`, or structured `condition`.\n *\n * You can reference columns via `{column_name}` which will be resolved to\n * `table.column_name` in the generated Drizzle code.\n *\n * @example\n * // Rows published in the last 30 days are visible\n * { operation: \"select\", using: \"{published_at} > now() - interval '30 days'\" }\n *\n * @example\n * // Only the owner, or users with 'moderator' role\n * {\n * operation: \"select\",\n * using: \"{user_id} = auth.uid() OR auth.roles() ~ 'moderator'\"\n * }\n *\n * @group Models\n */\nexport interface RawSQLSecurityRule extends SecurityRuleBase {\n /**\n * Raw SQL expression for the `USING` clause.\n * This controls which *existing* rows are visible / can be modified / deleted.\n * Applied to SELECT, UPDATE, and DELETE.\n */\n using: string;\n\n /**\n * Raw SQL expression for the `WITH CHECK` clause.\n * This controls which *new/updated* row values are allowed.\n * Applied to INSERT and UPDATE.\n *\n * If not provided on INSERT/UPDATE policies, falls back to `using`\n * (which matches PostgreSQL's own default behavior).\n */\n withCheck?: string;\n\n ownerField?: never;\n access?: never;\n condition?: never;\n check?: never;\n}\n\n/**\n * Security rule that only filters by application roles, without any\n * row-level condition (USING/WITH CHECK).\n *\n * Useful for simple \"only admins can access this table\" rules where\n * no per-row filtering is needed.\n *\n * @example\n * // Only admins can delete\n * { operation: \"delete\", roles: [\"admin\"] }\n *\n * @group Models\n */\nexport interface RolesOnlySecurityRule extends SecurityRuleBase {\n ownerField?: never;\n access?: never;\n using?: never;\n withCheck?: never;\n condition?: never;\n check?: never;\n}\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 actions?: {\n resetPassword?: boolean | EntityAction;\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 /** Send an email. Only available when email service is configured. */\n sendEmail?: (options: { to: string; subject: string; html: string; text?: string }) => Promise<void>;\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 * 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 | ServerContextPolicyExpression\n | ExistsInPolicyExpression\n | RawPolicyExpression;\n\n/**\n * The id a request without a logged-in user reports as `auth.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 **`auth.uid() IS NOT NULL` is a\n * tautology on the user path** — it is true for anonymous visitors too. Use\n * {@link policy.authenticated} (or `auth.uid() <> 'anonymous'`) to mean \"signed\n * in\", and {@link policy.serverContext} to mean \"the trusted server context\".\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_ID = \"anonymous\";\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 = auth.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(auth.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(auth.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 * `auth.uid() IS NOT NULL AND auth.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 `auth.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 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 * `auth.uid()` is `NULL` for them and only for them. Compiles to\n * `auth.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 = auth.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\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 `auth.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(auth.roles(), ',')`.\n * @group Models\n */\nexport interface AuthRolesPolicyOperand {\n kind: \"authRoles\";\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 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};\n","import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\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 orderBy?: string;\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 orderBy?: string;\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 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 orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\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 `auth.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> & Partial<BranchAdmin>;\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 * 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","/**\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","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 CMS 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 * 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 // ── 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// ── 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 filterOperators: ALL_WHERE_FILTER_OPS,\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 // 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 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 filterOperators: ALL_WHERE_FILTER_OPS,\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 filterOperators: ALL_WHERE_FILTER_OPS,\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","/**\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 * 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 /** 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","import type React from \"react\";\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: React.ComponentType<P> }>;\n}\n\n/**\n * A reference to a React 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 *\n * @group Types\n */\nexport type ComponentRef<P = any> =\n | React.ComponentType<P>\n | LazyComponentRef<P>\n | (() => Promise<{ default: React.ComponentType<P> }>)\n | string;\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\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 over CDN.\n * - `admin` — the Rebase admin panel, either hosted by the platform or built\n * into this repository.\n * - `mobile` — a native app. Registration only: it gets client credentials and\n * configuration, and is never built or hosted here.\n * - `custom` — an arbitrary container image built from a Dockerfile. The eject\n * hatch: full control, no managed-runtime guarantees.\n */\nexport type RebaseAppType = \"backend\" | \"static\" | \"admin\" | \"mobile\" | \"custom\";\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\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\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 * Which collections source the runtime uses.\n *\n * - `cms` (default) — collections come from the config package.\n * - `baas` — collections are introspected from the live database at boot and\n * the config package is not required.\n */\n mode?: \"cms\" | \"baas\";\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/**\n * A static client bundle — SPA or static site — built here and served from CDN.\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 * Serve `index.html` for unmatched paths (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\n/**\n * The admin panel.\n *\n * `hosted` is the default and means the platform serves it — nothing is built\n * into this repository and nothing ships in the bundle. `bundled` builds it here,\n * which is what a self-hosted or air-gapped deployment wants.\n */\nexport interface RebaseAdminAppConfig {\n type: \"admin\";\n mode?: \"hosted\" | \"bundled\";\n /** Only for `bundled`: package directory containing the admin sources. */\n root?: string;\n /** Only for `bundled`: build command. */\n build?: string;\n /** Only for `bundled`: directory of built assets. */\n output?: string;\n}\n\n/**\n * A native app. Registered for credentials and configuration; never built here.\n */\nexport interface RebaseMobileAppConfig {\n type: \"mobile\";\n platform: \"ios\" | \"android\" | \"other\";\n}\n\n/**\n * An app built from a Dockerfile into an arbitrary image.\n *\n * This is the deliberate escape hatch. A project containing one is not eligible\n * for the managed runtime — the platform cannot make guarantees about an image\n * it did not build — but it still deploys, and nothing else about the project\n * changes.\n */\nexport interface RebaseCustomAppConfig {\n type: \"custom\";\n /** Dockerfile path relative to the repository root. */\n dockerfile?: string;\n /** Build context relative to the repository root. Default `.`. */\n context?: string;\n /** Port the container listens on. Default 8080. */\n port?: number;\n}\n\nexport type RebaseAppConfig =\n | RebaseBackendAppConfig\n | RebaseStaticAppConfig\n | RebaseAdminAppConfig\n | RebaseMobileAppConfig\n | RebaseCustomAppConfig;\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 **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 runtime: 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\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 */\nexport const BUNDLE_FORMAT_VERSION = 1;\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 */\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 /** Built admin assets, when the admin panel is bundled rather than hosted. */\n admin?: string;\n /** Built static assets to serve from the runtime, when not on a CDN. */\n static?: 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 */\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 * - `cms` — a backend with declared collections; the runtime provisions their\n * tables and serves the data API.\n * - `baas` — a backend that introspects an existing database rather than\n * declaring collections.\n * - `static` — no backend at all: the bundle is a built SPA (`entry.static`),\n * and the runtime only serves those assets. No database, no data sources —\n * this is how a `static`/`admin` app runs on the same image as the backend.\n */\n mode: \"cms\" | \"baas\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\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 deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\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 mode: \"cms\" | \"baas\";\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(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 * 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 { CollectionRegistryController } from \"./collection_registry\";\nimport type { EntityStatus, EntityValues } from \"../types/entities\";\nimport type { CollectionConfig, FilterValues } from \"../types/collections\";\nimport type { RebaseContext } from \"../rebase_context\";\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\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/** 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/** Hard ceiling clamped onto any client-supplied `limit`, on every surface. */\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 /** Upper bound clamped onto any client-supplied limit. */\n maxLimit?: number;\n}\n\n/**\n * Resolve a client-supplied list `limit` into a safe, always-defined value.\n *\n * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,\n * so `0`, negatives, and absurd values can never bypass the cap.\n * - An absent / blank / non-numeric limit falls back to the mode default:\n * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.\n *\n * The return is never `undefined` — no ingress that routes its client limit\n * through this can produce an unbounded read.\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 const parsed = typeof rawLimit === \"number\" ? rawLimit : parseInt(String(rawLimit), 10);\n if (Number.isFinite(parsed)) {\n return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);\n }\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 limit?: number;\n offset?: number;\n startAfter?: unknown;\n orderBy?: string;\n searchString?: string;\n order?: \"desc\" | \"asc\";\n /** Vector similarity search configuration */\n vectorSearch?: VectorSearchParams;\n}\n\n/**\n * @internal\n */\nexport type ListenCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchCollectionProps<M> &\n {\n onUpdate: (rows: Record<string, unknown>[]) => 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/**\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}\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\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 * Delete a entity\n * @param props\n * @return was the whole deletion flow successful\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 * 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: RebaseContext,\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 orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n searchString?: string;\n databaseId?: string;\n vectorSearch?: VectorSearchParams;\n },\n include?: string[]\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?: string[],\n databaseId?: string\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\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`.\n *\n * This is optional for backwards compatibility.\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/collections.ts","../src/types/policy.ts","../src/types/backend.ts","../src/types/channel_bus.ts","../src/types/data_source.ts","../src/types/storage_source.ts","../src/types/component_ref.ts","../src/types/project_manifest.ts","../src/types/collection_contract.ts","../src/types/schema_version.ts","../src/controllers/data_driver.ts","../src/controllers/storage.ts"],"sourcesContent":["/**\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 * Left `undefined` for realtime/WebSocket, network, and client-side\n * logic errors that have no HTTP status.\n */\n status?: number;\n /** Stable, machine-readable error code (e.g. `\"NOT_FOUND\"`, `\"BAD_REQUEST\"`). */\n code?: string;\n /** Structured error payload returned by the server, when present. */\n details?: unknown;\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. */\n readonly code?: string;\n /** Structured error payload from the server, when present. */\n readonly details?: unknown;\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 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 constructor(message: string) {\n super(message);\n this.name = \"RebaseClientError\";\n }\n}\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 * 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 * Canonical sort representation: `[fieldName, direction]`.\n *\n * Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.\n * The colon-string form (`\"field:direction\"`) exists only at the HTTP wire\n * boundary, handled by `serializeOrderBy` / `deserializeOrderBy` in\n * `@rebasepro/common`.\n *\n * Design note: the natural extension for multi-column sort is\n * `OrderByTuple[]` — not implemented yet (server consumes only the first).\n *\n * @group Models\n */\nexport type OrderByTuple<Key extends string = string> = [Key, \"asc\" | \"desc\"];\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 * 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 */\n sort?: OrderByTuple<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 * 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 * 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_TO_CANONICAL as Record<string, WhereFilterOp | undefined>)[op];\n}\n","import React, { Dispatch, SetStateAction } from \"react\";\nimport type { Entity, EntityStatus, EntityValues } from \"./entities\";\nimport type { CollectionCallbacks } from \"./entity_callbacks\";\n\nimport type { EnumValues, Properties, PostgresProperties, FirebaseProperties, MongoProperties } from \"./properties\";\nimport type { ExportConfig } from \"./export_import\";\n\nimport type { User } from \"../users\";\nimport type { RebaseContext } from \"../rebase_context\";\nimport type { Relation } from \"./relations\";\nimport type { EntityCustomView, FormViewConfig } from \"./entity_views\";\nimport type { EntityAction } from \"./entity_actions\";\nimport type { PolicyExpression } from \"./policy\";\nimport type { ComponentRef } from \"./component_ref\";\nimport type { CollectionComponentOverrideMap } from \"./component_overrides\";\nimport type { WhereFilterOp, FilterValues, FilterPreset, OrderByTuple } from \"./filter-operators\";\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 * You can set an alias that will be used internally instead of the collection name.\n * The `slug` value will be used to determine the URL of the collection.\n * Note that you can use this value in reference properties too.\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 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 * Icon for the navigation sidebar or cards.\n */\n icon?: string | React.ReactNode;\n\n /**\n * Navigation group for this collection.\n * Collections sharing the same group name will be visually grouped\n * together in the drawer and home page. If not set, the collection\n * falls into the default \"Views\" group.\n */\n group?: string;\n\n /**\n * Array of entity views that this collection has.\n * Can be an array of `EntityCustomView` or a string representing the key of a global `EntityCustomView`.\n */\n entityViews?: (string | EntityCustomView<Record<string, unknown>>)[];\n\n /**\n * Default preview properties displayed when this collection is referenced to.\n */\n previewProperties?: string[];\n\n /**\n * Properties to display as columns in the list view.\n * If not specified, the list view uses a smart default (Title, Status, Date).\n */\n listProperties?: string[];\n\n /**\n * Title property of the entity. This is the property that will be used\n * as the title in entity related views and references.\n * If not specified, the first property simple text property will be used.\n */\n readonly titleProperty?: Extract<keyof M, string> | (string & {});\n\n /**\n * When editing a entity, you can choose to open the entity in a side dialog\n * or in a full screen dialog. Defaults to `full_screen`.\n */\n openEntityMode?: \"side_panel\" | \"full_screen\" | \"split\" | \"dialog\";\n\n /**\n * Controls what happens when a user clicks on a entity in the collection view.\n * - `\"edit\"` (default): Opens the entity in the edit form.\n * - `\"view\"`: Opens a read-only detail view with an \"Edit\" button.\n */\n defaultEntityAction?: \"view\" | \"edit\";\n\n /**\n * Replace the default entity form with a custom component.\n * The Builder receives the same props as entity view tabs\n * (entity, formContext, collection, etc.) and has full control over the UI.\n *\n * Works in both edit mode and read-only mode (when `defaultEntityAction`\n * is `\"view\"`). In read-only mode, `formContext.readOnly` will be `true`.\n */\n formView?: FormViewConfig;\n\n /**\n * Prevent default actions from being displayed or executed on this collection.\n */\n disableDefaultActions?: (\"edit\" | \"copy\" | \"delete\")[];\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 * 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 = auth.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 /**\n * Order in which the properties are displayed.\n * If you are specifying your collection as code, the order is the same as the\n * one you define in `properties`. Additional columns are added at the\n * end of the list, if the order is not specified.\n *\n * You can use this prop to hide some properties from the table view.\n * Note that if you set this prop, other ways to hide fields, like\n * `hidden` in the property definition, will be ignored.\n * `propertiesOrder` has precedence over `hidden`.\n *\n * Supported entry formats:\n * - For properties, use the property key.\n * - For additional fields, use the field key.\n * - Child collections (Firestore subcollections, or Postgres relations\n * with `many` cardinality) each get a column with id\n * `subcollection:<slug>`, e.g. `subcollection:orders`.\n */\n propertiesOrder?: (Extract<keyof M, string> | (string & {}) | string | `subcollection:${string}`)[];\n\n /**\n * If enabled, content is loaded in batches. If `false` all entities in the\n * collection are loaded. This means that when reaching the end of the\n * collection, the CMS will load more entities.\n * You can specify a number to specify the pagination size (50 by default)\n * Defaults to `true`\n */\n pagination?: boolean | number;\n\n\n selectionEnabled?: boolean;\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 * Pass your own selection controller if you want to control selected\n * entities externally.\n * @see useSelectionController\n */\n selectionController?: SelectionController<M>;\n\n /**\n * Force a filter in this view. If applied, the rest of the filters will\n * be disabled. Filters applied with this prop cannot be changed.\n * e.g. `fixedFilter: { age: [\">\", 18] }`\n * e.g. `fixedFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n readonly fixedFilter?: FilterValues<Extract<keyof M, string> | (string & {})>;\n\n /**\n * Initial filters applied to the collection this collection is related to.\n * Defaults to none. Filters applied with this prop can be changed.\n * e.g. `defaultFilter: { age: [\">\", 18] }`\n * e.g. `defaultFilter: { related_user: [\"==\", new EntityReference(\"sdc43dsw2\", \"users\")] }`\n */\n readonly defaultFilter?: FilterValues<Extract<keyof M, string> | (string & {})>; // setting FilterValues<M> can break defining collections by code\n\n /**\n * Pre-defined filter presets that appear as quick-access options in the\n * collection toolbar. Each preset applies a set of filters (and\n * optionally a sort order) with a single click.\n *\n * ```ts\n * filterPresets: [\n * {\n * label: \"Shipped this month\",\n * filterValues: {\n * status: [\"==\", \"shipped\"],\n * order_date: [\">=\", new Date(Date.now() - 30 * 86400000)]\n * }\n * }\n * ]\n * ```\n */\n readonly filterPresets?: FilterPreset<Extract<keyof M, string> | (string & {})>[];\n\n /**\n * Default sort applied to this collection.\n * When setting this prop, entities will have a default order\n * applied in the collection.\n * e.g. `sort: [\"order\", \"asc\"]`\n */\n readonly sort?: OrderByTuple<Extract<keyof M, string> | (string & {})>;\n\n /**\n * You can add additional fields to the collection view by implementing\n * an additional field delegate.\n */\n readonly additionalFields?: AdditionalFieldDelegate<M, USER>[];\n\n /**\n * Default size of the rendered collection\n */\n defaultSize?: CollectionSize;\n\n /**\n * Can the elements in this collection be edited inline in the collection\n * view. Even when inline editing is disabled, entities can still be\n * edited in the side panel (subject to `securityRules`).\n */\n inlineEditing?: boolean;\n\n /**\n * Should this collection be hidden from the main navigation panel, if\n * it is at the root level, or in the entity side panel if it's a\n * subcollection.\n * It will still be accessible if you reach the specified path.\n * You can also use this collection as a reference target.\n */\n hideFromNavigation?: boolean;\n\n /**\n * If you want to open custom views or subcollections by default when opening the edit\n * view of a entity, you can specify the path to the view here.\n * The path is relative to the current collection. For example if you have a collection\n * that has a custom view as well as a subcollection that refers to another entity, you can\n * either specify the path to the custom view or the path to the subcollection.\n */\n defaultSelectedView?: string | DefaultSelectedViewBuilder;\n\n /**\n * Should the ID of this collection be hidden from the form view.\n */\n hideIdFromForm?: boolean;\n\n /**\n * Should the ID of this collection be hidden from the grid view.\n */\n hideIdFromCollection?: boolean;\n\n /**\n * If set to true, the form will be auto-saved when the user changes\n * the value of a field.\n * Defaults to false.\n * When a new entity is created, this property can be updated to generated a new ID\n */\n formAutoSave?: boolean;\n\n /**\n *\n */\n exportable?: boolean | ExportConfig<USER>;\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 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 * Width of the side dialog (in pixels) when opening a entity in this collection.\n */\n sideDialogWidth?: number | string;\n\n /**\n * If set to true, the default values of the properties will be applied\n * to the entity every time the entity is updated (not only when created).\n * Defaults to false.\n */\n alwaysApplyDefaultValues?: boolean;\n\n /**\n * If set to true, a tab including the JSON representation of the entity will be included.\n */\n includeJsonView?: boolean;\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` to let unknown keys through to the database, which is what\n * happened before this existed: a typo reached the INSERT and came back as\n * a Postgres error about a column, or — where a column really does exist\n * that the config never declared, populated by a trigger or a default —\n * quietly worked. The second case is the reason for the escape hatch.\n */\n strictWrites?: boolean;\n\n /**\n * Should local changes be backed up in local storage, to prevent data loss on\n * accidental navigations.\n * - `manual_apply`: When the user navigates back to a entity with local changes,\n * they will be prompted to restore the changes.\n * - `auto_apply`: When the user navigates back to a entity with local changes,\n * the changes will be automatically applied.\n * - `false`: Local changes will not be backed up.\n * Defaults to `manual_apply`.\n */\n localChangesBackup?: \"manual_apply\" | \"auto_apply\" | false;\n\n /**\n * Default view mode for displaying this collection.\n * - \"table\": Display entities in a table with inline editing (default)\n * - \"cards\": Display entities as a grid of cards with thumbnails\n * - \"kanban\": Display entities in a Kanban board grouped by a property\n * Defaults to \"table\".\n */\n defaultViewMode?: ViewMode;\n\n /**\n * Which view modes are available for this collection.\n * Possible values: \"table\", \"cards\", \"kanban\".\n * Defaults to all three: [\"table\", \"cards\", \"kanban\"].\n * Note: \"kanban\" will only be available if the collection has at least\n * one string property with `enum` defined, regardless of this setting.\n */\n enabledViews?: ViewMode[];\n\n /**\n * Configuration for Kanban board view mode.\n * When set, the Kanban view mode becomes available.\n */\n kanban?: KanbanConfig<M>;\n\n /**\n * Property key to use for ordering items.\n * Must reference a string/text property. When items are reordered,\n * this property will be updated with lexicographic sort keys\n * (e.g. \"a0\", \"a1\", \"a0V\") using string-based fractional indexing.\n * Used by Kanban view for ordering within columns\n * and can be used for general ordering purposes.\n */\n readonly orderProperty?: Extract<keyof M, string> | (string & {});\n\n /**\n * Actions that can be performed on the entities in this collection.\n */\n entityActions?: EntityAction<M, USER>[];\n\n /**\n * Builder for the collection actions rendered in the toolbar\n */\n Actions?: ComponentRef<CollectionActionsProps>[];\n\n /**\n * The database table name for this collection.\n * Automatically set for PostgreSQL collections.\n * For non-SQL backends, this may be undefined.\n */\n table?: string;\n\n /**\n * Relations defined for this collection.\n * Populated at normalization time from inline relation properties\n * or explicit relation definitions.\n */\n relations?: Relation[];\n\n /**\n * Security rules for this collection (Row Level Security).\n * When defined, the backend enforces access control policies.\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * Collection-scoped component overrides. These take precedence over\n * global overrides set on `<Rebase>`, but only within this collection's\n * views (entity form, detail view, table, empty state, etc.).\n *\n * Only collection-scoped components (like `Entity.Form`, `Collection.EmptyState`,\n * `Collection.Card`, etc.) can be overridden here. App-level components\n * (like `Shell.AppBar`, `HomePage`) can only be overridden at the `<Rebase>` level.\n *\n * @example\n * ```tsx\n * const productsCollection: PostgresCollectionConfig = {\n * name: \"Products\",\n * slug: \"products\",\n * table: \"products\",\n * components: {\n * \"Entity.Form\": { Component: ProductCustomForm },\n * \"Collection.Card\": { Component: ProductCard },\n * },\n * properties: { ... }\n * };\n * ```\n */\n components?: CollectionComponentOverrideMap;\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 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 * - `auth.uid()` — the current user's ID\n * - `auth.roles()` — comma-separated app role IDs\n * - `auth.jwt()` — full JWT claims as JSONB\n */\n securityRules?: readonly SecurityRule[];\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 * Type guard for PostgreSQL collections.\n * Returns true if the collection uses the Postgres engine (or the default engine).\n * @group Models\n */\nexport function isPostgresCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): collection is PostgresCollectionConfig<M, USER> {\n return !collection.engine || collection.engine === \"postgres\";\n}\n\n/**\n * Type guard for Firebase / Firestore collections.\n * @group Models\n */\nexport function isFirebaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): collection is FirebaseCollectionConfig<M, USER> {\n return collection.engine === \"firestore\";\n}\n\n/**\n * Type guard for MongoDB collections.\n * @group Models\n */\nexport function isMongoDBCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): collection is MongoDBCollectionConfig<M, USER> {\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/**\n * Configuration for Kanban board view mode.\n * @group Collections\n */\nexport interface KanbanConfig<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Property key to use for Kanban board columns.\n * Must reference a string property with `enum` values defined.\n * Entities will be grouped into columns based on this property's value.\n * The column order is determined by the order of `enum` values in the property.\n */\n columnProperty: Extract<keyof M, string> | (string & {});\n}\n\n/**\n * View mode for displaying a collection.\n * - \"list\": Simple, clean list view — the classic CMS default\n * - \"table\": Table with inline editing\n * - \"cards\": Grid of visual cards with thumbnails\n * - \"kanban\": Board view grouped by a property\n * @group Collections\n */\nexport type ViewMode = \"list\" | \"table\" | \"cards\" | \"kanban\";\n\n/**\n * Parameter passed to the `Actions` prop in the collection configuration.\n * The component will receive this prop when it is rendered in the collection\n * toolbar.\n *\n * @group Models\n */\nexport interface CollectionActionsProps<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User, EC extends CollectionConfig<M> = CollectionConfig<M>> {\n /**\n * Full collection path of this entity. This is the full path, like\n * `users/1234/addresses`\n */\n path: string;\n\n /**\n * Path of the last collection, like `addresses`\n */\n relativePath: string;\n\n /**\n * Array of the parent path segments like `['users']`\n */\n parentCollectionSlugs: string[];\n parentEntityIds: string[];\n\n /**\n * The collection configuration\n */\n collection: EC;\n\n /**\n * Use this controller to get the selected entities and to update the\n * selected entities state.\n */\n selectionController: SelectionController<M>;\n\n /**\n * Use this controller to get the table controller and to update the\n * table controller state.\n */\n tableController: EntityTableController<M>;\n\n /**\n * Context of the app status\n */\n context: RebaseContext<USER>;\n\n /**\n * Count of the entities in this collection.\n * undefined means the count is still loading.\n */\n collectionEntitiesCount?: number;\n\n /**\n * Programmatically open the new-document form for this collection,\n * optionally pre-populating it with initial field values.\n * The form opens in the same mode configured for the collection\n * (side panel, full screen, or split).\n *\n * This is the primary hook for workflows that need to create a document\n * from external data — e.g. fetching content from a URL, importing from\n * a third-party API, or duplicating from another system.\n *\n * @example\n * // Inside a custom CollectionAction component:\n * openNewDocument({ title: \"Fetched title\", body: \"...\" });\n */\n openNewDocument: (defaultValues?: Record<string, unknown>) => void;\n\n}\n\n/**\n * Use this controller to retrieve the selected entities or modify them in\n * an {@link CollectionConfig}\n * @group Models\n */\nexport interface SelectionController<M extends Record<string, unknown> = Record<string, unknown>> {\n selectedEntities: Entity<M>[];\n setSelectedEntities(entities: Entity<M>[]): void;\n setSelectedEntities(action: (prev: Entity<M>[]) => Entity<M>[]): void;\n isEntitySelected(entity: Entity<M>): boolean;\n toggleEntitySelection(entity: Entity<M>, newSelectedState?: boolean): void;\n}\n\n// Canonical filter types — re-exported from the single source-of-truth.\nexport type { WhereFilterOp, FilterValues, WireFilterValues, FilterPreset } from \"./filter-operators\";\n\n\n/**\n * Used to indicate valid filter combinations (e.g. created in Firestore)\n * If the user selects a specific filter/sort combination, the CMS checks if it's\n * valid, otherwise it reverts to the simpler valid case\n * @group Models\n */\nexport type FilterCombination<Key extends string> = Partial<Record<Key, \"asc\" | \"desc\">>;\n\n/**\n * Sizes in which a collection can be rendered\n * @group Models\n */\nexport type CollectionSize = \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\";\n\nexport type AdditionalFieldDelegateProps<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> = {\n entity: Entity<M>,\n context: RebaseContext<USER>\n};\n\n/**\n * Use this interface for adding additional fields to entity collection views and forms.\n * @group Models\n */\nexport interface AdditionalFieldDelegate<M extends Record<string, unknown> = Record<string, unknown>,\n USER extends User = User> {\n\n /**\n * ID of this column. You can use this id in the `properties` field of the\n * collection in any order you want\n */\n key: string;\n\n /**\n * Header of this column\n */\n name: string;\n\n /**\n * Width of the generated column in pixels\n */\n width?: number;\n\n /**\n * Builder for the custom field\n */\n Builder?(props: { entity: Entity<M>, context: RebaseContext<USER> }): React.ReactNode;\n\n\n /**\n * If this column needs to update dynamically based on other properties,\n * you can define an array of keys as strings with the\n * `dependencies` prop.\n * e.g. [\"name\", \"surname\"]\n * This is a performance optimization, if you don't define dependencies\n * it will be updated in every render.\n */\n dependencies?: NoInfer<Extract<keyof M, string>> | NoInfer<Extract<keyof M, string>>[] | (string & {}) | (string & {})[];\n\n /**\n * Use this prop to define the value of the column as a string or number.\n * This is the value that will be used for exporting the collection.\n * If `Builder` is defined, this prop will be ignored in the collection\n * view.\n * @param entity\n */\n value?(props: {\n entity: Entity<M>,\n context: RebaseContext\n }): string | number | Promise<string | number> | undefined;\n}\n\n\nexport type InferCollectionConfigType<S extends CollectionConfig> = S extends CollectionConfig<infer M> ? M : never;\n\n/**\n * Used in the {@link CollectionConfig#defaultSelectedView} to define the default\n * @group Models\n */\nexport type DefaultSelectedViewBuilder = (params: DefaultSelectedViewParams) => string | undefined;\n\n/**\n * Used in the {@link CollectionConfig#defaultSelectedView} to define the default\n * @group Models\n */\nexport type DefaultSelectedViewParams = {\n status?: EntityStatus;\n entityId?: string | number;\n};\n/**\n * You can use this controller to control the table view of a collection.\n */\nexport type EntityTableController<M extends Record<string, unknown> = Record<string, unknown>> = {\n data: Entity<M>[];\n dataLoading: boolean;\n noMoreToLoad: boolean;\n dataLoadingError?: Error;\n filterValues?: FilterValues<Extract<keyof M, string> | (string & {})>;\n setFilterValues?: (filterValues: FilterValues<Extract<keyof M, string> | (string & {})>) => void;\n sortBy?: [Extract<keyof M, string> | (string & {}), \"asc\" | \"desc\"];\n setSortBy?: (sortBy?: [Extract<keyof M, string> | (string & {}), \"asc\" | \"desc\"]) => void;\n searchString?: string;\n setSearchString?: (searchString?: string) => void;\n clearFilter?: () => void;\n itemCount?: number;\n setItemCount?: (itemCount: number) => void;\n initialScroll?: number;\n onScroll?: (props: {\n scrollDirection: \"forward\" | \"backward\",\n scrollOffset: number,\n scrollUpdateWasRequested: boolean\n }) => void;\n paginationEnabled?: boolean;\n pageSize?: number;\n checkFilterCombination?: (filterValues: FilterValues<string>,\n sortBy?: [string, \"asc\" | \"desc\"]) => boolean;\n popupCell?: SelectedCellProps<M>;\n setPopupCell?: (popupCell?: SelectedCellProps<M>) => void;\n\n onAddColumn?: (column: string) => void;\n}\n\nexport type SelectedCellProps<M extends Record<string, unknown> = Record<string, unknown>> = {\n propertyKey: Extract<keyof M, string> | (string & {});\n cellRect: DOMRect;\n width: number;\n height: number;\n entityPath: string;\n entityId: string | number;\n};\n\n/**\n * SQL operation that a policy applies to.\n * @group Models\n */\nexport type SecurityOperation = \"select\" | \"insert\" | \"update\" | \"delete\" | \"all\";\n\n/**\n * Flexible Row Level Security rule for a collection.\n *\n * Built on PostgreSQL Row Level Security. Rules can range from\n * simple convenience shortcuts to fully custom SQL expressions, giving you the\n * full power of PostgreSQL Row Level Security.\n *\n * The authenticated user's identity is available in raw SQL via:\n * - `auth.uid()` — the user's ID\n * - `auth.roles()` — comma-separated app role IDs\n * - `auth.jwt()` — full JWT claims as JSONB\n *\n * These are set automatically per-transaction by the backend.\n *\n * **How rules combine:** PostgreSQL evaluates all matching policies for an\n * operation. Permissive rules are OR'd together (any one passing is enough).\n * Restrictive rules are AND'd (all must pass). This is standard PostgreSQL RLS behavior.\n *\n * **Mutual exclusivity:** `ownerField`, `access`, structured `condition`, and\n * raw SQL (`using`/`withCheck`) cannot be combined. The type system enforces\n * this — attempting to set conflicting fields will produce a compile-time\n * error.\n *\n * **Which form to reach for:** prefer the structured {@link StructuredSecurityRule}\n * (`condition`/`check`) or the shortcuts (`ownerField`, `access`, `roles`). These\n * are engine-agnostic and evaluated identically by the database and the admin UI,\n * so the UI never shows an action the database will reject. Raw SQL\n * ({@link RawSQLSecurityRule}) keeps full PostgreSQL power but is Postgres-only\n * and server-authoritative (the UI cannot evaluate arbitrary SQL locally).\n *\n * @group Models\n */\nexport type SecurityRule =\n | OwnerSecurityRule\n | PublicSecurityRule\n | StructuredSecurityRule\n | RawSQLSecurityRule\n | RolesOnlySecurityRule;\n\n/**\n * Shared fields for all SecurityRule variants.\n * @group Models\n */\nexport interface SecurityRuleBase {\n /**\n * Optional human-readable name for the policy.\n * If not provided, one will be auto-generated from the table name and operation.\n * Must be unique per table.\n *\n * When using `operations` (array), each generated policy will have the\n * operation name appended, e.g. `\"owner_access_select\"`, `\"owner_access_update\"`.\n */\n name?: string;\n\n /**\n * Which SQL operation this policy applies to.\n * Use this when the policy targets a single operation or all operations.\n *\n * For multiple specific operations, use `operations` (array) instead.\n * If neither is specified, defaults to `\"all\"`.\n *\n * @default \"all\"\n */\n operation?: SecurityOperation;\n\n /**\n * Array of SQL operations this policy applies to.\n * The compiler will generate one PostgreSQL policy per operation, sharing\n * the same configuration.\n *\n * This reduces boilerplate when the same rule applies to multiple (but not all)\n * operations.\n *\n * Takes precedence over `operation` (singular) if both are specified.\n *\n * @example\n * // Same rule for select and update\n * { operations: [\"select\", \"update\"], ownerField: \"user_id\" }\n *\n * @example\n * // Equivalent to operation: \"all\"\n * { operations: [\"all\"], ownerField: \"user_id\" }\n */\n operations?: readonly SecurityOperation[];\n\n /**\n * Whether this policy is `\"permissive\"` (default) or `\"restrictive\"`.\n *\n * - **permissive**: Multiple permissive policies for the same operation are\n * OR'd together — if *any* passes, access is granted.\n * - **restrictive**: Restrictive policies are AND'd with all permissive\n * policies — they act as additional gates that *must* also pass.\n *\n * This is the standard PostgreSQL RLS model.\n *\n * @default \"permissive\"\n */\n mode?: \"permissive\" | \"restrictive\";\n\n /**\n * **Shortcut.** Restrict this rule to users that have one of these\n * application-level roles.\n *\n * **Important:** These are NOT native PostgreSQL database roles — names\n * like `public`, `anon` or `authenticated` belong to {@link pgRoles} and\n * produce a condition no user can satisfy if used here. These are\n * application roles managed by Rebase, stored as an inline `roles TEXT[]`\n * column on the users table, and injected into each transaction as\n * `app.user_roles` — which `auth.roles()` reads.\n *\n * There is no roles registry: a role exists once it is assigned to a user.\n *\n * Generates a safe array-overlap condition — the user passes if they hold\n * *any* of the listed roles:\n * `string_to_array(auth.roles(), ',') && ARRAY['<role1>', '<role2>']`\n *\n * (Note: this is a true set intersection, NOT a regex/substring match, so\n * a role named `admin` never matches `nonadmin` or `superadmin`.)\n *\n * Can be combined with `ownerField`, `access`, `condition`, or raw\n * `using`/`withCheck`. When combined, the role check is AND'd with the\n * other condition.\n *\n * @example\n * // Only admins can delete\n * { operation: \"delete\", roles: [\"admin\"] }\n *\n * @example\n * // Admins have unfiltered read access to all rows\n * { operation: \"select\", roles: [\"admin\"], using: \"true\" }\n */\n roles?: readonly string[];\n\n // ── Advanced: native PostgreSQL role targeting ───────────────────────\n\n /**\n * **Advanced.** Native PostgreSQL database roles the policy applies to.\n *\n * By default, all generated policies target the `public` role (i.e.\n * every database connection). This is correct for most setups where\n * a single database role is used for all connections.\n *\n * **Important:** These are NOT the same as the application-level\n * {@link roles} (admin, editor, viewer, etc.) — those are enforced in the\n * USING/WITH CHECK clauses via `auth.roles()`. This field controls the\n * PostgreSQL `TO` clause in `CREATE POLICY ... TO role_name`.\n *\n * Use this if you have dedicated PostgreSQL roles (e.g. `app_read`,\n * `app_write`) and want policies to target specific ones.\n *\n * @default [\"public\"]\n *\n * @example\n * // Only apply this policy when connected as `app_role`\n * { operation: \"select\", access: \"public\", pgRoles: [\"app_role\"] }\n */\n pgRoles?: readonly string[];\n}\n\n/**\n * Security rule that grants access based on row ownership.\n * Generates a USING/WITH CHECK clause like: `<column> = auth.uid()`\n *\n * Cannot be combined with `using`, `withCheck`, or `access`.\n *\n * @example\n * { operation: \"all\", ownerField: \"user_id\" }\n *\n * @group Models\n */\nexport interface OwnerSecurityRule extends SecurityRuleBase {\n /** The property (column) that stores the owner's user ID. */\n ownerField: string;\n access?: never;\n using?: never;\n withCheck?: never;\n condition?: never;\n check?: never;\n}\n\n/**\n * Security rule that grants unrestricted row access (no row filtering).\n * Generates `USING (true)`.\n *\n * This means \"no row-level filter\", NOT \"anonymous/unauthenticated access\".\n * Authentication is still enforced at the API layer — this only controls which\n * *rows* authenticated users can see.\n *\n * Cannot be combined with `using`, `withCheck`, or `ownerField`.\n *\n * @example\n * // Public read (any authenticated user sees all rows)\n * { operation: \"select\", access: \"public\" }\n *\n * @group Models\n */\nexport interface PublicSecurityRule extends SecurityRuleBase {\n /** Grant unrestricted row access for this operation. */\n access: \"public\";\n ownerField?: never;\n using?: never;\n withCheck?: never;\n condition?: never;\n check?: never;\n}\n\n/**\n * Security rule expressed as a structured, engine-agnostic\n * {@link PolicyExpression}. This is the **recommended** way to write a\n * non-trivial condition: it compiles to PostgreSQL `USING`/`WITH CHECK` SQL\n * *and* is evaluated identically by the admin UI, so the UI can never show an\n * action the database will reject.\n *\n * Cannot be combined with `ownerField`, `access`, or raw `using`/`withCheck`.\n *\n * @example\n * // Owner, or any user holding the `moderator` role\n * {\n * operation: \"update\",\n * condition: policy.or(\n * policy.compare(policy.field(\"user_id\"), \"eq\", policy.authUid()),\n * policy.rolesOverlap([\"moderator\"])\n * )\n * }\n *\n * @group Models\n */\nexport interface StructuredSecurityRule extends SecurityRuleBase {\n /**\n * Structured condition for the `USING` clause — which *existing* rows are\n * visible / can be modified / deleted (SELECT, UPDATE, DELETE).\n */\n condition: PolicyExpression;\n\n /**\n * Structured condition for the `WITH CHECK` clause — which *new/updated*\n * row values are allowed (INSERT, UPDATE). Defaults to `condition` when\n * omitted, mirroring PostgreSQL's own behavior.\n */\n check?: PolicyExpression;\n\n ownerField?: never;\n access?: never;\n using?: never;\n withCheck?: never;\n}\n\n/**\n * Security rule using raw SQL expressions for full PostgreSQL RLS power.\n *\n * **Postgres-only and server-authoritative.** Arbitrary SQL cannot be\n * evaluated by the admin UI, so a rule using this form is treated as *unknown*\n * client-side (never silently allowed) and its effect on visible actions is\n * reflected from the server. For conditions that should also drive the UI\n * precisely, prefer the structured {@link StructuredSecurityRule}.\n *\n * Cannot be combined with `ownerField`, `access`, or structured `condition`.\n *\n * You can reference columns via `{column_name}` which will be resolved to\n * `table.column_name` in the generated Drizzle code.\n *\n * @example\n * // Rows published in the last 30 days are visible\n * { operation: \"select\", using: \"{published_at} > now() - interval '30 days'\" }\n *\n * @example\n * // Only the owner, or users with 'moderator' role\n * {\n * operation: \"select\",\n * using: \"{user_id} = auth.uid() OR auth.roles() ~ 'moderator'\"\n * }\n *\n * @group Models\n */\nexport interface RawSQLSecurityRule extends SecurityRuleBase {\n /**\n * Raw SQL expression for the `USING` clause.\n * This controls which *existing* rows are visible / can be modified / deleted.\n * Applied to SELECT, UPDATE, and DELETE.\n */\n using: string;\n\n /**\n * Raw SQL expression for the `WITH CHECK` clause.\n * This controls which *new/updated* row values are allowed.\n * Applied to INSERT and UPDATE.\n *\n * If not provided on INSERT/UPDATE policies, falls back to `using`\n * (which matches PostgreSQL's own default behavior).\n */\n withCheck?: string;\n\n ownerField?: never;\n access?: never;\n condition?: never;\n check?: never;\n}\n\n/**\n * Security rule that only filters by application roles, without any\n * row-level condition (USING/WITH CHECK).\n *\n * Useful for simple \"only admins can access this table\" rules where\n * no per-row filtering is needed.\n *\n * @example\n * // Only admins can delete\n * { operation: \"delete\", roles: [\"admin\"] }\n *\n * @group Models\n */\nexport interface RolesOnlySecurityRule extends SecurityRuleBase {\n ownerField?: never;\n access?: never;\n using?: never;\n withCheck?: never;\n condition?: never;\n check?: never;\n}\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 actions?: {\n resetPassword?: boolean | EntityAction;\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 /** Send an email. Only available when email service is configured. */\n sendEmail?: (options: { to: string; subject: string; html: string; text?: string }) => Promise<void>;\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 * 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 | ServerContextPolicyExpression\n | ExistsInPolicyExpression\n | RawPolicyExpression;\n\n/**\n * The id a request without a logged-in user reports as `auth.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 **`auth.uid() IS NOT NULL` is a\n * tautology on the user path** — it is true for anonymous visitors too. Use\n * {@link policy.authenticated} (or `auth.uid() <> 'anonymous'`) to mean \"signed\n * in\", and {@link policy.serverContext} to mean \"the trusted server context\".\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_ID = \"anonymous\";\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 = auth.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(auth.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(auth.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 * `auth.uid() IS NOT NULL AND auth.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 `auth.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 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 * `auth.uid()` is `NULL` for them and only for them. Compiles to\n * `auth.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 = auth.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\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 `auth.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(auth.roles(), ',')`.\n * @group Models\n */\nexport interface AuthRolesPolicyOperand {\n kind: \"authRoles\";\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 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};\n","import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\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 orderBy?: string;\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 orderBy?: string;\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 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 orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\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 `auth.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> & Partial<BranchAdmin>;\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 * 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 ensureCollectionSchema?(\n collections: unknown[],\n driverResult: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\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","/**\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","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 CMS 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 * 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 // ── 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// ── 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 filterOperators: ALL_WHERE_FILTER_OPS,\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 // 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 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 filterOperators: ALL_WHERE_FILTER_OPS,\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 filterOperators: ALL_WHERE_FILTER_OPS,\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","/**\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 * 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 /** 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","import type React from \"react\";\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: React.ComponentType<P> }>;\n}\n\n/**\n * A reference to a React 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 *\n * @group Types\n */\nexport type ComponentRef<P = any> =\n | React.ComponentType<P>\n | LazyComponentRef<P>\n | (() => Promise<{ default: React.ComponentType<P> }>)\n | string;\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\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 over CDN.\n * - `admin` — the Rebase admin panel, either hosted by the platform or built\n * into this repository.\n * - `mobile` — a native app. Registration only: it gets client credentials and\n * configuration, and is never built or hosted here.\n * - `custom` — an arbitrary container image built from a Dockerfile. The eject\n * hatch: full control, no managed-runtime guarantees.\n */\nexport type RebaseAppType = \"backend\" | \"static\" | \"admin\" | \"mobile\" | \"custom\";\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\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\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 * Which collections source the runtime uses.\n *\n * - `cms` (default) — collections come from the config package.\n * - `baas` — collections are introspected from the live database at boot and\n * the config package is not required.\n */\n mode?: \"cms\" | \"baas\";\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/**\n * A static client bundle — SPA or static site — built here and served from CDN.\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 * Serve `index.html` for unmatched paths (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\n/**\n * The admin panel.\n *\n * `hosted` is the default and means the platform serves it — nothing is built\n * into this repository and nothing ships in the bundle. `bundled` builds it here,\n * which is what a self-hosted or air-gapped deployment wants.\n */\nexport interface RebaseAdminAppConfig {\n type: \"admin\";\n mode?: \"hosted\" | \"bundled\";\n /** Only for `bundled`: package directory containing the admin sources. */\n root?: string;\n /** Only for `bundled`: build command. */\n build?: string;\n /** Only for `bundled`: directory of built assets. */\n output?: string;\n}\n\n/**\n * A native app. Registered for credentials and configuration; never built here.\n */\nexport interface RebaseMobileAppConfig {\n type: \"mobile\";\n platform: \"ios\" | \"android\" | \"other\";\n}\n\n/**\n * An app built from a Dockerfile into an arbitrary image.\n *\n * This is the deliberate escape hatch. A project containing one is not eligible\n * for the managed runtime — the platform cannot make guarantees about an image\n * it did not build — but it still deploys, and nothing else about the project\n * changes.\n */\nexport interface RebaseCustomAppConfig {\n type: \"custom\";\n /** Dockerfile path relative to the repository root. */\n dockerfile?: string;\n /** Build context relative to the repository root. Default `.`. */\n context?: string;\n /** Port the container listens on. Default 8080. */\n port?: number;\n}\n\nexport type RebaseAppConfig =\n | RebaseBackendAppConfig\n | RebaseStaticAppConfig\n | RebaseAdminAppConfig\n | RebaseMobileAppConfig\n | RebaseCustomAppConfig;\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 **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 runtime: 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\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 */\nexport const BUNDLE_FORMAT_VERSION = 1;\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 */\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 /** Built admin assets, when the admin panel is bundled rather than hosted. */\n admin?: string;\n /** Built static assets to serve from the runtime, when not on a CDN. */\n static?: 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 */\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 * - `cms` — a backend with declared collections; the runtime provisions their\n * tables and serves the data API.\n * - `baas` — a backend that introspects an existing database rather than\n * declaring collections.\n * - `static` — no backend at all: the bundle is a built SPA (`entry.static`),\n * and the runtime only serves those assets. No database, no data sources —\n * this is how a `static`/`admin` app runs on the same image as the backend.\n */\n mode: \"cms\" | \"baas\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\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 deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\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 mode: \"cms\" | \"baas\";\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(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 * 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 { CollectionRegistryController } from \"./collection_registry\";\nimport type { EntityStatus, EntityValues } from \"../types/entities\";\nimport type { CollectionConfig, FilterValues } from \"../types/collections\";\nimport type { RebaseContext } from \"../rebase_context\";\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\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/** 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/** Hard ceiling clamped onto any client-supplied `limit`, on every surface. */\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 /** Upper bound clamped onto any client-supplied limit. */\n maxLimit?: number;\n}\n\n/**\n * Resolve a client-supplied list `limit` into a safe, always-defined value.\n *\n * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,\n * so `0`, negatives, and absurd values can never bypass the cap.\n * - An absent / blank / non-numeric limit falls back to the mode default:\n * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.\n *\n * The return is never `undefined` — no ingress that routes its client limit\n * through this can produce an unbounded read.\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 const parsed = typeof rawLimit === \"number\" ? rawLimit : parseInt(String(rawLimit), 10);\n if (Number.isFinite(parsed)) {\n return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);\n }\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 limit?: number;\n offset?: number;\n startAfter?: unknown;\n orderBy?: string;\n searchString?: string;\n order?: \"desc\" | \"asc\";\n /** Vector similarity search configuration */\n vectorSearch?: VectorSearchParams;\n}\n\n/**\n * @internal\n */\nexport type ListenCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchCollectionProps<M> &\n {\n onUpdate: (rows: Record<string, unknown>[]) => 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/**\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}\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\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 * Delete a entity\n * @param props\n * @return was the whole deletion flow successful\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 * 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: RebaseContext,\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 orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n searchString?: string;\n databaseId?: string;\n vectorSearch?: VectorSearchParams;\n },\n include?: string[]\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?: string[],\n databaseId?: string\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\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`.\n *\n * This is optional for backwards compatibility.\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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"}
@@ -593,6 +593,24 @@ export interface BackendBootstrapper {
593
593
  * Return admin capabilities for this driver.
594
594
  */
595
595
  getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;
596
+ /**
597
+ * Bring the database's collection tables up to date, additively.
598
+ *
599
+ * Optional because it is only meaningful for schema-ful drivers. A managed
600
+ * runtime boots a compiled project against a database it has never seen; auth
601
+ * tables are ensured on boot but collection tables were created by nothing,
602
+ * so every data request answered 500 on a missing relation. The CLI's `db
603
+ * push` cannot fill the gap — it needs Atlas, and the runtime image ships no
604
+ * CLI.
605
+ *
606
+ * Implementations MUST be additive-only: create missing tables, columns and
607
+ * enum types, and never drop, narrow or rewrite anything. This runs
608
+ * unattended against live customer data with nobody reading a diff, so the
609
+ * destructive half stays a deliberate migration.
610
+ */
611
+ ensureCollectionSchema?(collections: unknown[], driverResult: InitializedDriver, log?: (message: string) => void): Promise<{
612
+ applied: number;
613
+ }>;
596
614
  /**
597
615
  * Initialize WebSocket server for realtime operations.
598
616
  */
@@ -24,6 +24,38 @@ export interface StorageAuthorizeContext {
24
24
  user: StorageAuthorizeUser | null;
25
25
  /** Named backend the request targeted, when one was given. */
26
26
  storageId?: string;
27
+ /**
28
+ * Trusted, RLS-bypassing data access, so the hook can answer the question it
29
+ * actually has to answer: *who owns this object?*
30
+ *
31
+ * Without it the hook can only do prefix arithmetic on the key, which
32
+ * expresses no real multi-tenant rule — ownership lives in a row, not in a
33
+ * string. And it cannot simply import the server to get one: a project
34
+ * declares this hook from its **config** package, which depends on
35
+ * `@rebasepro/types` alone and cannot resolve `@rebasepro/server` at
36
+ * runtime. So the accessor is handed in.
37
+ *
38
+ * It bypasses row-level security on purpose. The hook IS the authorization
39
+ * decision; asking it to make that decision through a reader that has
40
+ * already been narrowed by the caller's own permissions is circular.
41
+ */
42
+ data?: StorageAuthorizeData;
43
+ }
44
+ /**
45
+ * The slice of the data API a storage hook needs: read a collection, in the
46
+ * trusted server context.
47
+ *
48
+ * Deliberately read-only and deliberately tiny. A hook that can write is a hook
49
+ * that can be tricked into writing, and an authorization check has no business
50
+ * mutating anything.
51
+ */
52
+ export interface StorageAuthorizeData {
53
+ collection(slug: string): {
54
+ find(query?: Record<string, unknown>): Promise<{
55
+ data: Record<string, unknown>[];
56
+ }>;
57
+ findById(id: string): Promise<Record<string, unknown> | null>;
58
+ };
27
59
  }
28
60
  /**
29
61
  * Per-object access control for storage, the analogue of an RLS policy on a
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/types",
3
3
  "type": "module",
4
- "version": "0.10.1-canary.b1e3dbf",
4
+ "version": "0.10.1-canary.d8d45b2",
5
5
  "description": "Rebase type definitions — shared interfaces and controller types",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -777,6 +777,27 @@ export interface BackendBootstrapper {
777
777
  */
778
778
  getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;
779
779
 
780
+ /**
781
+ * Bring the database's collection tables up to date, additively.
782
+ *
783
+ * Optional because it is only meaningful for schema-ful drivers. A managed
784
+ * runtime boots a compiled project against a database it has never seen; auth
785
+ * tables are ensured on boot but collection tables were created by nothing,
786
+ * so every data request answered 500 on a missing relation. The CLI's `db
787
+ * push` cannot fill the gap — it needs Atlas, and the runtime image ships no
788
+ * CLI.
789
+ *
790
+ * Implementations MUST be additive-only: create missing tables, columns and
791
+ * enum types, and never drop, narrow or rewrite anything. This runs
792
+ * unattended against live customer data with nobody reading a diff, so the
793
+ * destructive half stays a deliberate migration.
794
+ */
795
+ ensureCollectionSchema?(
796
+ collections: unknown[],
797
+ driverResult: InitializedDriver,
798
+ log?: (message: string) => void
799
+ ): Promise<{ applied: number }>;
800
+
780
801
  /**
781
802
  * Initialize WebSocket server for realtime operations.
782
803
  */
@@ -27,6 +27,37 @@ export interface StorageAuthorizeContext {
27
27
  user: StorageAuthorizeUser | null;
28
28
  /** Named backend the request targeted, when one was given. */
29
29
  storageId?: string;
30
+ /**
31
+ * Trusted, RLS-bypassing data access, so the hook can answer the question it
32
+ * actually has to answer: *who owns this object?*
33
+ *
34
+ * Without it the hook can only do prefix arithmetic on the key, which
35
+ * expresses no real multi-tenant rule — ownership lives in a row, not in a
36
+ * string. And it cannot simply import the server to get one: a project
37
+ * declares this hook from its **config** package, which depends on
38
+ * `@rebasepro/types` alone and cannot resolve `@rebasepro/server` at
39
+ * runtime. So the accessor is handed in.
40
+ *
41
+ * It bypasses row-level security on purpose. The hook IS the authorization
42
+ * decision; asking it to make that decision through a reader that has
43
+ * already been narrowed by the caller's own permissions is circular.
44
+ */
45
+ data?: StorageAuthorizeData;
46
+ }
47
+
48
+ /**
49
+ * The slice of the data API a storage hook needs: read a collection, in the
50
+ * trusted server context.
51
+ *
52
+ * Deliberately read-only and deliberately tiny. A hook that can write is a hook
53
+ * that can be tricked into writing, and an authorization check has no business
54
+ * mutating anything.
55
+ */
56
+ export interface StorageAuthorizeData {
57
+ collection(slug: string): {
58
+ find(query?: Record<string, unknown>): Promise<{ data: Record<string, unknown>[] }>;
59
+ findById(id: string): Promise<Record<string, unknown> | null>;
60
+ };
30
61
  }
31
62
 
32
63
  /**