@rebasepro/server 0.21.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -11
- package/dist/api/errors.d.ts +41 -0
- package/dist/auth/admin-user-ops.d.ts +23 -1
- package/dist/auth/auth-hooks.d.ts +4 -0
- package/dist/auth/reset-password-admin.d.ts +4 -0
- package/dist/auth/token-revocation.d.ts +39 -5
- package/dist/{auth-gzoRjnuh.js → auth-BS4WBA10.js} +1636 -1565
- package/dist/auth-BS4WBA10.js.map +1 -0
- package/dist/backup/backup-common.d.ts +2 -1
- package/dist/{backup-DGu0v9Ku.js → backup-DzI9jLwc.js} +38 -10
- package/dist/backup-DzI9jLwc.js.map +1 -0
- package/dist/collections/validate-config.d.ts +4 -2
- package/dist/{cron-routes-BamgO0Xr.js → cron-routes-B_wlLybo.js} +3 -3
- package/dist/{cron-routes-BamgO0Xr.js.map → cron-routes-B_wlLybo.js.map} +1 -1
- package/dist/{cron-scheduler-D47tdB9T.js → cron-scheduler-COPQxlEq.js} +59 -10
- package/dist/cron-scheduler-COPQxlEq.js.map +1 -0
- package/dist/{cron-store-yXOyIEmn.js → cron-store-Dp0X7zRB.js} +2 -2
- package/dist/{cron-store-yXOyIEmn.js.map → cron-store-Dp0X7zRB.js.map} +1 -1
- package/dist/ddl-bootstrap-CfNvxMuK.js.map +1 -1
- package/dist/deploy/pod-contract.d.ts +14 -0
- package/dist/{errors-DMImyqyR.js → errors-DWsX4yTd.js} +55 -18
- package/dist/errors-DWsX4yTd.js.map +1 -0
- package/dist/{function-routes-C4nB2h0z.js → function-routes-Chet4-lB.js} +2 -2
- package/dist/{function-routes-C4nB2h0z.js.map → function-routes-Chet4-lB.js.map} +1 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-hBGuXcZc.js → history-recorder-5VKT7nss.js} +2 -2
- package/dist/{history-recorder-hBGuXcZc.js.map → history-recorder-5VKT7nss.js.map} +1 -1
- package/dist/{history-store-C9EtGtoL.js → history-store-By2R_G1b.js} +2 -2
- package/dist/{history-store-C9EtGtoL.js.map → history-store-By2R_G1b.js.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +286 -245
- package/dist/index.es.js.map +1 -1
- package/dist/init/middlewares.d.ts +19 -1
- package/dist/init/storage.d.ts +17 -0
- package/dist/init.d.ts +6 -2
- package/dist/{jobs-DZ8DLLcU.js → jobs-1IkI4Lle.js} +2 -2
- package/dist/{jobs-DZ8DLLcU.js.map → jobs-1IkI4Lle.js.map} +1 -1
- package/dist/{logs-routes-DnJINsMu.js → logs-routes-3EEzPjhl.js} +2 -2
- package/dist/{logs-routes-DnJINsMu.js.map → logs-routes-3EEzPjhl.js.map} +1 -1
- package/dist/{openapi-generator-CAFKF36E.js → openapi-generator-BDpoKZQ9.js} +2 -2
- package/dist/{openapi-generator-CAFKF36E.js.map → openapi-generator-BDpoKZQ9.js.map} +1 -1
- package/dist/{query-parser-uqzkZ82i.js → query-parser-C-rl30ce.js} +3 -3
- package/dist/{query-parser-uqzkZ82i.js.map → query-parser-C-rl30ce.js.map} +1 -1
- package/dist/{request-timeout-BR-OBwES.js → request-timeout-C_4C2BeR.js} +2 -2
- package/dist/{request-timeout-BR-OBwES.js.map → request-timeout-C_4C2BeR.js.map} +1 -1
- package/dist/{schema-editor-routes-BKOmdf4M.js → schema-editor-routes-DdLihzp0.js} +2 -2
- package/dist/{schema-editor-routes-BKOmdf4M.js.map → schema-editor-routes-DdLihzp0.js.map} +1 -1
- package/dist/src-Br6ARbs6.js.map +1 -1
- package/dist/{src-DW1cQ_28.js → src-Caz7nREj.js} +2 -3
- package/dist/src-Caz7nREj.js.map +1 -0
- package/package.json +5 -5
- package/dist/auth-gzoRjnuh.js.map +0 -1
- package/dist/backup-DGu0v9Ku.js.map +0 -1
- package/dist/cron-scheduler-D47tdB9T.js.map +0 -1
- package/dist/errors-DMImyqyR.js.map +0 -1
- package/dist/src-DW1cQ_28.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ddl-bootstrap-CfNvxMuK.js","names":[],"sources":["../../types/src/types/backend.ts","../src/boot/ddl-bootstrap.ts"],"sourcesContent":["import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { OrderByTuple } from \"./filter-operators\";\nimport type { LogicalCondition } from \"../controllers/data\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\nimport type { SchemaEditingAdmin } from \"./schema_editing\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /**\n * An `or(...)`/`and(...)` group, alongside `filter`.\n *\n * Counted as well as fetched, or `total` describes a different set of rows\n * from the one that was served — the same reason `filter` is here.\n */\n logical?: LogicalCondition;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * Declared here because a subscription is a query, and every field a query\n * has this one needs too. It was missing, so the type-checked boundary\n * dropped it: the client sent the group, nothing rejected it, and the\n * subscription re-fetched with the group gone — pushing every row the\n * caller's policies allowed rather than the ones they asked for. The same\n * defect `FetchCollectionProps.logical` documents, one layer up.\n */\n logical?: LogicalCondition;\n /**\n * Where the subscription's page starts. Missing for the same reason, with\n * a quieter symptom: a subscriber watching page two was pushed page one,\n * and a `collection_update` frame carries no window for it to notice with.\n */\n offset?: number;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched. */\n searchExplain?: boolean;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void\n ): void;\n\n /**\n * Subscribe to single entity changes\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `rebase.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>\n & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;\n\n/**\n * Type guard: can this admin plan a live schema change?\n *\n * Planning is engine-specific — it renders DDL, a Drizzle schema and the\n * declarative SQL artifacts — so the implementation lives in the driver\n * package. The server detects the capability structurally, exactly as it does\n * for SQL, rather than importing an engine it is supposed to know nothing\n * about.\n *\n * @group Admin\n */\nexport function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {\n return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === \"function\";\n}\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Ask the database whether it is there, before anything else touches it.\n *\n * Boot's first database call is not `initializeDriver` — it is the schema\n * provisioning that runs ahead of it, and a driver's connection diagnosis\n * therefore never got the chance to run. A stopped database produced\n * `Failed query: [redacted]` and a stack through drizzle internals: no host,\n * no port, no `ECONNREFUSED`, and no hint about starting the thing.\n *\n * Implementations MUST issue the cheapest round trip they have (`SELECT 1`),\n * MUST throw an error whose message names the host, the port and the\n * driver's own reason, and MAY log a fuller diagnosis first. They MUST NOT\n * throw for a reachable database that merely answered something unexpected —\n * the caller treats a throw as fatal.\n *\n * `driverResult` is optional for the same reason as\n * {@link ensureCollectionSchema}: this runs before `initializeDriver`, so an\n * adapter that was constructed with its own connection has to fall back to\n * it.\n */\n verifyConnection?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n *\n * `driverResult` is optional: this runs before `initializeDriver`, and only\n * the bundle path has a pre-init stand-in to pass. An adapter built by an\n * application already holds its own connection and MUST use it when this is\n * `undefined` — dereferencing it unconditionally works for managed tenants\n * and breaks every app that builds its own adapter.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Apply the collections' row-level-security policies, additively and\n * idempotently — the companion to {@link ensureCollectionSchema}.\n *\n * That method creates the tables; a table with RLS disabled and no policies\n * is not servable, because authenticated requests run as a restricted role:\n * a read with no `SELECT` policy returns nothing (a public collection\n * answers 401) and a write with no `INSERT`/`UPDATE` policy is denied. The\n * `db push` CLI applies these from the same collections, but it cannot reach\n * a managed tenant's in-cluster database — the runtime, already connected,\n * is the only thing that can.\n *\n * MUST be idempotent (re-run on every boot) and MUST NOT be destructive.\n * Runs after auth initialization, because the generated policies call the\n * `auth.*` helper functions and `CREATE POLICY` validates they exist.\n */\n ensureCollectionPolicies?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Create the RLS helper functions on this source's database. See\n * `DatabaseAdapter.ensureRlsRuntime`; needed on every source that is not\n * the default, whose helpers arrive with the auth tables.\n */\n ensureRlsRuntime?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Re-check, after the schema exists, that requests will actually be\n * constrained by the database's own authorization.\n *\n * A driver that isolates user requests by switching to a restricted role has\n * to decide at connect time whether the switch is needed — and on a fresh\n * database that question is asked before there is anything to answer with.\n * The process then creates the schema, becomes its owner, and an owner is\n * exempt from the policies on what it owns. So the answer that was true when\n * the driver initialized can be false by the time it serves a request.\n *\n * This is where a driver asks again. It runs once, after collection tables,\n * auth tables and policies are all in place, and it MUST fail rather than\n * serve when the answer changed and cannot be acted on: booting anyway\n * produces exactly the unenforced server this exists to prevent.\n *\n * Optional, because it is only meaningful for drivers whose isolation\n * depends on state the schema affects. A driver with nothing to re-check\n * omits it.\n */\n finalizeSecurityPosture?(driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Read the collections schema version this database was last provisioned\n * from, or `null` when nothing has ever stamped it.\n *\n * The companion to {@link stampCollectionsSchemaVersion}: one process writes\n * what it applied, every other process compares itself to it. This is what\n * lets a split deployment — several processes over one database, only one of\n * which provisions — notice that a unit is serving against a schema it was\n * not built for. That failure is otherwise silent in both directions: a\n * column that does not exist is a SQL error on one route, and a policy that\n * was never applied is a 200 with no rows.\n *\n * `null` is not an error and MUST NOT be treated as one — every database\n * provisioned before the stamp existed reads this way, and so does every\n * fresh one until its first provisioning boot finishes.\n */\n readCollectionsSchemaVersion?(\n driverResult?: InitializedDriver\n ): Promise<string | null>;\n\n /**\n * Record the collections schema version this process just applied.\n *\n * Called only by the process that provisions, and only after both\n * {@link ensureCollectionSchema} and {@link ensureCollectionPolicies} have\n * run — a stamp written before the policies would claim a schema that is\n * only half in place, and the half that is missing is the one that fails\n * without an error.\n */\n stampCollectionsSchemaVersion?(\n version: string,\n driverResult?: InitializedDriver\n ): Promise<void>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Role management service (optional, roles are now simple strings). */\n roleService?: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n","import { logger } from \"../utils/logger.js\";\n\n/**\n * Helpers for the \"create my internal table if it isn't there yet\" bootstrap\n * that several stores run at boot.\n *\n * These all look trivially safe — every statement is `IF NOT EXISTS` — and they\n * are not, for two reasons that only show up with more than one app instance:\n *\n * 1. `CREATE … IF NOT EXISTS` reads the catalog and then writes to it, and\n * those two steps are not one atomic operation. Instances starting together\n * — a rolling deploy, a replica count going from 1 to N, a crash loop\n * restarting the fleet — can both see \"absent\" and both try to create. The\n * loser gets a duplicate key on a *catalog* index instead of the silent\n * no-op the syntax appears to promise. Measured against Postgres 18: with\n * five instances booting at once, 8 of 10 `ensureTable` calls hit it.\n *\n * 2. A bootstrap written as one long `try` block therefore abandons everything\n * after the losing statement — including, in every store that has one, the\n * `REVOKE` that takes the table back off the end-user role. That revoke is\n * a security control and must not be collateral damage from a race.\n *\n * So: retry the race, contain each statement separately, and decide what to do\n * next from what actually exists rather than from who won.\n */\n\n/** A driver's `executeSql`, narrowed to what a bootstrap needs. */\nexport type SqlExec = (sqlText: string, options?: { params?: unknown[] }) => Promise<Record<string, unknown>[]>;\n\n\n/**\n * SQLSTATEs a *simultaneous boot* can raise from statements that are otherwise\n * idempotent. Retrying is always the right answer for these: the second attempt\n * finds the object present and does nothing.\n */\nexport const CONCURRENT_DDL_SQLSTATES = new Set([\n \"23505\", // unique_violation on pg_type / pg_class / pg_namespace\n \"42P06\", // duplicate_schema\n \"42P07\", // duplicate_table\n \"42710\", // duplicate_object — an index or a constraint\n \"40P01\" // deadlock_detected — two boots taking catalog locks in step\n]);\n\n/** Attempts per idempotent DDL statement, including the first. */\nexport const DDL_ATTEMPTS = 4;\nconst DDL_RETRY_BASE_MS = 40;\n\n/**\n * Walk an error's `cause` chain, stopping at the first link `visit` accepts.\n * Drizzle wraps the driver error, so nothing useful is ever on the top level.\n */\nexport function hasInCauseChain(err: unknown, visit: (e: Record<string, unknown>) => boolean): boolean {\n let current: unknown = err;\n for (let depth = 0; depth < 10 && current; depth++) {\n if (typeof current !== \"object\") break;\n const e = current as Record<string, unknown>;\n if (visit(e)) return true;\n current = e.cause;\n }\n return false;\n}\n\n/**\n * Is this the loser of a race to create something that already exists?\n *\n * Deliberately narrow. A permission failure, an unreachable database or a typo\n * in the DDL must surface on the first attempt rather than being retried four\n * times and then reported as a race that never was.\n */\nexport function isConcurrentDdlRace(err: unknown): boolean {\n return hasInCauseChain(err, (e) =>\n (typeof e.code === \"string\" && CONCURRENT_DDL_SQLSTATES.has(e.code)) ||\n // SQLite and MySQL say it in words rather than in a shared SQLSTATE.\n (typeof e.message === \"string\" && /already exists/i.test(e.message))\n );\n}\n\n/**\n * SQLSTATEs that mean, unambiguously, *the object is already there*.\n *\n * A subset of {@link CONCURRENT_DDL_SQLSTATES} and a stricter question. The\n * broad set answers \"should this be retried\"; this one answers \"is it safe to\n * carry on as though the statement had succeeded\", which is a claim about the\n * end state rather than about the attempt. Deadlock is not in it — a deadlocked\n * statement did nothing and must be retried, not skipped.\n */\nconst DUPLICATE_OBJECT_SQLSTATES = new Set([\n \"42P06\", // duplicate_schema\n \"42P07\", // duplicate_table\n \"42710\" // duplicate_object — a type, an index, a constraint\n]);\n\n/**\n * Did this statement fail *because a peer already created the same object*?\n *\n * The narrow companion to {@link isConcurrentDdlRace}, for the one caller that\n * needs to tell \"someone beat me to it\" from \"this genuinely failed\": a loop\n * applying a schema plan, where treating every `23505` as a harmless race would\n * silently swallow the one that matters — a unique constraint that cannot be\n * added because the customer's existing rows violate it.\n *\n * `23505` is therefore only accepted when it names a `pg_catalog` index. That is\n * what a lost `CREATE TYPE`/`CREATE TABLE` race raises (`pg_type_typname_nsp_index`\n * is the one seen in practice); a unique violation on user data names the user's\n * own constraint and is left to the caller.\n *\n * With one exception, in {@link NOT_A_RACE_PG_INDEXES}.\n */\nexport function isDuplicateObjectRace(err: unknown): boolean {\n return hasInCauseChain(err, (e) => {\n if (typeof e.code !== \"string\") return false;\n if (DUPLICATE_OBJECT_SQLSTATES.has(e.code)) return true;\n if (e.code !== \"23505\") return false;\n // node-postgres puts the violated index in `constraint`; some paths only\n // carry it in the detail text, so check both rather than miss the race.\n const constraint = typeof e.constraint === \"string\" ? e.constraint : \"\";\n const detail = typeof e.detail === \"string\" ? e.detail : \"\";\n for (const index of NOT_A_RACE_PG_INDEXES) {\n if (constraint === index || detail.includes(index)) return false;\n }\n return constraint.startsWith(\"pg_\") || /\\bpg_[a-z_]+_index\\b/.test(detail);\n });\n}\n\n/**\n * `pg_catalog` indexes whose `23505` is never a race.\n *\n * `pg_enum_typid_label_index` is unique on (type, label), and one statement\n * violates it on its own: `CREATE TYPE t AS ENUM ('draft', 'draft')` — which is\n * what a collection with two enum entries carrying the same `id` generates.\n * Under the `pg_` prefix rule that read as \"a peer created it first\", so the\n * type was never created, boot carried on, and the column fell back to `TEXT`\n * with no enum behind it. Verified against PGlite: the duplicate `CREATE TYPE`\n * raises exactly this.\n *\n * The concurrent-boot case this rule exists for does not come through here at\n * all. Two pods adding the same label race on `ALTER TYPE … ADD VALUE`, which\n * raises `42710` (already in {@link DUPLICATE_OBJECT_SQLSTATES}) — and the\n * generator writes `ADD VALUE IF NOT EXISTS`, so it usually raises nothing.\n *\n * The duplicate `id` is rejected earlier now, by `validate-config`, which names\n * the property. This is the second line: a config that reaches the database with\n * one fails loudly instead of degrading.\n */\nconst NOT_A_RACE_PG_INDEXES = [\"pg_enum_typid_label_index\"];\n\nexport interface DdlBootstrapper {\n /**\n * Run one idempotent statement — `CREATE … IF NOT EXISTS`, `ALTER TABLE …\n * ADD COLUMN IF NOT EXISTS` — retrying the catalog race a simultaneous boot\n * produces. Never throws: a statement that cannot be made to work is logged\n * and the caller carries on to the next one.\n */\n ensureObject(label: string, sqlText: string): Promise<void>;\n\n /** Contain one step's failure so that the steps after it still run. */\n step(label: string, run: () => Promise<unknown>): Promise<void>;\n\n /**\n * Is this table there and readable? Asked with a query any SQL dialect\n * answers, rather than `to_regclass`, so a future non-Postgres SQL driver\n * gets a real answer instead of a syntax error read as \"missing\".\n */\n isReadable(table: string): Promise<boolean>;\n}\n\n/**\n * @param exec the driver's SQL escape hatch\n * @param scope log prefix identifying the caller, e.g. `\"cron-store\"`\n */\nexport function createDdlBootstrapper(exec: SqlExec, scope: string): DdlBootstrapper {\n /** Jittered, so peers that collided once do not collide again in lockstep. */\n const backoff = (attempt: number) =>\n new Promise(resolve => setTimeout(resolve, DDL_RETRY_BASE_MS * attempt * (1 + Math.random())));\n\n const step: DdlBootstrapper[\"step\"] = async (label, run) => {\n try {\n await run();\n } catch (err) {\n logger.error(`[${scope}] ${label} failed`, { error: err });\n }\n };\n\n return {\n step,\n\n ensureObject(label, sqlText) {\n return step(label, async () => {\n for (let attempt = 1; ; attempt++) {\n try {\n await exec(sqlText);\n return;\n } catch (err) {\n if (!isConcurrentDdlRace(err) || attempt >= DDL_ATTEMPTS) throw err;\n logger.debug(\n `[${scope}] Lost a create race for ${label} with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await backoff(attempt);\n }\n }\n });\n },\n\n async isReadable(table) {\n try {\n await exec(`SELECT 1 FROM ${table} WHERE false`);\n return true;\n } catch {\n return false;\n }\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqkBA,SAAgB,qBAAqB,OAA+D;CAChG,OAAO,CAAC,CAAC,SAAS,OAAQ,MAA6B,qBAAqB;AAChF;;;;;AAMA,SAAgB,WAAW,OAAqD;CAC5E,OAAO,CAAC,CAAC,SAAS,OAAQ,MAAmB,eAAe;AAChE;;;;;;;;AC5iBA,IAAa,2CAA2B,IAAI,IAAI;CAC5C;CACA;CACA;CACA;CACA;AACJ,CAAC;AAID,IAAM,oBAAoB;;;;;AAM1B,SAAgB,gBAAgB,KAAc,OAAyD;CACnG,IAAI,UAAmB;CACvB,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,SAAS,SAAS;EAChD,IAAI,OAAO,YAAY,UAAU;EACjC,MAAM,IAAI;EACV,IAAI,MAAM,CAAC,GAAG,OAAO;EACrB,UAAU,EAAE;CAChB;CACA,OAAO;AACX;;;;;;;;AASA,SAAgB,oBAAoB,KAAuB;CACvD,OAAO,gBAAgB,MAAM,MACxB,OAAO,EAAE,SAAS,YAAY,yBAAyB,IAAI,EAAE,IAAI,KAEjE,OAAO,EAAE,YAAY,YAAY,kBAAkB,KAAK,EAAE,OAAO,CACtE;AACJ;;;;;;;;;;AAWA,IAAM,6CAA6B,IAAI,IAAI;CACvC;CACA;CACA;AACJ,CAAC;;;;;;;;;;;;;;;;;AAkBD,SAAgB,sBAAsB,KAAuB;CACzD,OAAO,gBAAgB,MAAM,MAAM;EAC/B,IAAI,OAAO,EAAE,SAAS,UAAU,OAAO;EACvC,IAAI,2BAA2B,IAAI,EAAE,IAAI,GAAG,OAAO;EACnD,IAAI,EAAE,SAAS,SAAS,OAAO;EAG/B,MAAM,aAAa,OAAO,EAAE,eAAe,WAAW,EAAE,aAAa;EACrE,MAAM,SAAS,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;EACzD,KAAK,MAAM,SAAS,uBAChB,IAAI,eAAe,SAAS,OAAO,SAAS,KAAK,GAAG,OAAO;EAE/D,OAAO,WAAW,WAAW,KAAK,KAAK,uBAAuB,KAAK,MAAM;CAC7E,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAM,wBAAwB,CAAC,2BAA2B;;;;;AA0B1D,SAAgB,sBAAsB,MAAe,OAAgC;;CAEjF,MAAM,WAAW,YACb,IAAI,SAAQ,YAAW,WAAW,SAAS,oBAAoB,WAAW,IAAI,KAAK,OAAO,EAAE,CAAC;CAEjG,MAAM,OAAgC,OAAO,OAAO,QAAQ;EACxD,IAAI;GACA,MAAM,IAAI;EACd,SAAS,KAAK;GACV,OAAO,MAAM,IAAI,MAAM,IAAI,MAAM,UAAU,EAAE,OAAO,IAAI,CAAC;EAC7D;CACJ;CAEA,OAAO;EACH;EAEA,aAAa,OAAO,SAAS;GACzB,OAAO,KAAK,OAAO,YAAY;IAC3B,KAAK,IAAI,UAAU,IAAK,WACpB,IAAI;KACA,MAAM,KAAK,OAAO;KAClB;IACJ,SAAS,KAAK;KACV,IAAI,CAAC,oBAAoB,GAAG,KAAK,WAAA,GAAyB,MAAM;KAChE,OAAO,MACH,IAAI,MAAM,2BAA2B,MAAM,kCAC/B,QAAQ,eACxB;KACA,MAAM,QAAQ,OAAO;IACzB;GAER,CAAC;EACL;EAEA,MAAM,WAAW,OAAO;GACpB,IAAI;IACA,MAAM,KAAK,iBAAiB,MAAM,aAAa;IAC/C,OAAO;GACX,QAAQ;IACJ,OAAO;GACX;EACJ;CACJ;AACJ"}
|
|
1
|
+
{"version":3,"file":"ddl-bootstrap-CfNvxMuK.js","names":[],"sources":["../../types/src/types/backend.ts","../src/boot/ddl-bootstrap.ts"],"sourcesContent":["import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { OrderByTuple } from \"./filter-operators\";\nimport type { LogicalCondition } from \"../controllers/data\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\nimport type { SchemaEditingAdmin } from \"./schema_editing\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /**\n * An `or(...)`/`and(...)` group, alongside `filter`.\n *\n * Counted as well as fetched, or `total` describes a different set of rows\n * from the one that was served — the same reason `filter` is here.\n */\n logical?: LogicalCondition;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * Declared here because a subscription is a query, and every field a query\n * has this one needs too. It was missing, so the type-checked boundary\n * dropped it: the client sent the group, nothing rejected it, and the\n * subscription re-fetched with the group gone — pushing every row the\n * caller's policies allowed rather than the ones they asked for. The same\n * defect `FetchCollectionProps.logical` documents, one layer up.\n */\n logical?: LogicalCondition;\n /**\n * Where the subscription's page starts. Missing for the same reason, with\n * a quieter symptom: a subscriber watching page two was pushed page one,\n * and a `collection_update` frame carries no window for it to notice with.\n */\n offset?: number;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched. */\n searchExplain?: boolean;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes.\n *\n * `onError` is called when a fetch behind the subscription fails, a\n * refetch after a change included, with the error as it was thrown. It is\n * not called for a fetch that newer rows have already overtaken, or after\n * the subscription is gone. Without it, a subscriber whose fetch failed\n * was told nothing and kept waiting for rows.\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void,\n onError?: (error: unknown) => void\n ): void;\n\n /**\n * Subscribe to single entity changes. `onError` as for\n * {@link RealtimeProvider.subscribeToCollection}.\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void,\n onError?: (error: unknown) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `rebase.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>\n & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;\n\n/**\n * Type guard: can this admin plan a live schema change?\n *\n * Planning is engine-specific — it renders DDL, a Drizzle schema and the\n * declarative SQL artifacts — so the implementation lives in the driver\n * package. The server detects the capability structurally, exactly as it does\n * for SQL, rather than importing an engine it is supposed to know nothing\n * about.\n *\n * @group Admin\n */\nexport function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {\n return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === \"function\";\n}\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Ask the database whether it is there, before anything else touches it.\n *\n * Boot's first database call is not `initializeDriver` — it is the schema\n * provisioning that runs ahead of it, and a driver's connection diagnosis\n * therefore never got the chance to run. A stopped database produced\n * `Failed query: [redacted]` and a stack through drizzle internals: no host,\n * no port, no `ECONNREFUSED`, and no hint about starting the thing.\n *\n * Implementations MUST issue the cheapest round trip they have (`SELECT 1`),\n * MUST throw an error whose message names the host, the port and the\n * driver's own reason, and MAY log a fuller diagnosis first. They MUST NOT\n * throw for a reachable database that merely answered something unexpected —\n * the caller treats a throw as fatal.\n *\n * `driverResult` is optional for the same reason as\n * {@link ensureCollectionSchema}: this runs before `initializeDriver`, so an\n * adapter that was constructed with its own connection has to fall back to\n * it.\n */\n verifyConnection?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n *\n * `driverResult` is optional: this runs before `initializeDriver`, and only\n * the bundle path has a pre-init stand-in to pass. An adapter built by an\n * application already holds its own connection and MUST use it when this is\n * `undefined` — dereferencing it unconditionally works for managed tenants\n * and breaks every app that builds its own adapter.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Apply the collections' row-level-security policies, additively and\n * idempotently — the companion to {@link ensureCollectionSchema}.\n *\n * That method creates the tables; a table with RLS disabled and no policies\n * is not servable, because authenticated requests run as a restricted role:\n * a read with no `SELECT` policy returns nothing (a public collection\n * answers 401) and a write with no `INSERT`/`UPDATE` policy is denied. The\n * `db push` CLI applies these from the same collections, but it cannot reach\n * a managed tenant's in-cluster database — the runtime, already connected,\n * is the only thing that can.\n *\n * MUST be idempotent (re-run on every boot) and MUST NOT be destructive.\n * Runs after auth initialization, because the generated policies call the\n * `auth.*` helper functions and `CREATE POLICY` validates they exist.\n */\n ensureCollectionPolicies?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Create the RLS helper functions on this source's database. See\n * `DatabaseAdapter.ensureRlsRuntime`; needed on every source that is not\n * the default, whose helpers arrive with the auth tables.\n */\n ensureRlsRuntime?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Re-check, after the schema exists, that requests will actually be\n * constrained by the database's own authorization.\n *\n * A driver that isolates user requests by switching to a restricted role has\n * to decide at connect time whether the switch is needed — and on a fresh\n * database that question is asked before there is anything to answer with.\n * The process then creates the schema, becomes its owner, and an owner is\n * exempt from the policies on what it owns. So the answer that was true when\n * the driver initialized can be false by the time it serves a request.\n *\n * This is where a driver asks again. It runs once, after collection tables,\n * auth tables and policies are all in place, and it MUST fail rather than\n * serve when the answer changed and cannot be acted on: booting anyway\n * produces exactly the unenforced server this exists to prevent.\n *\n * Optional, because it is only meaningful for drivers whose isolation\n * depends on state the schema affects. A driver with nothing to re-check\n * omits it.\n */\n finalizeSecurityPosture?(driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Read the collections schema version this database was last provisioned\n * from, or `null` when nothing has ever stamped it.\n *\n * The companion to {@link stampCollectionsSchemaVersion}: one process writes\n * what it applied, every other process compares itself to it. This is what\n * lets a split deployment — several processes over one database, only one of\n * which provisions — notice that a unit is serving against a schema it was\n * not built for. That failure is otherwise silent in both directions: a\n * column that does not exist is a SQL error on one route, and a policy that\n * was never applied is a 200 with no rows.\n *\n * `null` is not an error and MUST NOT be treated as one — every database\n * provisioned before the stamp existed reads this way, and so does every\n * fresh one until its first provisioning boot finishes.\n */\n readCollectionsSchemaVersion?(\n driverResult?: InitializedDriver\n ): Promise<string | null>;\n\n /**\n * Record the collections schema version this process just applied.\n *\n * Called only by the process that provisions, and only after both\n * {@link ensureCollectionSchema} and {@link ensureCollectionPolicies} have\n * run — a stamp written before the policies would claim a schema that is\n * only half in place, and the half that is missing is the one that fails\n * without an error.\n */\n stampCollectionsSchemaVersion?(\n version: string,\n driverResult?: InitializedDriver\n ): Promise<void>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Role management service (optional, roles are now simple strings). */\n roleService?: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n","import { logger } from \"../utils/logger.js\";\n\n/**\n * Helpers for the \"create my internal table if it isn't there yet\" bootstrap\n * that several stores run at boot.\n *\n * These all look trivially safe — every statement is `IF NOT EXISTS` — and they\n * are not, for two reasons that only show up with more than one app instance:\n *\n * 1. `CREATE … IF NOT EXISTS` reads the catalog and then writes to it, and\n * those two steps are not one atomic operation. Instances starting together\n * — a rolling deploy, a replica count going from 1 to N, a crash loop\n * restarting the fleet — can both see \"absent\" and both try to create. The\n * loser gets a duplicate key on a *catalog* index instead of the silent\n * no-op the syntax appears to promise. Measured against Postgres 18: with\n * five instances booting at once, 8 of 10 `ensureTable` calls hit it.\n *\n * 2. A bootstrap written as one long `try` block therefore abandons everything\n * after the losing statement — including, in every store that has one, the\n * `REVOKE` that takes the table back off the end-user role. That revoke is\n * a security control and must not be collateral damage from a race.\n *\n * So: retry the race, contain each statement separately, and decide what to do\n * next from what actually exists rather than from who won.\n */\n\n/** A driver's `executeSql`, narrowed to what a bootstrap needs. */\nexport type SqlExec = (sqlText: string, options?: { params?: unknown[] }) => Promise<Record<string, unknown>[]>;\n\n\n/**\n * SQLSTATEs a *simultaneous boot* can raise from statements that are otherwise\n * idempotent. Retrying is always the right answer for these: the second attempt\n * finds the object present and does nothing.\n */\nexport const CONCURRENT_DDL_SQLSTATES = new Set([\n \"23505\", // unique_violation on pg_type / pg_class / pg_namespace\n \"42P06\", // duplicate_schema\n \"42P07\", // duplicate_table\n \"42710\", // duplicate_object — an index or a constraint\n \"40P01\" // deadlock_detected — two boots taking catalog locks in step\n]);\n\n/** Attempts per idempotent DDL statement, including the first. */\nexport const DDL_ATTEMPTS = 4;\nconst DDL_RETRY_BASE_MS = 40;\n\n/**\n * Walk an error's `cause` chain, stopping at the first link `visit` accepts.\n * Drizzle wraps the driver error, so nothing useful is ever on the top level.\n */\nexport function hasInCauseChain(err: unknown, visit: (e: Record<string, unknown>) => boolean): boolean {\n let current: unknown = err;\n for (let depth = 0; depth < 10 && current; depth++) {\n if (typeof current !== \"object\") break;\n const e = current as Record<string, unknown>;\n if (visit(e)) return true;\n current = e.cause;\n }\n return false;\n}\n\n/**\n * Is this the loser of a race to create something that already exists?\n *\n * Deliberately narrow. A permission failure, an unreachable database or a typo\n * in the DDL must surface on the first attempt rather than being retried four\n * times and then reported as a race that never was.\n */\nexport function isConcurrentDdlRace(err: unknown): boolean {\n return hasInCauseChain(err, (e) =>\n (typeof e.code === \"string\" && CONCURRENT_DDL_SQLSTATES.has(e.code)) ||\n // SQLite and MySQL say it in words rather than in a shared SQLSTATE.\n (typeof e.message === \"string\" && /already exists/i.test(e.message))\n );\n}\n\n/**\n * SQLSTATEs that mean, unambiguously, *the object is already there*.\n *\n * A subset of {@link CONCURRENT_DDL_SQLSTATES} and a stricter question. The\n * broad set answers \"should this be retried\"; this one answers \"is it safe to\n * carry on as though the statement had succeeded\", which is a claim about the\n * end state rather than about the attempt. Deadlock is not in it — a deadlocked\n * statement did nothing and must be retried, not skipped.\n */\nconst DUPLICATE_OBJECT_SQLSTATES = new Set([\n \"42P06\", // duplicate_schema\n \"42P07\", // duplicate_table\n \"42710\" // duplicate_object — a type, an index, a constraint\n]);\n\n/**\n * Did this statement fail *because a peer already created the same object*?\n *\n * The narrow companion to {@link isConcurrentDdlRace}, for the one caller that\n * needs to tell \"someone beat me to it\" from \"this genuinely failed\": a loop\n * applying a schema plan, where treating every `23505` as a harmless race would\n * silently swallow the one that matters — a unique constraint that cannot be\n * added because the customer's existing rows violate it.\n *\n * `23505` is therefore only accepted when it names a `pg_catalog` index. That is\n * what a lost `CREATE TYPE`/`CREATE TABLE` race raises (`pg_type_typname_nsp_index`\n * is the one seen in practice); a unique violation on user data names the user's\n * own constraint and is left to the caller.\n *\n * With one exception, in {@link NOT_A_RACE_PG_INDEXES}.\n */\nexport function isDuplicateObjectRace(err: unknown): boolean {\n return hasInCauseChain(err, (e) => {\n if (typeof e.code !== \"string\") return false;\n if (DUPLICATE_OBJECT_SQLSTATES.has(e.code)) return true;\n if (e.code !== \"23505\") return false;\n // node-postgres puts the violated index in `constraint`; some paths only\n // carry it in the detail text, so check both rather than miss the race.\n const constraint = typeof e.constraint === \"string\" ? e.constraint : \"\";\n const detail = typeof e.detail === \"string\" ? e.detail : \"\";\n for (const index of NOT_A_RACE_PG_INDEXES) {\n if (constraint === index || detail.includes(index)) return false;\n }\n return constraint.startsWith(\"pg_\") || /\\bpg_[a-z_]+_index\\b/.test(detail);\n });\n}\n\n/**\n * `pg_catalog` indexes whose `23505` is never a race.\n *\n * `pg_enum_typid_label_index` is unique on (type, label), and one statement\n * violates it on its own: `CREATE TYPE t AS ENUM ('draft', 'draft')` — which is\n * what a collection with two enum entries carrying the same `id` generates.\n * Under the `pg_` prefix rule that read as \"a peer created it first\", so the\n * type was never created, boot carried on, and the column fell back to `TEXT`\n * with no enum behind it. Verified against PGlite: the duplicate `CREATE TYPE`\n * raises exactly this.\n *\n * The concurrent-boot case this rule exists for does not come through here at\n * all. Two pods adding the same label race on `ALTER TYPE … ADD VALUE`, which\n * raises `42710` (already in {@link DUPLICATE_OBJECT_SQLSTATES}) — and the\n * generator writes `ADD VALUE IF NOT EXISTS`, so it usually raises nothing.\n *\n * The duplicate `id` is rejected earlier now, by `validate-config`, which names\n * the property. This is the second line: a config that reaches the database with\n * one fails loudly instead of degrading.\n */\nconst NOT_A_RACE_PG_INDEXES = [\"pg_enum_typid_label_index\"];\n\nexport interface DdlBootstrapper {\n /**\n * Run one idempotent statement — `CREATE … IF NOT EXISTS`, `ALTER TABLE …\n * ADD COLUMN IF NOT EXISTS` — retrying the catalog race a simultaneous boot\n * produces. Never throws: a statement that cannot be made to work is logged\n * and the caller carries on to the next one.\n */\n ensureObject(label: string, sqlText: string): Promise<void>;\n\n /** Contain one step's failure so that the steps after it still run. */\n step(label: string, run: () => Promise<unknown>): Promise<void>;\n\n /**\n * Is this table there and readable? Asked with a query any SQL dialect\n * answers, rather than `to_regclass`, so a future non-Postgres SQL driver\n * gets a real answer instead of a syntax error read as \"missing\".\n */\n isReadable(table: string): Promise<boolean>;\n}\n\n/**\n * @param exec the driver's SQL escape hatch\n * @param scope log prefix identifying the caller, e.g. `\"cron-store\"`\n */\nexport function createDdlBootstrapper(exec: SqlExec, scope: string): DdlBootstrapper {\n /** Jittered, so peers that collided once do not collide again in lockstep. */\n const backoff = (attempt: number) =>\n new Promise(resolve => setTimeout(resolve, DDL_RETRY_BASE_MS * attempt * (1 + Math.random())));\n\n const step: DdlBootstrapper[\"step\"] = async (label, run) => {\n try {\n await run();\n } catch (err) {\n logger.error(`[${scope}] ${label} failed`, { error: err });\n }\n };\n\n return {\n step,\n\n ensureObject(label, sqlText) {\n return step(label, async () => {\n for (let attempt = 1; ; attempt++) {\n try {\n await exec(sqlText);\n return;\n } catch (err) {\n if (!isConcurrentDdlRace(err) || attempt >= DDL_ATTEMPTS) throw err;\n logger.debug(\n `[${scope}] Lost a create race for ${label} with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await backoff(attempt);\n }\n }\n });\n },\n\n async isReadable(table) {\n try {\n await exec(`SELECT 1 FROM ${table} WHERE false`);\n return true;\n } catch {\n return false;\n }\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8kBA,SAAgB,qBAAqB,OAA+D;CAChG,OAAO,CAAC,CAAC,SAAS,OAAQ,MAA6B,qBAAqB;AAChF;;;;;AAMA,SAAgB,WAAW,OAAqD;CAC5E,OAAO,CAAC,CAAC,SAAS,OAAQ,MAAmB,eAAe;AAChE;;;;;;;;ACrjBA,IAAa,2CAA2B,IAAI,IAAI;CAC5C;CACA;CACA;CACA;CACA;AACJ,CAAC;AAID,IAAM,oBAAoB;;;;;AAM1B,SAAgB,gBAAgB,KAAc,OAAyD;CACnG,IAAI,UAAmB;CACvB,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,SAAS,SAAS;EAChD,IAAI,OAAO,YAAY,UAAU;EACjC,MAAM,IAAI;EACV,IAAI,MAAM,CAAC,GAAG,OAAO;EACrB,UAAU,EAAE;CAChB;CACA,OAAO;AACX;;;;;;;;AASA,SAAgB,oBAAoB,KAAuB;CACvD,OAAO,gBAAgB,MAAM,MACxB,OAAO,EAAE,SAAS,YAAY,yBAAyB,IAAI,EAAE,IAAI,KAEjE,OAAO,EAAE,YAAY,YAAY,kBAAkB,KAAK,EAAE,OAAO,CACtE;AACJ;;;;;;;;;;AAWA,IAAM,6CAA6B,IAAI,IAAI;CACvC;CACA;CACA;AACJ,CAAC;;;;;;;;;;;;;;;;;AAkBD,SAAgB,sBAAsB,KAAuB;CACzD,OAAO,gBAAgB,MAAM,MAAM;EAC/B,IAAI,OAAO,EAAE,SAAS,UAAU,OAAO;EACvC,IAAI,2BAA2B,IAAI,EAAE,IAAI,GAAG,OAAO;EACnD,IAAI,EAAE,SAAS,SAAS,OAAO;EAG/B,MAAM,aAAa,OAAO,EAAE,eAAe,WAAW,EAAE,aAAa;EACrE,MAAM,SAAS,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;EACzD,KAAK,MAAM,SAAS,uBAChB,IAAI,eAAe,SAAS,OAAO,SAAS,KAAK,GAAG,OAAO;EAE/D,OAAO,WAAW,WAAW,KAAK,KAAK,uBAAuB,KAAK,MAAM;CAC7E,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAM,wBAAwB,CAAC,2BAA2B;;;;;AA0B1D,SAAgB,sBAAsB,MAAe,OAAgC;;CAEjF,MAAM,WAAW,YACb,IAAI,SAAQ,YAAW,WAAW,SAAS,oBAAoB,WAAW,IAAI,KAAK,OAAO,EAAE,CAAC;CAEjG,MAAM,OAAgC,OAAO,OAAO,QAAQ;EACxD,IAAI;GACA,MAAM,IAAI;EACd,SAAS,KAAK;GACV,OAAO,MAAM,IAAI,MAAM,IAAI,MAAM,UAAU,EAAE,OAAO,IAAI,CAAC;EAC7D;CACJ;CAEA,OAAO;EACH;EAEA,aAAa,OAAO,SAAS;GACzB,OAAO,KAAK,OAAO,YAAY;IAC3B,KAAK,IAAI,UAAU,IAAK,WACpB,IAAI;KACA,MAAM,KAAK,OAAO;KAClB;IACJ,SAAS,KAAK;KACV,IAAI,CAAC,oBAAoB,GAAG,KAAK,WAAA,GAAyB,MAAM;KAChE,OAAO,MACH,IAAI,MAAM,2BAA2B,MAAM,kCAC/B,QAAQ,eACxB;KACA,MAAM,QAAQ,OAAO;IACzB;GAER,CAAC;EACL;EAEA,MAAM,WAAW,OAAO;GACpB,IAAI;IACA,MAAM,KAAK,iBAAiB,MAAM,aAAa;IAC/C,OAAO;GACX,QAAQ;IACJ,OAAO;GACX;EACJ;CACJ;AACJ"}
|
|
@@ -137,3 +137,17 @@ export declare const RUNTIME_MIN_TERMINATION_GRACE_SECONDS: number;
|
|
|
137
137
|
export declare const RUNTIME_STARTUP_BUDGET_SECONDS = 300;
|
|
138
138
|
/** Where a bundle is mounted, and what the runtime is told to read. */
|
|
139
139
|
export declare const RUNTIME_BUNDLE_MOUNT = "/bundle";
|
|
140
|
+
/**
|
|
141
|
+
* The request-body limit every API route has by default (`maxBodySize`,
|
|
142
|
+
* `REBASE_MAX_BODY_SIZE`), in bytes.
|
|
143
|
+
*
|
|
144
|
+
* It is not the largest body the runtime accepts. `POST /storage/upload` is
|
|
145
|
+
* exempt from it and meets the storage config's `maxFileSize` instead, whose
|
|
146
|
+
* default is `DEFAULT_MAX_FILE_SIZE` (`storage/types.ts`), 50 MB. A proxy in
|
|
147
|
+
* front of the runtime has to allow more than the larger of the two. Otherwise
|
|
148
|
+
* it refuses uploads the runtime would accept, with its own error page in
|
|
149
|
+
* place of the runtime's JSON 413. The chart's ingress was left at 12m after
|
|
150
|
+
* the upload route got its own limit, and admin-panel uploads between 12 and
|
|
151
|
+
* 50 MB failed that way.
|
|
152
|
+
*/
|
|
153
|
+
export declare const RUNTIME_DEFAULT_MAX_BODY_SIZE: number;
|
|
@@ -208,6 +208,49 @@ var ApiError = class ApiError extends Error {
|
|
|
208
208
|
}
|
|
209
209
|
};
|
|
210
210
|
/**
|
|
211
|
+
* The status, code and message an error carries as its own answer — or
|
|
212
|
+
* `undefined` for an error that carries none, which is a server fault and gets
|
|
213
|
+
* masked.
|
|
214
|
+
*
|
|
215
|
+
* Two classes carry one. The server's `ApiError`, and `RebaseApiError` (or its
|
|
216
|
+
* `RebaseClientError` subclass) from `@rebasepro/types` once it has a status.
|
|
217
|
+
* The second is the browser-safe class: a `config/collections/*.ts` file is
|
|
218
|
+
* bundled into the admin SPA and cannot import this package, so it is what a
|
|
219
|
+
* collection callback throws, and what a callback refusal becomes —
|
|
220
|
+
* `callbackRefusal` returns one, and `toCallbackError` wraps anything thrown
|
|
221
|
+
* that does not already carry a status.
|
|
222
|
+
*
|
|
223
|
+
* One function because several doors turn an error into an answer: the REST
|
|
224
|
+
* error handler, the two WebSocket servers, and the Postgres realtime
|
|
225
|
+
* subscriptions. Each used to list the classes it recognised by hand. The
|
|
226
|
+
* sockets listed only `ApiError`, so a `beforeDelete` veto that REST
|
|
227
|
+
* answered as 400 `CALLBACK_REJECTED` with the author's message reached the
|
|
228
|
+
* admin panel — which writes through the socket — as `INTERNAL_ERROR`, and in
|
|
229
|
+
* production as "An unexpected error occurred".
|
|
230
|
+
*
|
|
231
|
+
* Matched by name as well as `instanceof`: a monorepo can resolve two copies of
|
|
232
|
+
* a package, and `instanceof` is false across them. Name matching is also why
|
|
233
|
+
* this file needs no runtime import of `@rebasepro/types`, which it may not
|
|
234
|
+
* have — it is in the graph of `@rebasepro/server/functions`.
|
|
235
|
+
*/
|
|
236
|
+
function declaredErrorAnswer(error) {
|
|
237
|
+
if (error === null || typeof error !== "object") return void 0;
|
|
238
|
+
const e = error;
|
|
239
|
+
let status;
|
|
240
|
+
if (error instanceof ApiError || e.name === "ApiError") status = typeof e.statusCode === "number" ? e.statusCode : void 0;
|
|
241
|
+
else if (typeof e.name === "string" && /^Rebase(Api|Client)Error$/.test(e.name)) {
|
|
242
|
+
status = typeof e.statusCode === "number" ? e.statusCode : typeof e.status === "number" ? e.status : void 0;
|
|
243
|
+
if (status === void 0) return void 0;
|
|
244
|
+
} else return;
|
|
245
|
+
return {
|
|
246
|
+
status: status || 500,
|
|
247
|
+
code: typeof e.code === "string" && e.code ? e.code : "INTERNAL_ERROR",
|
|
248
|
+
message: typeof e.message === "string" ? e.message : String(e.message ?? ""),
|
|
249
|
+
...e.details !== void 0 && { details: e.details },
|
|
250
|
+
expected: error instanceof ApiError && error.expected
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
211
254
|
* Leave the code and the message where the request log will find them.
|
|
212
255
|
*
|
|
213
256
|
* A failed request used to produce two lines, each holding half of it: this
|
|
@@ -243,29 +286,23 @@ function requestWillBeLogged(c) {
|
|
|
243
286
|
var errorHandler = (err, c) => {
|
|
244
287
|
const error = err;
|
|
245
288
|
const reqId = typeof c.get === "function" ? c.get("requestId") : void 0;
|
|
246
|
-
const isBrowserSafeError = /^Rebase(Api|Client)Error$/.test(error.name);
|
|
247
289
|
const driftFor = (status) => (status === 400 || status === 404) && typeof c.get === "function" ? schemaDriftCause(c.get("schemaDrift")) : void 0;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
if (error instanceof ApiError || error.name === "ApiError" || isBrowserSafeError && typeof error.statusCode === "number") {
|
|
253
|
-
const expected = error instanceof ApiError && error.expected;
|
|
254
|
-
handOffToRequestLog(c, error.code || "INTERNAL_ERROR", error.message);
|
|
290
|
+
const answer = declaredErrorAnswer(error);
|
|
291
|
+
if (answer) {
|
|
292
|
+
handOffToRequestLog(c, answer.code, answer.message);
|
|
255
293
|
if (!requestWillBeLogged(c)) {
|
|
256
|
-
const line = `[API] ${c.req.method} ${c.req.path} → ${
|
|
257
|
-
if (expected) logger.debug(line);
|
|
294
|
+
const line = `[API] ${c.req.method} ${c.req.path} → ${answer.status} ${answer.code}: ${answer.message}` + (reqId ? ` [${reqId}]` : "");
|
|
295
|
+
if (answer.expected) logger.debug(line);
|
|
258
296
|
else logger.warn(`⚠️ ${line}`);
|
|
259
297
|
}
|
|
260
|
-
const
|
|
261
|
-
const apiErrorDrift = driftFor(apiErrorStatus);
|
|
298
|
+
const apiErrorDrift = driftFor(answer.status);
|
|
262
299
|
return c.json({ error: {
|
|
263
|
-
message:
|
|
264
|
-
code:
|
|
265
|
-
...
|
|
300
|
+
message: answer.message,
|
|
301
|
+
code: answer.code,
|
|
302
|
+
...answer.details !== void 0 && { details: answer.details },
|
|
266
303
|
...reqId && { requestId: reqId },
|
|
267
304
|
...apiErrorDrift && { cause: apiErrorDrift }
|
|
268
|
-
} },
|
|
305
|
+
} }, answer.status);
|
|
269
306
|
}
|
|
270
307
|
let statusCode = error.statusCode || codeToStatus(error.code) || 500;
|
|
271
308
|
let code = error.code || "INTERNAL_ERROR";
|
|
@@ -385,6 +422,6 @@ function codeToStatus(code) {
|
|
|
385
422
|
}[code];
|
|
386
423
|
}
|
|
387
424
|
//#endregion
|
|
388
|
-
export {
|
|
425
|
+
export { schemaDriftRemedy as i, declaredErrorAnswer as n, errorHandler as r, ApiError as t };
|
|
389
426
|
|
|
390
|
-
//# sourceMappingURL=errors-
|
|
427
|
+
//# sourceMappingURL=errors-DWsX4yTd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-DWsX4yTd.js","names":[],"sources":["../src/api/errors.ts"],"sourcesContent":["import type { Context, ErrorHandler } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\nimport { hostEnv } from \"../utils/host\";\n\n/**\n * A stale caller's schema stamp, as the cause of the error it explains.\n *\n * `createSchemaDriftDetector` puts the two stamps on the context when a request\n * carries an `x-rebase-schema` older than this backend's. It lives next door;\n * this half is here because `errors.ts` is in the graph of\n * `@rebasepro/server/functions` and may not reach `@rebasepro/types` at runtime.\n */\nfunction schemaDriftCause(drift: { client: string; server: string } | undefined): {\n code: string;\n message: string;\n clientSchema: string;\n serverSchema: string;\n} | undefined {\n if (!drift) return undefined;\n return {\n code: \"SCHEMA_DRIFT\",\n message:\n `This client was generated against schema ${drift.client}; this backend serves `\n + `${drift.server}. If the field named above was renamed or removed, regenerate the `\n + \"SDK (`rebase generate-sdk`) and rebuild.\",\n clientSchema: drift.client,\n serverSchema: drift.server\n };\n}\n\n/** Tracks whether we've already shown the doctor hint (once per process). */\nlet _schemaDriftHinted = false;\n\n/**\n * The schema-drift remedy, in the words that work on *this* database.\n *\n * The three copies of this message hard-coded `Run \\`pnpm db:push\\``, and on a\n * stock scaffold — where the managed PGlite database is the default — that\n * command answers `✗ rebase db push does not work on the managed development\n * database.` and exits 1. So the one instruction the server gave when a\n * developer's schema had drifted was a command their project refuses.\n *\n * Atlas plans a push by diffing against a second, empty database, and PGlite\n * serves exactly one — which is why it cannot run there, and why the remedy\n * has to know which database is under this run. There, boot applies additive\n * changes, so restarting `rebase dev` *is* the fix.\n *\n * `REBASE_DEV_DATABASE_KIND` is set by the CLI from the database it resolved,\n * the same variable `rebase schema generate`'s closing line already branches\n * on. Absent — a deployed backend, a container, anything not started by\n * `rebase dev` — the answer is the general one.\n */\nexport function schemaDriftRemedy(): { short: string; lines: string[] } {\n if (hostEnv().REBASE_DEV_DATABASE_KIND === \"managed\") {\n return {\n short: \"Restart `rebase dev` — boot applies additive schema changes to the managed database.\",\n lines: [\n \" Quick fixes (managed development database):\",\n \" restart `rebase dev` boot applies additive changes\",\n \" rebase doctor full 3-way drift report\",\n \"\",\n \" `rebase db push` does not run here: Atlas plans against a\",\n \" second, empty database and PGlite serves one. For a change\",\n \" boot leaves alone, use your own Postgres (DATABASE_URL)\",\n \" or `rebase dev --docker`.\"\n ]\n };\n }\n\n return {\n short: \"Run `rebase db push` to sync your schema, or `rebase db migrate` to apply pending migrations.\",\n lines: [\n \" Quick fixes (local dev, against DATABASE_URL):\",\n \" rebase db push sync schema to database (dev)\",\n \" rebase db migrate apply pending migrations (prod)\",\n \" rebase doctor full 3-way drift report\",\n \"\",\n \" Managed cloud: the runtime applies schema + RLS at boot\",\n \" (REBASE_MIGRATE_ON_BOOT); redeploy rather than db push,\",\n \" which cannot reach the tenant database.\"\n ]\n };\n}\n\n/** Shape of Postgres / network errors with diagnostic codes */\ninterface PgLikeError {\n code?: string;\n address?: string;\n port?: number;\n message?: string;\n table?: string;\n column?: string;\n schema?: string;\n detail?: string;\n hint?: string;\n constraint?: string;\n}\n\n/** 5-character SQLSTATE, e.g. `42501`, `23505`. */\nconst SQLSTATE_RE = /^[0-9A-Z]{5}$/;\n\n/**\n * What SQLSTATE 25006 means here, in the only terms that help the author fix it.\n *\n * Every request-scoped read runs `withTransaction(..., { accessMode: \"read\n * only\" })`, so a write attempted anywhere under it — including from a\n * `context.data` call inside an `afterRead` callback — is refused by Postgres\n * rather than by us. The callback name is in the message because that is the\n * file the reader has to open, and nothing else on a read path can raise this.\n */\nconst READ_ONLY_TRANSACTION_MESSAGE =\n \"An `afterRead` callback tried to write. Request-scoped reads run in a READ ONLY \" +\n \"transaction, so neither the callback nor anything it calls (context.data included) \" +\n \"may write. Move the write outside the read: enqueue a background job, or use \" +\n \"`rebase.dataAsAdmin` from a job or a custom function.\";\n\n/**\n * Walk the cause chain for the underlying database error, identified by a\n * 5-char SQLSTATE `code`. Drizzle wraps the pg error in `.cause`, and route\n * code sometimes wraps drizzle again, so the real error may sit several\n * levels down.\n */\nfunction extractDbError(error: unknown, depth = 0): PgLikeError | null {\n if (!error || typeof error !== \"object\" || depth > 8) return null;\n const e = error as PgLikeError & { cause?: unknown };\n if (typeof e.code === \"string\" && SQLSTATE_RE.test(e.code)) return e;\n if (e.cause && typeof e.cause === \"object\") return extractDbError(e.cause, depth + 1);\n return null;\n}\n\n/**\n * Extract the missing table or column name from a PG error.\n * PG 42P01 messages look like: 'relation \"my_table\" does not exist'\n * PG 42703 messages look like: 'column \"my_col\" does not exist' or 'column my_table.my_col does not exist'\n */\nfunction extractMissingIdentifier(pgMessage?: string): string | null {\n if (!pgMessage) return null;\n // Match quoted identifier: relation \"xxx\" / column \"xxx\"\n const quoted = pgMessage.match(/(?:relation|column|table)\\s+\"([^\"]+)\"/i);\n if (quoted) return quoted[1];\n // Match unquoted: column table.col does not exist\n const unquoted = pgMessage.match(/(?:relation|column|table)\\s+([\\w.]+)\\s+does not exist/i);\n if (unquoted) return unquoted[1];\n return null;\n}\n\n/**\n * The sentence a `INVALID_FILTER_VALUE` answer carries.\n *\n * Built from Postgres's own wording rather than passed through, because the\n * driver hands the whole failed statement over as `error.message` and this one\n * goes to the caller in production too. What is quoted back is the type name\n * and the literal the caller themselves sent — never a table, a column list or\n * a statement.\n *\n * The three shapes are the ones a filter actually produces: an unparseable\n * literal (22P02, `?id=eq.abc`), a value outside an enum's labels (22P02 with\n * different wording), and a number past the column type's range (22003). A\n * fourth SQLSTATE in class 22 lands on the general sentence, which still says\n * the useful thing: it is the value that is wrong, not the server.\n */\nfunction describeDataException(dbError?: PgLikeError): string {\n const message = dbError?.message ?? \"\";\n const column = dbError?.column ? ` for column \"${dbError.column}\"` : \"\";\n\n const syntax = message.match(/invalid input syntax for type ([\\w ]+): \"(.*)\"/);\n if (syntax) return `\"${syntax[2]}\" is not a valid ${syntax[1]}${column}.`;\n\n const enumValue = message.match(/invalid input value for enum ([\\w.\"]+): \"(.*)\"/);\n if (enumValue) return `\"${enumValue[2]}\" is not one of the values of ${enumValue[1]}${column}.`;\n\n const range = message.match(/value \"(.*)\" is out of range for type ([\\w ]+)/);\n if (range) return `\"${range[1]}\" is out of range for ${range[2]}${column}.`;\n\n return `A value in this request could not be read as the type of the column it was compared against${column}.`;\n}\n\n/**\n * Standardized API error class.\n * Throw this from any route handler — the errorHandler middleware\n * will format it into `{ error: { message, code, details? } }`.\n */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n public readonly details?: unknown;\n /**\n * Whether this outcome is a routine part of normal operation rather than\n * something an operator should look at. Expected errors log at debug; every\n * other operational error logs at warn.\n *\n * The motivating case is `POST /auth/refresh` with no session: clients\n * refresh on page load before they know whether one exists, so every\n * anonymous page view is a 401 — correct, and not worth a warning line.\n *\n * The other class is a caller-caused 4xx that never reached the database: a\n * mistyped filter operator, sort direction or limit, a request for a\n * collection that does not exist. Nothing on this server is wrong, and the\n * response body has already told the caller what to fix — while one client\n * holding a stale name would otherwise write a warning per request, forever,\n * until the level means nothing. See `api/rest/query-parser.ts`.\n *\n * What stays at warn is anything that says something about the *server*:\n * a schema that has drifted from the code, a permission the database\n * refused, a dependency that failed. Those are 4xx too, and they are still\n * incidents.\n */\n public readonly expected: boolean;\n\n constructor(statusCode: number, code: string, message: string, details?: unknown, expected = false) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n this.details = details;\n this.expected = expected;\n }\n\n // ── Factory methods ──────────────────────────────────────────────\n\n static badRequest(message: string, code = \"BAD_REQUEST\", details?: unknown): ApiError {\n return new ApiError(400, code, message, details);\n }\n\n static unauthorized(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message);\n }\n\n /**\n * A 401 that is a normal outcome, not an incident — logged at debug.\n * See {@link ApiError.expected}.\n */\n static unauthenticated(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message, undefined, true);\n }\n\n static forbidden(message: string, code = \"FORBIDDEN\"): ApiError {\n return new ApiError(403, code, message);\n }\n\n static notFound(message: string, code = \"NOT_FOUND\"): ApiError {\n return new ApiError(404, code, message);\n }\n\n /**\n * `details` because a 409 is usually a `23505`, and the one thing the\n * caller needs is *which field* collided. The column name goes in; the\n * value never does — see `pgFieldViolations`.\n */\n static conflict(message: string, code = \"CONFLICT\", details?: unknown): ApiError {\n return new ApiError(409, code, message, details);\n }\n\n static internal(message: string, code = \"INTERNAL_ERROR\"): ApiError {\n return new ApiError(500, code, message);\n }\n\n static serviceUnavailable(message: string, code = \"SERVICE_UNAVAILABLE\"): ApiError {\n return new ApiError(503, code, message);\n }\n}\n\n/**\n * Canonical error response shape:\n * `{ error: { message: string, code: string, details?: unknown } }`\n */\nexport interface ErrorResponse {\n error: {\n message: string;\n code: string;\n details?: unknown;\n /** Request correlation ID for tracing (echoes X-Request-ID). */\n requestId?: string;\n /**\n * Why this request was going to fail whatever it asked for.\n *\n * Only `SCHEMA_DRIFT` today: the caller's `x-rebase-schema` stamp is\n * older than this backend's, so a 400 naming an unknown field is very\n * likely a rename the client has not regenerated for. The error itself\n * is unchanged — this explains it, it does not cause it.\n */\n cause?: {\n code: string;\n message: string;\n clientSchema: string;\n serverSchema: string;\n };\n };\n}\n\n/**\n * General shape of errors that flow through the API error handler.\n * Extends Error with optional HTTP status, error code, and details.\n */\nexport interface RebaseApiError extends Error {\n statusCode?: number;\n code?: string;\n details?: unknown;\n}\n\n/**\n * The answer an error chose for itself, read the same way at every door.\n *\n * @see declaredErrorAnswer\n */\nexport interface DeclaredErrorAnswer {\n /** The HTTP status the error carries. A socket frame has no slot for it. */\n status: number;\n code: string;\n message: string;\n details?: unknown;\n /** See {@link ApiError.expected}: log it at debug, not warn. */\n expected: boolean;\n}\n\n/**\n * The status, code and message an error carries as its own answer — or\n * `undefined` for an error that carries none, which is a server fault and gets\n * masked.\n *\n * Two classes carry one. The server's `ApiError`, and `RebaseApiError` (or its\n * `RebaseClientError` subclass) from `@rebasepro/types` once it has a status.\n * The second is the browser-safe class: a `config/collections/*.ts` file is\n * bundled into the admin SPA and cannot import this package, so it is what a\n * collection callback throws, and what a callback refusal becomes —\n * `callbackRefusal` returns one, and `toCallbackError` wraps anything thrown\n * that does not already carry a status.\n *\n * One function because several doors turn an error into an answer: the REST\n * error handler, the two WebSocket servers, and the Postgres realtime\n * subscriptions. Each used to list the classes it recognised by hand. The\n * sockets listed only `ApiError`, so a `beforeDelete` veto that REST\n * answered as 400 `CALLBACK_REJECTED` with the author's message reached the\n * admin panel — which writes through the socket — as `INTERNAL_ERROR`, and in\n * production as \"An unexpected error occurred\".\n *\n * Matched by name as well as `instanceof`: a monorepo can resolve two copies of\n * a package, and `instanceof` is false across them. Name matching is also why\n * this file needs no runtime import of `@rebasepro/types`, which it may not\n * have — it is in the graph of `@rebasepro/server/functions`.\n */\nexport function declaredErrorAnswer(error: unknown): DeclaredErrorAnswer | undefined {\n if (error === null || typeof error !== \"object\") return undefined;\n const e = error as { name?: unknown; message?: unknown; code?: unknown; details?: unknown; statusCode?: unknown; status?: unknown };\n\n let status: number | undefined;\n if (error instanceof ApiError || e.name === \"ApiError\") {\n status = typeof e.statusCode === \"number\" ? e.statusCode : undefined;\n } else if (typeof e.name === \"string\" && /^Rebase(Api|Client)Error$/.test(e.name)) {\n // It spells its status `status`; `statusCode` wins when both are set.\n status = typeof e.statusCode === \"number\" ? e.statusCode\n : typeof e.status === \"number\" ? e.status\n : undefined;\n // Without a status it has not chosen an answer — `RebaseClientError`\n // is also raised for plain logic errors — so it is not one here.\n if (status === undefined) return undefined;\n } else {\n return undefined;\n }\n\n return {\n status: status || 500,\n code: typeof e.code === \"string\" && e.code ? e.code : \"INTERNAL_ERROR\",\n message: typeof e.message === \"string\" ? e.message : String(e.message ?? \"\"),\n ...(e.details !== undefined && { details: e.details }),\n expected: error instanceof ApiError && error.expected\n };\n}\n\n// `isRebaseApiError` was here. It read `return error instanceof Error`, so it\n// answered yes to every error while being named and used as though it\n// discriminated — the create and update handlers guarded a \"classify this as\n// BAD_REQUEST\" branch on it, and an unreachable database was therefore reported\n// to callers as a bad request. Deleted rather than repaired: the shape it\n// claimed to test is not decidable from an `Error`, and the layer that does\n// know — the driver, which holds the SQLSTATE — raises a real `ApiError`.\n\n/**\n * Leave the code and the message where the request log will find them.\n *\n * A failed request used to produce two lines, each holding half of it: this\n * handler had the code and the diagnosis, `requestLogger` had the user, the\n * collection, the status and the latency. Correlating them meant matching on\n * the request id — which only one of them printed reliably — and the pair cost\n * twice the volume for less than one line's worth of meaning.\n */\nfunction handOffToRequestLog(c: Context<HonoEnv>, code: string, message: string): void {\n if (typeof c.set !== \"function\") return;\n c.set(\"errorSummary\", { code, message });\n}\n\n/**\n * Is a request line coming for this request?\n *\n * `requestLogger` claims it before the handler runs, so by the time an error\n * reaches here the answer is already known. When nothing claimed it — a router\n * a project mounted onto its own Hono app, a test driving `app.fetch`\n * directly — this handler stays the only thing that would report the failure,\n * so it still writes its own line. Silence is the one outcome neither half may\n * produce.\n */\nfunction requestWillBeLogged(c: Context<HonoEnv>): boolean {\n return typeof c.get === \"function\" && c.get(\"requestLogged\") === true;\n}\n\n/**\n * Hono error-handling middleware (`app.onError`).\n * Converts any error into the canonical `{ error: { message, code } }` shape.\n */\nexport const errorHandler: ErrorHandler<HonoEnv> = (err, c) => {\n // Typecast custom error properties\n const error: RebaseApiError = err;\n const reqId = typeof c.get === \"function\" ? c.get(\"requestId\") : undefined;\n\n /* A stale SDK, named on the errors it explains.\n\n 400 and 404 only: those are what a renamed or removed field produces —\n an unknown filter field is a 400, a collection gone from under its slug\n is a 404 — and they are the two a caller can act on by regenerating.\n Attaching it to a 500 would be noise, since a server fault has nothing to\n do with how old the caller's schema is. */\n const driftFor = (status: number) =>\n (status === 400 || status === 404) && typeof c.get === \"function\"\n ? schemaDriftCause(c.get(\"schemaDrift\"))\n : undefined;\n\n // An error that chose its own answer — `ApiError`, or the browser-safe\n // `RebaseApiError` a collection callback throws. The same predicate the\n // WebSocket servers use; see `declaredErrorAnswer`.\n const answer = declaredErrorAnswer(error);\n if (answer) {\n // Operational errors — log at warn, unless the error declares itself a\n // routine outcome (see ApiError.expected), which would otherwise put a\n // warning in the log for every anonymous page view.\n handOffToRequestLog(c, answer.code, answer.message);\n if (!requestWillBeLogged(c)) {\n const line = `[API] ${c.req.method} ${c.req.path} → ${answer.status} ${answer.code}: ${answer.message}` +\n (reqId ? ` [${reqId}]` : \"\");\n if (answer.expected) {\n logger.debug(line);\n } else {\n logger.warn(`⚠️ ${line}`);\n }\n }\n const apiErrorDrift = driftFor(answer.status);\n return c.json({\n error: {\n message: answer.message,\n code: answer.code,\n ...(answer.details !== undefined && { details: answer.details }),\n ...(reqId && { requestId: reqId }),\n ...(apiErrorDrift && { cause: apiErrorDrift })\n }\n } satisfies ErrorResponse, answer.status as ContentfulStatusCode);\n }\n\n let statusCode = error.statusCode || codeToStatus(error.code) || 500;\n let code = error.code || \"INTERNAL_ERROR\";\n\n // Handle DB connection and specific system errors for better logging\n let logMessage = error.message;\n\n // Resolve the actual cause — Node's net module wraps dual-stack failures\n // in an AggregateError whose inner errors carry the real address/port.\n let resolvedCause: PgLikeError | undefined;\n if (error.cause && typeof error.cause === \"object\" && error.cause !== null && \"code\" in error.cause) {\n const cause = error.cause as PgLikeError & { errors?: PgLikeError[] };\n if (cause.code === \"ECONNREFUSED\" && !cause.address && Array.isArray(cause.errors)) {\n // AggregateError — pick the first inner error that has address info\n resolvedCause = cause.errors.find(e => e.address) || cause;\n } else {\n resolvedCause = cause;\n }\n }\n\n // The real database error may sit several levels down the cause chain.\n // Losing it turns a precise failure (e.g. an RLS denial) into an opaque\n // \"Failed query: …\" 500 that is undiagnosable without direct DB access.\n const dbError = extractDbError(error);\n\n if (resolvedCause && (resolvedCause.code === \"ENETUNREACH\" || resolvedCause.code === \"ECONNREFUSED\")) {\n const cause = resolvedCause;\n if (cause.code === \"ENETUNREACH\") {\n logMessage = `Network unreachable. Cannot connect to database at ${cause.address}:${cause.port}.`;\n } else {\n logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;\n }\n } else if (\"code\" in error && error.code === \"ENETUNREACH\") {\n const netErr = error as PgLikeError;\n logMessage = `Network unreachable. Cannot connect to service at ${netErr.address}:${netErr.port}.`;\n } else if (dbError && (dbError.code === \"42703\" || dbError.code === \"42P01\")) {\n code = \"SCHEMA_DRIFT\";\n const issue = dbError.code === \"42703\" ? \"column\" : \"table\";\n const identifier = dbError.table || dbError.column || extractMissingIdentifier(dbError.message) || \"unknown\";\n logMessage = `Schema drift: ${issue} \"${identifier}\" does not exist in the database. ${schemaDriftRemedy().short}`;\n } else if (dbError) {\n const parts = [`[PG ${dbError.code}] ${dbError.message}`];\n if (dbError.detail) parts.push(`Detail: ${dbError.detail}`);\n if (dbError.hint) parts.push(`Hint: ${dbError.hint}`);\n if (dbError.table) parts.push(`Table: ${dbError.table}`);\n if (dbError.column) parts.push(`Column: ${dbError.column}`);\n if (dbError.constraint) parts.push(`Constraint: ${dbError.constraint}`);\n if (dbError.code === \"42501\") {\n code = \"DB_PERMISSION_DENIED\";\n parts.push(\n \"The database rejected the statement for lack of privilege — usually a row-level \" +\n `security policy${dbError.table ? ` on \"${dbError.table}\"` : \"\"} denying this role, ` +\n \"or a stale FORCE ROW LEVEL SECURITY flag binding the owner connection.\"\n );\n }\n // 25006 read_only_sql_transaction. A request-scoped read opens its\n // transaction `READ ONLY`, so the only way to reach this is user code on\n // a read path attempting a write — which means an `afterRead` callback,\n // or something it called. Left in the generic branch it was a 500\n // \"Internal Server Error\", indistinguishable from the database being\n // down; it is the caller's own code, and it is not a server failure.\n if (dbError.code === \"25006\") {\n code = \"READ_ONLY_TRANSACTION\";\n statusCode = 409;\n parts.push(READ_ONLY_TRANSACTION_MESSAGE);\n }\n // SQLSTATE class 22 — data exception. The caller sent a value the\n // column's type cannot hold: `?id=eq.abc` on an integer key,\n // `?status=eq.nope` on an enum, a timestamp that is not one, a number\n // past the type's range. Every *other* bad query parameter already has\n // a precise 400 (`INVALID_LIMIT`, `UNKNOWN_FILTER_FIELD`,\n // `INVALID_LOGICAL_GROUP`); a bad *value* fell off the end of this\n // chain and answered 500 INTERNAL_ERROR — and in production `dbMessage`\n // is stripped, so the caller got a bare 500 naming nothing and had no\n // way to learn their own typo was the cause.\n if (dbError.code?.startsWith(\"22\")) {\n code = \"INVALID_FILTER_VALUE\";\n statusCode = 400;\n }\n logMessage = parts.join(\". \");\n }\n\n const isDbSchemaMismatch = code === \"SCHEMA_DRIFT\";\n\n // `logMessage`, not the sanitized client message: the request line is a\n // server log, and the whole point of this branch is the diagnosis it built.\n handOffToRequestLog(c, code, logMessage);\n\n if (isDbSchemaMismatch) {\n // Database schema mismatch is logged as a warning instead of a fatal error\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n // In dev mode, show a one-time hint to run `rebase doctor`\n if (!_schemaDriftHinted && hostEnv().NODE_ENV !== \"production\") {\n _schemaDriftHinted = true;\n // Drawn rather than hand-aligned: the remedy inside it now varies\n // with the database, and a box whose rows were padded by hand\n // stayed straight only for the text it was written around.\n const WIDTH = 62;\n const row = (text: string) => `│${text.padEnd(WIDTH).slice(0, WIDTH)}│`;\n logger.warn([\n \"\",\n `┌${\"─\".repeat(WIDTH)}┐`,\n // One space short, deliberately: the emoji occupies two columns\n // in a terminal and one in `String.length`.\n `│${\" 💡 TIP: Run `rebase doctor` for full schema diagnostics\".padEnd(WIDTH - 1)}│`,\n row(\"\"),\n ...schemaDriftRemedy().lines.map(row),\n `└${\"─\".repeat(WIDTH)}┘`,\n \"\"\n ].join(\"\\n\"));\n }\n } else if (code === \"READ_ONLY_TRANSACTION\" || code === \"INVALID_FILTER_VALUE\") {\n // A 4xx: the application's own callback, refused — or a filter value\n // the caller's own request could not have worked with. Not a server\n // fault, so not an ❌ in the log either — and, like the drift arm above,\n // not a second line when the request log is already going to carry it.\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n } else if (!requestWillBeLogged(c)) {\n // Unexpected errors — log at error level\n logger.error(\n `❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n }\n\n // Suppress the huge stack trace for known DB errors: it is noisy, and the\n // extracted [PG …] line above carries the signal. The SQL and the bound\n // params it used to leak are no longer this branch's problem — `logger`\n // strips Drizzle's `Failed query: … / params: …` wrapper out of every\n // message and stack it emits, so the fallbacks below (a connection dropped\n // mid-statement carries no SQLSTATE, so `dbError` is null and the stack is\n // logged) are covered too.\n const suppressStack = isDbSchemaMismatch || dbError !== null || (statusCode < 500 && code === \"BAD_REQUEST\");\n if (!suppressStack) {\n // The error goes in as a value, not as `String(error.stack)`. A string\n // is a leaf to the logger: `serialiseError` — the `.cause`/\n // `AggregateError` walker the boot path relies on — never runs on one,\n // so the request path used to print the outer wrapper's stack and drop\n // the sentence that says what actually failed (`connect ECONNRESET`,\n // sitting two `.cause` links down). Structured, it walks the chain and\n // redacts each link on the way.\n logger.error(\"unhandled request error\", { error });\n }\n\n // Sanitize the message for the client to prevent leaking sensitive details\n // like SQL queries or internal IP addresses.\n let clientMessage = \"An unexpected error occurred\";\n if (code === \"READ_ONLY_TRANSACTION\") {\n // Ahead of the generic 4xx arm below, which would echo the raw driver\n // message (\"Failed query: insert into …\") back to the caller.\n clientMessage = READ_ONLY_TRANSACTION_MESSAGE;\n } else if (code === \"INVALID_FILTER_VALUE\") {\n // Also ahead of the 4xx arm: `error.message` here is the driver's\n // \"Failed query: select … / params: …\", which is both unhelpful and the\n // one thing this envelope must never carry.\n clientMessage = describeDataException(dbError || (error as PgLikeError));\n } else if (statusCode < 500 && error.message) {\n // If it's a 4xx error (e.g. from validation), it's generally safe to send the message\n clientMessage = error.message;\n } else if (error instanceof ApiError || error.name === \"ApiError\") {\n // We already handled ApiError above, but just in case\n clientMessage = error.message;\n } else if (code === \"SCHEMA_DRIFT\") {\n const pgErr = dbError || (error as PgLikeError);\n const issue = pgErr.code === \"42703\" ? \"column\" : \"table\";\n const identifier = pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || \"unknown\";\n clientMessage = `Schema drift: ${issue} \"${identifier}\" does not exist. ${schemaDriftRemedy().short}`;\n } else if (code === \"DB_PERMISSION_DENIED\") {\n clientMessage = `Permission denied by the database${dbError?.table ? ` on \"${dbError.table}\"` : \"\"} (row-level security). Check the RLS policies for this table.`;\n } else if (code === \"INTERNAL_ERROR\") {\n clientMessage = \"Internal Server Error\";\n }\n\n // Database diagnostics for the envelope: the SQLSTATE is always safe to\n // return; message/detail/hint can reference schema internals, so only\n // outside production.\n const dbDetails = dbError ? {\n dbCode: dbError.code,\n ...(hostEnv().NODE_ENV !== \"production\" && {\n dbMessage: dbError.message,\n ...(dbError.detail && { detail: dbError.detail }),\n ...(dbError.hint && { hint: dbError.hint })\n })\n } : undefined;\n\n const drift = driftFor(statusCode);\n\n return c.json({\n error: {\n message: clientMessage,\n code,\n ...(error.details !== undefined\n ? { details: error.details }\n : dbDetails !== undefined ? { details: dbDetails } : {}),\n ...(reqId && { requestId: reqId }),\n ...(drift && { cause: drift })\n }\n } satisfies ErrorResponse, statusCode as ContentfulStatusCode);\n};\n\n/**\n * Map known error codes to HTTP status codes.\n */\nfunction codeToStatus(code?: string): number | undefined {\n if (!code) return undefined;\n const map: Record<string, number> = {\n BAD_REQUEST: 400,\n INVALID_INPUT: 400,\n WEAK_PASSWORD: 400,\n UNAUTHORIZED: 401,\n INVALID_CREDENTIALS: 401,\n INVALID_TOKEN: 401,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n CONFLICT: 409,\n EMAIL_EXISTS: 409,\n ROLE_EXISTS: 409,\n READ_ONLY_TRANSACTION: 409,\n INVALID_FILTER_VALUE: 400,\n SCHEMA_DRIFT: 500,\n DB_PERMISSION_DENIED: 500,\n INTERNAL_ERROR: 500,\n NOT_CONFIGURED: 503,\n SERVICE_UNAVAILABLE: 503\n };\n return map[code];\n}\n\n\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAS,iBAAiB,OAKZ;CACV,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,OAAO;EACH,MAAM;EACN,SACI,4CAA4C,MAAM,OAAO,wBACpD,MAAM,OAAO;EAEtB,cAAc,MAAM;EACpB,cAAc,MAAM;CACxB;AACJ;;AAGA,IAAI,qBAAqB;;;;;;;;;;;;;;;;;;;;AAqBzB,SAAgB,oBAAwD;CACpE,IAAI,QAAQ,CAAC,CAAC,6BAA6B,WACvC,OAAO;EACH,OAAO;EACP,OAAO;GACH;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACJ;CACJ;CAGJ,OAAO;EACH,OAAO;EACP,OAAO;GACH;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACJ;CACJ;AACJ;;AAiBA,IAAM,cAAc;;;;;;;;;;AAWpB,IAAM,gCACF;;;;;;;AAWJ,SAAS,eAAe,OAAgB,QAAQ,GAAuB;CACnE,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,QAAQ,GAAG,OAAO;CAC7D,MAAM,IAAI;CACV,IAAI,OAAO,EAAE,SAAS,YAAY,YAAY,KAAK,EAAE,IAAI,GAAG,OAAO;CACnE,IAAI,EAAE,SAAS,OAAO,EAAE,UAAU,UAAU,OAAO,eAAe,EAAE,OAAO,QAAQ,CAAC;CACpF,OAAO;AACX;;;;;;AAOA,SAAS,yBAAyB,WAAmC;CACjE,IAAI,CAAC,WAAW,OAAO;CAEvB,MAAM,SAAS,UAAU,MAAM,wCAAwC;CACvE,IAAI,QAAQ,OAAO,OAAO;CAE1B,MAAM,WAAW,UAAU,MAAM,wDAAwD;CACzF,IAAI,UAAU,OAAO,SAAS;CAC9B,OAAO;AACX;;;;;;;;;;;;;;;;AAiBA,SAAS,sBAAsB,SAA+B;CAC1D,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,SAAS,SAAS,SAAS,gBAAgB,QAAQ,OAAO,KAAK;CAErE,MAAM,SAAS,QAAQ,MAAM,gDAAgD;CAC7E,IAAI,QAAQ,OAAO,IAAI,OAAO,GAAG,mBAAmB,OAAO,KAAK,OAAO;CAEvE,MAAM,YAAY,QAAQ,MAAM,gDAAgD;CAChF,IAAI,WAAW,OAAO,IAAI,UAAU,GAAG,gCAAgC,UAAU,KAAK,OAAO;CAE7F,MAAM,QAAQ,QAAQ,MAAM,gDAAgD;CAC5E,IAAI,OAAO,OAAO,IAAI,MAAM,GAAG,wBAAwB,MAAM,KAAK,OAAO;CAEzE,OAAO,8FAA8F,OAAO;AAChH;;;;;;AAOA,IAAa,WAAb,MAAa,iBAAiB,MAAM;CAChC;CACA;CACA;;;;;;;;;;;;;;;;;;;;;;CAsBA;CAEA,YAAY,YAAoB,MAAc,SAAiB,SAAmB,WAAW,OAAO;EAChG,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,aAAa;EAClB,KAAK,OAAO;EACZ,KAAK,UAAU;EACf,KAAK,WAAW;CACpB;CAIA,OAAO,WAAW,SAAiB,OAAO,eAAe,SAA6B;EAClF,OAAO,IAAI,SAAS,KAAK,MAAM,SAAS,OAAO;CACnD;CAEA,OAAO,aAAa,SAAiB,OAAO,gBAA0B;EAClE,OAAO,IAAI,SAAS,KAAK,MAAM,OAAO;CAC1C;;;;;CAMA,OAAO,gBAAgB,SAAiB,OAAO,gBAA0B;EACrE,OAAO,IAAI,SAAS,KAAK,MAAM,SAAS,KAAA,GAAW,IAAI;CAC3D;CAEA,OAAO,UAAU,SAAiB,OAAO,aAAuB;EAC5D,OAAO,IAAI,SAAS,KAAK,MAAM,OAAO;CAC1C;CAEA,OAAO,SAAS,SAAiB,OAAO,aAAuB;EAC3D,OAAO,IAAI,SAAS,KAAK,MAAM,OAAO;CAC1C;;;;;;CAOA,OAAO,SAAS,SAAiB,OAAO,YAAY,SAA6B;EAC7E,OAAO,IAAI,SAAS,KAAK,MAAM,SAAS,OAAO;CACnD;CAEA,OAAO,SAAS,SAAiB,OAAO,kBAA4B;EAChE,OAAO,IAAI,SAAS,KAAK,MAAM,OAAO;CAC1C;CAEA,OAAO,mBAAmB,SAAiB,OAAO,uBAAiC;EAC/E,OAAO,IAAI,SAAS,KAAK,MAAM,OAAO;CAC1C;AACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiFA,SAAgB,oBAAoB,OAAiD;CACjF,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO,KAAA;CACxD,MAAM,IAAI;CAEV,IAAI;CACJ,IAAI,iBAAiB,YAAY,EAAE,SAAS,YACxC,SAAS,OAAO,EAAE,eAAe,WAAW,EAAE,aAAa,KAAA;MACxD,IAAI,OAAO,EAAE,SAAS,YAAY,4BAA4B,KAAK,EAAE,IAAI,GAAG;EAE/E,SAAS,OAAO,EAAE,eAAe,WAAW,EAAE,aACxC,OAAO,EAAE,WAAW,WAAW,EAAE,SAC7B,KAAA;EAGV,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;CACrC,OACI;CAGJ,OAAO;EACH,QAAQ,UAAU;EAClB,MAAM,OAAO,EAAE,SAAS,YAAY,EAAE,OAAO,EAAE,OAAO;EACtD,SAAS,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU,OAAO,EAAE,WAAW,EAAE;EAC3E,GAAI,EAAE,YAAY,KAAA,KAAa,EAAE,SAAS,EAAE,QAAQ;EACpD,UAAU,iBAAiB,YAAY,MAAM;CACjD;AACJ;;;;;;;;;;AAmBA,SAAS,oBAAoB,GAAqB,MAAc,SAAuB;CACnF,IAAI,OAAO,EAAE,QAAQ,YAAY;CACjC,EAAE,IAAI,gBAAgB;EAAE;EAAM;CAAQ,CAAC;AAC3C;;;;;;;;;;;AAYA,SAAS,oBAAoB,GAA8B;CACvD,OAAO,OAAO,EAAE,QAAQ,cAAc,EAAE,IAAI,eAAe,MAAM;AACrE;;;;;AAMA,IAAa,gBAAuC,KAAK,MAAM;CAE3D,MAAM,QAAwB;CAC9B,MAAM,QAAQ,OAAO,EAAE,QAAQ,aAAa,EAAE,IAAI,WAAW,IAAI,KAAA;CASjE,MAAM,YAAY,YACb,WAAW,OAAO,WAAW,QAAQ,OAAO,EAAE,QAAQ,aACjD,iBAAiB,EAAE,IAAI,aAAa,CAAC,IACrC,KAAA;CAKV,MAAM,SAAS,oBAAoB,KAAK;CACxC,IAAI,QAAQ;EAIR,oBAAoB,GAAG,OAAO,MAAM,OAAO,OAAO;EAClD,IAAI,CAAC,oBAAoB,CAAC,GAAG;GACzB,MAAM,OAAO,SAAS,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,KAAK,OAAO,OAAO,GAAG,OAAO,KAAK,IAAI,OAAO,aACzF,QAAQ,KAAK,MAAM,KAAK;GAC7B,IAAI,OAAO,UACP,OAAO,MAAM,IAAI;QAEjB,OAAO,KAAK,MAAM,MAAM;EAEhC;EACA,MAAM,gBAAgB,SAAS,OAAO,MAAM;EAC5C,OAAO,EAAE,KAAK,EACV,OAAO;GACH,SAAS,OAAO;GAChB,MAAM,OAAO;GACb,GAAI,OAAO,YAAY,KAAA,KAAa,EAAE,SAAS,OAAO,QAAQ;GAC9D,GAAI,SAAS,EAAE,WAAW,MAAM;GAChC,GAAI,iBAAiB,EAAE,OAAO,cAAc;EAChD,EACJ,GAA2B,OAAO,MAA8B;CACpE;CAEA,IAAI,aAAa,MAAM,cAAc,aAAa,MAAM,IAAI,KAAK;CACjE,IAAI,OAAO,MAAM,QAAQ;CAGzB,IAAI,aAAa,MAAM;CAIvB,IAAI;CACJ,IAAI,MAAM,SAAS,OAAO,MAAM,UAAU,YAAY,MAAM,UAAU,QAAQ,UAAU,MAAM,OAAO;EACjG,MAAM,QAAQ,MAAM;EACpB,IAAI,MAAM,SAAS,kBAAkB,CAAC,MAAM,WAAW,MAAM,QAAQ,MAAM,MAAM,GAE7E,gBAAgB,MAAM,OAAO,MAAK,MAAK,EAAE,OAAO,KAAK;OAErD,gBAAgB;CAExB;CAKA,MAAM,UAAU,eAAe,KAAK;CAEpC,IAAI,kBAAkB,cAAc,SAAS,iBAAiB,cAAc,SAAS,iBAAiB;EAClG,MAAM,QAAQ;EACd,IAAI,MAAM,SAAS,eACf,aAAa,sDAAsD,MAAM,QAAQ,GAAG,MAAM,KAAK;OAE/F,aAAa,qCAAqC,MAAM,QAAQ,GAAG,MAAM,KAAK;CAEtF,OAAO,IAAI,UAAU,SAAS,MAAM,SAAS,eAAe;EACvD,MAAM,SAAS;EACf,aAAa,qDAAqD,OAAO,QAAQ,GAAG,OAAO,KAAK;CACrG,OAAO,IAAI,YAAY,QAAQ,SAAS,WAAW,QAAQ,SAAS,UAAU;EAC1E,OAAO;EAGP,aAAa,iBAFC,QAAQ,SAAS,UAAU,WAAW,QAEhB,IADjB,QAAQ,SAAS,QAAQ,UAAU,yBAAyB,QAAQ,OAAO,KAAK,UAChD,oCAAoC,kBAAkB,CAAC,CAAC;CAC/G,OAAO,IAAI,SAAS;EAChB,MAAM,QAAQ,CAAC,OAAO,QAAQ,KAAK,IAAI,QAAQ,SAAS;EACxD,IAAI,QAAQ,QAAQ,MAAM,KAAK,WAAW,QAAQ,QAAQ;EAC1D,IAAI,QAAQ,MAAM,MAAM,KAAK,SAAS,QAAQ,MAAM;EACpD,IAAI,QAAQ,OAAO,MAAM,KAAK,UAAU,QAAQ,OAAO;EACvD,IAAI,QAAQ,QAAQ,MAAM,KAAK,WAAW,QAAQ,QAAQ;EAC1D,IAAI,QAAQ,YAAY,MAAM,KAAK,eAAe,QAAQ,YAAY;EACtE,IAAI,QAAQ,SAAS,SAAS;GAC1B,OAAO;GACP,MAAM,KACF,kGACkB,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,KAAK,GAAG,2FAEpE;EACJ;EAOA,IAAI,QAAQ,SAAS,SAAS;GAC1B,OAAO;GACP,aAAa;GACb,MAAM,KAAK,6BAA6B;EAC5C;EAUA,IAAI,QAAQ,MAAM,WAAW,IAAI,GAAG;GAChC,OAAO;GACP,aAAa;EACjB;EACA,aAAa,MAAM,KAAK,IAAI;CAChC;CAEA,MAAM,qBAAqB,SAAS;CAIpC,oBAAoB,GAAG,MAAM,UAAU;CAEvC,IAAI,oBAAoB;EAEpB,IAAI,CAAC,oBAAoB,CAAC,GAAG,OAAO,KAChC,YAAY,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,KAAK,WAAW,GAAG,KAAK,IAAI,gBAClE,QAAQ,KAAK,MAAM,KAAK,GAC7B;EAEA,IAAI,CAAC,sBAAsB,QAAQ,CAAC,CAAC,aAAa,cAAc;GAC5D,qBAAqB;GAIrB,MAAM,QAAQ;GACd,MAAM,OAAO,SAAiB,IAAI,KAAK,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,KAAK,EAAE;GACrE,OAAO,KAAK;IACR;IACA,IAAI,IAAI,OAAO,KAAK,EAAE;IAGtB,IAAI,4DAA4D,OAAO,QAAQ,CAAC,EAAE;IAClF,IAAI,EAAE;IACN,GAAG,kBAAkB,CAAC,CAAC,MAAM,IAAI,GAAG;IACpC,IAAI,IAAI,OAAO,KAAK,EAAE;IACtB;GACJ,CAAC,CAAC,KAAK,IAAI,CAAC;EAChB;CACJ,OAAO,IAAI,SAAS,2BAA2B,SAAS;MAKhD,CAAC,oBAAoB,CAAC,GAAG,OAAO,KAChC,YAAY,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,KAAK,WAAW,GAAG,KAAK,IAAI,gBAClE,QAAQ,KAAK,MAAM,KAAK,GAC7B;CAAA,OACG,IAAI,CAAC,oBAAoB,CAAC,GAE7B,OAAO,MACH,WAAW,EAAE,IAAI,OAAO,GAAG,EAAE,IAAI,KAAK,KAAK,WAAW,GAAG,KAAK,IAAI,gBACjE,QAAQ,KAAK,MAAM,KAAK,GAC7B;CAWJ,IAAI,EADkB,sBAAsB,YAAY,QAAS,aAAa,OAAO,SAAS,gBAS1F,OAAO,MAAM,2BAA2B,EAAE,MAAM,CAAC;CAKrD,IAAI,gBAAgB;CACpB,IAAI,SAAS,yBAGT,gBAAgB;MACb,IAAI,SAAS,wBAIhB,gBAAgB,sBAAsB,WAAY,KAAqB;MACpE,IAAI,aAAa,OAAO,MAAM,SAEjC,gBAAgB,MAAM;MACnB,IAAI,iBAAiB,YAAY,MAAM,SAAS,YAEnD,gBAAgB,MAAM;MACnB,IAAI,SAAS,gBAAgB;EAChC,MAAM,QAAQ,WAAY;EAG1B,gBAAgB,iBAFF,MAAM,SAAS,UAAU,WAAW,QAEX,IADpB,MAAM,SAAS,MAAM,UAAU,yBAAyB,MAAM,WAAW,MAAM,OAAO,KAAK,UACxD,oBAAoB,kBAAkB,CAAC,CAAC;CAClG,OAAO,IAAI,SAAS,wBAChB,gBAAgB,oCAAoC,SAAS,QAAQ,QAAQ,QAAQ,MAAM,KAAK,GAAG;MAChG,IAAI,SAAS,kBAChB,gBAAgB;CAMpB,MAAM,YAAY,UAAU;EACxB,QAAQ,QAAQ;EAChB,GAAI,QAAQ,CAAC,CAAC,aAAa,gBAAgB;GACvC,WAAW,QAAQ;GACnB,GAAI,QAAQ,UAAU,EAAE,QAAQ,QAAQ,OAAO;GAC/C,GAAI,QAAQ,QAAQ,EAAE,MAAM,QAAQ,KAAK;EAC7C;CACJ,IAAI,KAAA;CAEJ,MAAM,QAAQ,SAAS,UAAU;CAEjC,OAAO,EAAE,KAAK,EACV,OAAO;EACH,SAAS;EACT;EACA,GAAI,MAAM,YAAY,KAAA,IAChB,EAAE,SAAS,MAAM,QAAQ,IACzB,cAAc,KAAA,IAAY,EAAE,SAAS,UAAU,IAAI,CAAC;EAC1D,GAAI,SAAS,EAAE,WAAW,MAAM;EAChC,GAAI,SAAS,EAAE,OAAO,MAAM;CAChC,EACJ,GAA2B,UAAkC;AACjE;;;;AAKA,SAAS,aAAa,MAAmC;CACrD,IAAI,CAAC,MAAM,OAAO,KAAA;CAqBlB,OAAO;EAnBH,aAAa;EACb,eAAe;EACf,eAAe;EACf,cAAc;EACd,qBAAqB;EACrB,eAAe;EACf,WAAW;EACX,WAAW;EACX,UAAU;EACV,cAAc;EACd,aAAa;EACb,uBAAuB;EACvB,sBAAsB;EACtB,cAAc;EACd,sBAAsB;EACtB,gBAAgB;EAChB,gBAAgB;EAChB,qBAAqB;CAElB,EAAI;AACf"}
|
|
@@ -3,7 +3,7 @@ import __rebaseProcess from "process";
|
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
5
|
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
-
import { t as ApiError } from "./errors-
|
|
6
|
+
import { t as ApiError } from "./errors-DWsX4yTd.js";
|
|
7
7
|
import { n as hasAdministrativeRole } from "./admin-roles-vYdp_Pil.js";
|
|
8
8
|
import { Hono } from "hono";
|
|
9
9
|
//#region src/functions/context.ts
|
|
@@ -264,4 +264,4 @@ function createFunctionRoutes(functions, problems = [], mountPath = "/functions"
|
|
|
264
264
|
//#endregion
|
|
265
265
|
export { getDriver as a, getUser as c, identityResolved as d, isAdmin as f, getApiKey as i, getUserId as l, requireDriver as m, function_routes_exports as n, getRequestId as o, isAuthenticated as p, requireRole as r, getRoles as s, createFunctionRoutes as t, hasRole as u };
|
|
266
266
|
|
|
267
|
-
//# sourceMappingURL=function-routes-
|
|
267
|
+
//# sourceMappingURL=function-routes-Chet4-lB.js.map
|