@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g249daa1

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.
Files changed (70) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/{src-DlPBctw_.js → auth-users-columns-Dt9g712t.js} +721 -47
  3. package/dist/auth-users-columns-Dt9g712t.js.map +1 -0
  4. package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
  5. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
  6. package/dist/cli-helpers.d.ts +57 -1
  7. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-CwYUliAa.js} +2 -2
  8. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-CwYUliAa.js.map} +1 -1
  9. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DRxaUG96.js} +86 -15
  10. package/dist/ensure-collection-tables-DRxaUG96.js.map +1 -0
  11. package/dist/index.es.js +765 -223
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/{policy-CeA1JcxP.js → policy-CPkCqVTz.js} +4 -4
  14. package/dist/policy-CPkCqVTz.js.map +1 -0
  15. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  16. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  17. package/dist/schema/auth-users-columns.d.ts +97 -0
  18. package/dist/schema/ensure-collection-tables.d.ts +2 -2
  19. package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
  20. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  21. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  22. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  23. package/dist/schema/search-column.d.ts +199 -0
  24. package/dist/security/rls-enforcement.d.ts +53 -2
  25. package/dist/services/FetchService.d.ts +25 -7
  26. package/dist/services/dataService.d.ts +3 -0
  27. package/dist/services/realtimeService.d.ts +27 -21
  28. package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
  29. package/dist/src-C_wvdMnl.js.map +1 -0
  30. package/dist/utils/drizzle-conditions.d.ts +71 -2
  31. package/dist/{websocket-B2LsrINK.js → websocket-D0TBU3ia.js} +3 -3
  32. package/dist/websocket-D0TBU3ia.js.map +1 -0
  33. package/package.json +9 -8
  34. package/src/PostgresBackendDriver.ts +165 -3
  35. package/src/PostgresBootstrapper.ts +41 -2
  36. package/src/auth/ensure-tables.ts +185 -86
  37. package/src/cli-helpers.ts +129 -10
  38. package/src/cli.ts +232 -30
  39. package/src/collections/validate-relations.ts +124 -17
  40. package/src/data-transformer.ts +31 -3
  41. package/src/history/ensure-history-table.ts +7 -0
  42. package/src/schema/auth-users-columns.ts +131 -0
  43. package/src/schema/doctor.ts +7 -5
  44. package/src/schema/ensure-collection-tables.ts +165 -20
  45. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  46. package/src/schema/generate-postgres-ddl-logic.ts +266 -15
  47. package/src/schema/generate-postgres-ddl.ts +25 -2
  48. package/src/schema/generated-schema-staleness.ts +169 -0
  49. package/src/schema/introspect-db-logic.ts +1 -1
  50. package/src/schema/non-sql-collections.test.ts +131 -0
  51. package/src/schema/rls-bootstrap-sql.ts +288 -0
  52. package/src/schema/search-column.ts +558 -0
  53. package/src/security/anonymous-grants.test.ts +4 -2
  54. package/src/security/rls-enforcement.ts +141 -3
  55. package/src/services/BranchService.ts +5 -0
  56. package/src/services/FetchService.ts +148 -108
  57. package/src/services/PersistService.ts +14 -1
  58. package/src/services/RelationService.ts +2 -1
  59. package/src/services/channel-history.ts +8 -0
  60. package/src/services/channel-presence.ts +6 -0
  61. package/src/services/dataService.ts +3 -0
  62. package/src/services/realtimeService.ts +46 -37
  63. package/src/utils/drizzle-conditions.ts +223 -2
  64. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  65. package/dist/policy-CeA1JcxP.js.map +0 -1
  66. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  67. package/dist/src-DlPBctw_.js.map +0 -1
  68. package/dist/src-DoU9yPqq.js.map +0 -1
  69. package/dist/websocket-B2LsrINK.js.map +0 -1
  70. package/src/schema/auth-bootstrap-sql.ts +0 -47
package/dist/index.es.js CHANGED
@@ -2,11 +2,12 @@ import { createRequire as __createRequire } from "module";
2
2
  import process from "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, o as pinSearchPath, r as createPostgresDatabaseConnection } from "./connection-BuZ97wsr.js";
5
- import { A as toSnakeCase, C as normalizeToEntityRelation, D as getPolicyNamesForRule, F as Vector, N as hasForeignKeyOnTarget, O as mergeDeep, P as isManyToMany, S as createRelationRefWithData, T as generateForeignKeyName, _ as buildCompositeId, a as getJunctionSecurityRules, b as parseIdValues, c as policyToPostgres, d as findRelation, f as getColumnName, g as resolveCollectionRelations, h as getTableVarName, i as getJunctionCollectionConfig, k as camelCase, l as securityRuleToConditions, m as getTableName$1, n as CollectionRegistry, o as resolveJunctionSpecs, p as getEnumVarName, r as resolveStringColumnLength, s as getEffectiveSecurityRules, t as buildSdkData, u as findAnonymousGrants, v as getDeclaredPrimaryKeys, w as updateDateAutoValues, x as createRelationRef, y as isAddressableId } from "./src-DlPBctw_.js";
6
- import { n as isPostgresCollectionConfig, r as isRelationalCollectionConfig } from "./src-DoU9yPqq.js";
7
- import { t as ANONYMOUS_USER_ID } from "./policy-CeA1JcxP.js";
8
- import { t as createPostgresWebSocket } from "./websocket-B2LsrINK.js";
9
- import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as listBackups, d as resolvePgBinary, f as restoreDump, g as selectBackupsToPrune, h as require_source, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as preflight, m as validateDump, n as applyGlobals, o as ensureDatabaseExists, p as uploadBackup, s as getServerVersionMajor, t as BackupToolError, u as pruneBackups, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-CD8o_1Sl.js";
5
+ import { A as resolveCollectionRelations, B as legacyForeignKeyName, C as securityRuleToConditions, D as getEnumVarName, E as getColumnName, F as createRelationRef, G as camelCase, H as getPolicyNamesForRule, I as createRelationRefWithData, K as toSnakeCase, L as normalizeToEntityRelation, M as getDeclaredPrimaryKeys, N as isAddressableId, O as getTableName$1, P as parseIdValues, Q as Vector, R as updateDateAutoValues, S as policyToPostgres, T as findRelation, U as isPrototypePollutingKey, W as mergeDeep, X as hasForeignKeyOnTarget, Y as resolveClientListLimit, Z as isManyToMany, _ as resolveStringColumnLength, b as resolveJunctionSpecs, c as buildSearchColumnSpec, g as relationalCollections, h as CollectionRegistry, j as buildCompositeId, k as getTableVarName, l as hiddenColumnsOption, m as buildSdkData, o as SEARCH_UNACCENT_FN, p as visibleColumnProjection, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS, v as getJunctionCollectionConfig, w as findAnonymousGrants, x as getEffectiveSecurityRules, y as getJunctionSecurityRules, z as generateForeignKeyName } from "./auth-users-columns-Dt9g712t.js";
6
+ import { c as isPostgresCollectionConfig, l as isRelationalCollectionConfig, n as REBASE_SCHEMA, o as usesLegacyRlsFunctions } from "./src-C_wvdMnl.js";
7
+ import { t as ANONYMOUS_USER_ID } from "./policy-CPkCqVTz.js";
8
+ import { t as createPostgresWebSocket } from "./websocket-D0TBU3ia.js";
9
+ import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as listBackups, d as resolvePgBinary, f as restoreDump, g as selectBackupsToPrune, h as require_source, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as preflight, m as validateDump, n as applyGlobals, o as ensureDatabaseExists, p as uploadBackup, s as getServerVersionMajor, t as BackupToolError, u as pruneBackups, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-Bww-Lg0s.js";
10
+ import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-Bpv3nUZo.js";
10
11
  import { Client, Pool } from "pg";
11
12
  import { drizzle } from "drizzle-orm/node-postgres";
12
13
  import { ApiError, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
@@ -30,25 +31,6 @@ import { randomUUID } from "crypto";
30
31
  function isChannelBusInstance(setting) {
31
32
  return typeof setting?.publish === "function";
32
33
  }
33
- /**
34
- * Resolve a client-supplied list `limit` into a safe, always-defined value.
35
- *
36
- * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
37
- * so `0`, negatives, and absurd values can never bypass the cap.
38
- * - An absent / blank / non-numeric limit falls back to the mode default:
39
- * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
40
- *
41
- * The return is never `undefined` — no ingress that routes its client limit
42
- * through this can produce an unbounded read.
43
- */
44
- function resolveClientListLimit(rawLimit, opts = {}) {
45
- const maxLimit = opts.maxLimit ?? 1e3;
46
- if (rawLimit != null && String(rawLimit).trim() !== "") {
47
- const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
48
- if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
49
- }
50
- return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
51
- }
52
34
  //#endregion
53
35
  //#region ../common/src/util/email.ts
54
36
  /**
@@ -152,6 +134,142 @@ var buildPropertyCallbacks = (properties) => {
152
134
  return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
153
135
  };
154
136
  //#endregion
137
+ //#region ../common/src/util/internal-tables.ts
138
+ /**
139
+ * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
140
+ * end-user role away from them.
141
+ *
142
+ * ## Why this exists
143
+ *
144
+ * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
145
+ * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
146
+ * in the schemas a project uses — including `rebase`, because a project's own
147
+ * collections are allowed to live there (the scaffold puts `users` there). It
148
+ * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
149
+ * migrating role inherits the same grant.
150
+ *
151
+ * Every framework-internal table is created later: auth's tables come up during
152
+ * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
153
+ * job registers, `idempotency_keys` on the first request that carries a key. So
154
+ * they all inherited full DML for the end-user role — and none of them enables
155
+ * row-level security, because none of them is a collection with
156
+ * `securityRules`. Measured on a freshly provisioned database, `SET ROLE
157
+ * rebase_user` could read `rebase.refresh_tokens` (session token hashes),
158
+ * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
159
+ * `rebase.api_keys` (including its `admin` flag), and insert into
160
+ * `rebase.app_config`.
161
+ *
162
+ * Nothing routes a user-context query at those tables today, so this was not
163
+ * reachable over the API. That is the wrong thing to depend on: the documented
164
+ * model is that RLS is the authorization boundary, and these tables sat outside
165
+ * it. The boundary is now a privilege boundary instead — the role simply cannot
166
+ * address them.
167
+ *
168
+ * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
169
+ *
170
+ * RLS with no policy denies every row, which is the same outcome, but it is the
171
+ * *weaker* statement: it leaves the grant in place, so a later policy — or a
172
+ * `FORCE` flag cleared by some future migration — reopens the table. There is no
173
+ * row of `refresh_tokens` any end user should ever reach, so the honest encoding
174
+ * is "this role has no privilege here at all". It also keeps the owner
175
+ * connection (which auth actually runs on) completely unaffected.
176
+ *
177
+ * ## Keeping it true
178
+ *
179
+ * `packages/rls-check` scans the `rebase` schema — it used to skip it as a
180
+ * "platform" schema — and its `rls-disabled` check fires on exactly the
181
+ * condition this module removes: RLS off *and* a DML grant to a reachable role.
182
+ * So a table added here without a revoke is caught by `pnpm rls:check`, not by
183
+ * someone re-reading this file.
184
+ */
185
+ /**
186
+ * The Postgres role authenticated requests run as.
187
+ *
188
+ * Defined here rather than in the Postgres driver because both the driver (which
189
+ * provisions the role) and this module (which revokes on its behalf) need it,
190
+ * and a second spelling of a role name is a silent no-op waiting to happen.
191
+ */
192
+ var REBASE_USER_ROLE = "rebase_user";
193
+ /**
194
+ * Framework-internal table names, unqualified.
195
+ *
196
+ * Deliberately NOT including `users`: the auth user table is also a collection,
197
+ * with `securityRules`, RLS enabled and policies applied. Users read their own
198
+ * row through it — revoking there would break sign-in.
199
+ *
200
+ * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
201
+ * because `db migrate apply` passes `--revisions-schema rebase`.
202
+ */
203
+ var REBASE_INTERNAL_TABLES = [
204
+ "user_identities",
205
+ "refresh_tokens",
206
+ "password_reset_tokens",
207
+ "magic_link_tokens",
208
+ "mfa_factors",
209
+ "mfa_challenges",
210
+ "recovery_codes",
211
+ "app_config",
212
+ "schema_meta",
213
+ "api_keys",
214
+ "cron_logs",
215
+ "cron_claims",
216
+ "idempotency_keys",
217
+ "entity_history",
218
+ "branches",
219
+ "channel_messages",
220
+ "channel_cursors",
221
+ "channel_presence",
222
+ "atlas_schema_revisions"
223
+ ];
224
+ /** Postgres identifiers this module is willing to interpolate. */
225
+ var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
226
+ /**
227
+ * A single statement that takes every privilege on `schema.table` away from the
228
+ * end-user role.
229
+ *
230
+ * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
231
+ * happen in practice:
232
+ *
233
+ * - the role does not exist when the connection is unprivileged (Rebase then
234
+ * relies on native RLS rather than a role switch), and a bare `REVOKE` on a
235
+ * missing role is an error, not a no-op;
236
+ * - the table may not exist yet — `cron_logs` never appears in a project with
237
+ * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
238
+ *
239
+ * One command, so it is safe on handles that speak the extended query protocol
240
+ * and reject multi-statement strings.
241
+ */
242
+ function revokeInternalTableSql(schema, table) {
243
+ if (!SAFE_IDENTIFIER.test(schema)) throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
244
+ if (!SAFE_IDENTIFIER.test(table)) throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
245
+ const qualified = `"${schema}"."${table}"`;
246
+ return `
247
+ DO $rebase_revoke$
248
+ BEGIN
249
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
250
+ AND to_regclass('${qualified}') IS NOT NULL THEN
251
+ EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
252
+ END IF;
253
+ END
254
+ $rebase_revoke$;
255
+ `.trim();
256
+ }
257
+ /**
258
+ * Revoke on every internal table in `schema`, one statement at a time.
259
+ *
260
+ * Best-effort per table: a connection that does not own one of them (a
261
+ * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and
262
+ * that must not take down a boot. The caller decides how loud to be — `onError`
263
+ * exists so the driver can warn without this module importing a logger.
264
+ */
265
+ async function revokeInternalTableAccess(execute, schema, options) {
266
+ for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) try {
267
+ await execute(revokeInternalTableSql(schema, table));
268
+ } catch (error) {
269
+ options?.onError?.(table, error);
270
+ }
271
+ }
272
+ //#endregion
155
273
  //#region ../common/src/table-classification.ts
156
274
  /** Schemas that are always considered Rebase-internal. */
157
275
  var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
@@ -436,6 +554,12 @@ function deriveRowAddress(row, collection, registry) {
436
554
  //#endregion
437
555
  //#region src/utils/drizzle-conditions.ts
438
556
  /**
557
+ * Postgres's own default for `pg_trgm.word_similarity_threshold`. Named here
558
+ * because the fuzzy predicate has to know when the index-backed operator agrees
559
+ * with the collection's declared threshold and when it would narrow too far.
560
+ */
561
+ var PG_TRGM_WORD_SIMILARITY_DEFAULT = .6;
562
+ /**
439
563
  * Process-wide default, set once when the driver is constructed.
440
564
  *
441
565
  * The condition builder is a set of *static* methods reached from a dozen
@@ -493,7 +617,7 @@ function toMembershipList(value) {
493
617
  * @example
494
618
  * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;
495
619
  */
496
- var DrizzleConditionBuilder = class {
620
+ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
497
621
  /**
498
622
  * Express "reachable from this parent through this relation" as a plain
499
623
  * `WHERE` condition on the target table.
@@ -1119,10 +1243,28 @@ var DrizzleConditionBuilder = class {
1119
1243
  return or(...conditions);
1120
1244
  }
1121
1245
  /**
1122
- * Build search conditions for text fields
1246
+ * Build search conditions for text fields.
1247
+ *
1248
+ * Two shapes, chosen by whether the collection declared a `search` block:
1249
+ *
1250
+ * - **Declared** — one `@@ websearch_to_tsquery` against the generated
1251
+ * `tsvector` column. Stems, drops stopwords, AND-es the terms, reaches
1252
+ * inside JSONB and arrays, and uses the GIN index.
1253
+ * - **Not declared** — the original `ILIKE '%term%'` OR-ed across top-level
1254
+ * string properties, unchanged.
1255
+ *
1256
+ * The second is the default and stays the default. A collection that has
1257
+ * not opted in compiles to exactly the SQL it compiled to before this
1258
+ * branch existed, which is the only reason it is safe to have added it.
1259
+ *
1260
+ * `collection` is optional so that the callers which genuinely have no
1261
+ * collection in hand — nested paths, derived views — keep working; without
1262
+ * one there is no `search` block to read and the ILIKE path is correct.
1123
1263
  */
1124
- static buildSearchConditions(searchString, properties, table) {
1264
+ static buildSearchConditions(searchString, properties, table, collection) {
1125
1265
  const searchConditions = [];
1266
+ const ftsCondition = collection ? DrizzleConditionBuilder.buildFullTextCondition(searchString, table, collection) : void 0;
1267
+ if (ftsCondition) return [ftsCondition];
1126
1268
  for (const [key, prop] of Object.entries(properties)) {
1127
1269
  const p = prop;
1128
1270
  if (p.type === "string" && !p.enum && p.isId !== "uuid") {
@@ -1135,6 +1277,117 @@ var DrizzleConditionBuilder = class {
1135
1277
  return searchConditions;
1136
1278
  }
1137
1279
  /**
1280
+ * The `@@` predicate for a collection that declared a `search` block, or
1281
+ * undefined for one that did not.
1282
+ *
1283
+ * The query is normalized exactly as the indexed content was — same text
1284
+ * search configuration, same accent folding. Skipping that on the query
1285
+ * side is the subtle way to get a search that matches nothing: the column
1286
+ * would hold `gestion` while the query asked for `gestión`.
1287
+ *
1288
+ * `websearch_to_tsquery` rather than `plainto_tsquery` because it is the
1289
+ * one that behaves the way a search box looks like it should — quoted
1290
+ * phrases, `or`, and a leading `-` to exclude — and because it never throws
1291
+ * on user input, which `to_tsquery` does on so much as a stray parenthesis.
1292
+ */
1293
+ static buildFullTextCondition(searchString, table, collection) {
1294
+ let spec;
1295
+ try {
1296
+ spec = buildSearchColumnSpec(collection);
1297
+ } catch {
1298
+ return;
1299
+ }
1300
+ if (!spec) return void 0;
1301
+ const column = table[spec.column];
1302
+ if (!column) return;
1303
+ const exact = sql`${column} @@ ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)}`;
1304
+ if (!spec.fuzzy) return exact;
1305
+ const fuzzyColumn = table[spec.fuzzy.column];
1306
+ if (!fuzzyColumn) return exact;
1307
+ const needle = spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`;
1308
+ const similar = sql`public.word_similarity(${needle}, ${fuzzyColumn}) >= ${spec.fuzzy.threshold}`;
1309
+ return sql`(${exact} OR ${spec.fuzzy.threshold > PG_TRGM_WORD_SIMILARITY_DEFAULT ? sql`(${needle} OPERATOR(public.<%) ${fuzzyColumn} AND ${similar})` : similar})`;
1310
+ }
1311
+ /**
1312
+ * `websearch_to_tsquery(<config>, <normalized search string>)`.
1313
+ *
1314
+ * Split out because the ranking expression needs the identical query — a
1315
+ * row ranked against a different tsquery than it was matched against is a
1316
+ * ranking of something else.
1317
+ */
1318
+ static normalizedTsQuery(searchString, spec) {
1319
+ const normalized = spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`;
1320
+ return sql`websearch_to_tsquery(${spec.language}, ${normalized})`;
1321
+ }
1322
+ /**
1323
+ * A JSONB array of `{ field, snippet }` naming which declared fields matched
1324
+ * and showing the text around each hit — what backs `_matches`.
1325
+ *
1326
+ * A ranked list answers "which rows", never "why this row". For a talent
1327
+ * pool that difference is the product: a candidate surfacing for
1328
+ * "iso 14001" on a *certification* is a different candidate from one whose
1329
+ * bio happens to mention the standard, and the score cannot tell them apart.
1330
+ *
1331
+ * Built as a correlated subquery over a `VALUES` list of the declared
1332
+ * fields, rather than one `CASE` per field, so the shape does not change
1333
+ * with the number of fields and the empty result is a plain `[]`.
1334
+ *
1335
+ * `ts_headline` runs over the same normalized text that was indexed. Over
1336
+ * the *original* text it would find nothing to mark whenever `unaccent` is
1337
+ * on — the query's lexemes are folded and the document's are not — and
1338
+ * would return the text silently unhighlighted. Folded-but-marked beats
1339
+ * pretty-but-inert.
1340
+ *
1341
+ * Undefined when the collection has not opted in, when the column is not on
1342
+ * the table yet, or when the caller did not ask: this costs a `ts_headline`
1343
+ * per field per row and `ts_headline` re-parses the document.
1344
+ */
1345
+ static buildSearchMatchesExpression(searchString, table, collection) {
1346
+ let spec;
1347
+ try {
1348
+ spec = buildSearchColumnSpec(collection);
1349
+ } catch {
1350
+ return;
1351
+ }
1352
+ if (!spec || spec.fields.length === 0) return void 0;
1353
+ if (!table[spec.column]) return void 0;
1354
+ const query = DrizzleConditionBuilder.normalizedTsQuery(searchString, spec);
1355
+ const config = sql`${spec.language}`;
1356
+ const rows = spec.fields.map((f, i) => sql`(${i}, ${f.path}, ${sql.raw(f.textSql)})`);
1357
+ return sql`(
1358
+ SELECT coalesce(jsonb_agg(s.m ORDER BY f.ord), '[]'::jsonb)
1359
+ FROM (VALUES ${sql.join(rows, sql`, `)}) AS f(ord, path, txt)
1360
+ CROSS JOIN LATERAL (
1361
+ SELECT jsonb_build_object(
1362
+ 'field', f.path,
1363
+ 'snippet', ts_headline(${config}::regconfig, f.txt, ${query},
1364
+ 'StartSel=<mark>,StopSel=</mark>,MaxWords=14,MinWords=1,MaxFragments=1,FragmentDelimiter= … ')
1365
+ ) AS m
1366
+ WHERE to_tsvector(${config}::regconfig, f.txt) @@ ${query}
1367
+ ) s
1368
+ )`;
1369
+ }
1370
+ /**
1371
+ * `ts_rank(<column>, <query>)` for the collection, or undefined when it has
1372
+ * not opted in. This is what backs `orderBy: ["_score", "desc"]`.
1373
+ */
1374
+ static buildSearchRankExpression(searchString, table, collection) {
1375
+ let spec;
1376
+ try {
1377
+ spec = buildSearchColumnSpec(collection);
1378
+ } catch {
1379
+ return;
1380
+ }
1381
+ if (!spec) return void 0;
1382
+ const column = table[spec.column];
1383
+ if (!column) return void 0;
1384
+ const rank = sql`ts_rank(${column}, ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)})`;
1385
+ if (!spec.fuzzy) return rank;
1386
+ const fuzzyColumn = table[spec.fuzzy.column];
1387
+ if (!fuzzyColumn) return rank;
1388
+ return sql`(${rank} + public.word_similarity(${spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`}, ${fuzzyColumn}))`;
1389
+ }
1390
+ /**
1138
1391
  * Build a unique field check condition
1139
1392
  */
1140
1393
  static buildUniqueFieldCondition(fieldColumn, value, idColumn, excludeId) {
@@ -1276,7 +1529,11 @@ function sanitizeAndConvertDates(obj) {
1276
1529
  if (obj instanceof Date) return obj.toISOString();
1277
1530
  if (typeof obj === "object") {
1278
1531
  const newObj = {};
1279
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = sanitizeAndConvertDates(obj[key]);
1532
+ for (const key in obj) {
1533
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
1534
+ if (isPrototypePollutingKey(key)) continue;
1535
+ newObj[key] = sanitizeAndConvertDates(obj[key]);
1536
+ }
1280
1537
  return newObj;
1281
1538
  }
1282
1539
  if (typeof obj === "string") {
@@ -1305,6 +1562,7 @@ function serializeDataToServer(row, properties, collection, registry) {
1305
1562
  if (relation.kind === "belongsTo") foreignKeys.add(relation.localKey);
1306
1563
  });
1307
1564
  for (const [key, value] of Object.entries(row)) {
1565
+ if (isPrototypePollutingKey(key)) continue;
1308
1566
  const property = properties[key];
1309
1567
  const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
1310
1568
  if (!property) {
@@ -1670,6 +1928,18 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
1670
1928
  * from the result (used by `normalizeDbValues` where
1671
1929
  * Drizzle's relational API already hydrates them).
1672
1930
  */
1931
+ /**
1932
+ * Keys a query computes and attaches to a row, rather than reads from a column.
1933
+ *
1934
+ * An explicit set rather than a `_` prefix rule: a user's column may perfectly
1935
+ * well be called `_internal`, and passing it through here would put a value on
1936
+ * the row that no property describes and nothing downstream knows how to type.
1937
+ */
1938
+ var QUERY_METADATA_KEYS = /* @__PURE__ */ new Set([
1939
+ "_score",
1940
+ "_distance",
1941
+ "_matches"
1942
+ ]);
1673
1943
  function normalizeScalarValues(data, properties, collection, resolvedRelations, options) {
1674
1944
  const result = {};
1675
1945
  const internalFKColumns = /* @__PURE__ */ new Set();
@@ -1681,6 +1951,10 @@ function normalizeScalarValues(data, properties, collection, resolvedRelations,
1681
1951
  result[key] = value === null ? null : typeof value === "number" ? value : String(value);
1682
1952
  continue;
1683
1953
  }
1954
+ if (QUERY_METADATA_KEYS.has(key)) {
1955
+ result[key] = value;
1956
+ continue;
1957
+ }
1684
1958
  const property = properties[key];
1685
1959
  if (!property) continue;
1686
1960
  if (options.skipRelations && property.type === "relation") continue;
@@ -1933,7 +2207,7 @@ var RelationService = class {
1933
2207
  let query = this.db.select().from(targetTable).$dynamic();
1934
2208
  const additionalFilters = [];
1935
2209
  if (options.searchString) {
1936
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, targetCollection.properties, targetTable);
2210
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, targetCollection.properties, targetTable, targetCollection);
1937
2211
  if (searchConditions.length === 0) return [];
1938
2212
  const searchCombined = DrizzleConditionBuilder.combineConditionsWithOr(searchConditions);
1939
2213
  if (searchCombined) additionalFilters.push(searchCombined);
@@ -3039,7 +3313,7 @@ function sanitizeErrorForClient(error, context) {
3039
3313
  * Service for handling all row read operations.
3040
3314
  * Handles fetching, searching, counting, and filtering rows.
3041
3315
  */
3042
- var FetchService = class {
3316
+ var FetchService = class FetchService {
3043
3317
  db;
3044
3318
  registry;
3045
3319
  relationService;
@@ -3112,6 +3386,24 @@ var FetchService = class {
3112
3386
  * and skips rows rather than erroring. The guesses stay, last, for a
3113
3387
  * caller that hands over no collection to resolve against.
3114
3388
  */
3389
+ /**
3390
+ * The ORDER BY target, which may be relevance rather than a column.
3391
+ *
3392
+ * `_score` is only meaningful for a collection that declared a `search`
3393
+ * block *and* for a request that carried a search string — ranking rows
3394
+ * against no query ranks them all at zero. Outside those two conditions it
3395
+ * is an unknown field and gets the same 400 as any other typo, which is the
3396
+ * behaviour that matters: a sort that is silently dropped returns 200 with
3397
+ * rows in arbitrary order, and paging over that repeats and skips rows.
3398
+ */
3399
+ static SCORE_FIELD = "_score";
3400
+ resolveOrderTarget(table, orderBy, collection, searchString) {
3401
+ if (orderBy === FetchService.SCORE_FIELD && collection && searchString) {
3402
+ const rank = DrizzleConditionBuilder.buildSearchRankExpression(searchString, table, collection);
3403
+ if (rank) return rank;
3404
+ }
3405
+ return this.resolveOrderByField(table, orderBy, collection);
3406
+ }
3115
3407
  resolveOrderByField(table, orderBy, collection) {
3116
3408
  const columnAt = (key) => (key in table ? table[key] : void 0) || void 0;
3117
3409
  const direct = columnAt(orderBy);
@@ -3239,12 +3531,14 @@ var FetchService = class {
3239
3531
  */
3240
3532
  buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig, scopeCondition) {
3241
3533
  const queryOpts = {};
3534
+ const hidden = hiddenColumnsOption(getTableColumns(table), this.registry.getCollectionByPath(collectionPath) ?? void 0);
3535
+ if (hidden) queryOpts.columns = hidden;
3242
3536
  if (withConfig) queryOpts.with = withConfig;
3243
3537
  const allConditions = [];
3244
3538
  if (scopeCondition) allConditions.push(scopeCondition);
3245
3539
  if (options.searchString) {
3246
3540
  const collection = getCollectionByPath(collectionPath, this.registry);
3247
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
3541
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3248
3542
  if (searchConditions.length === 0) {
3249
3543
  queryOpts.where = and(eq(idField, -99999999));
3250
3544
  return queryOpts;
@@ -3267,7 +3561,7 @@ var FetchService = class {
3267
3561
  const orderExpressions = [];
3268
3562
  if (options.orderBy) {
3269
3563
  const collection = getCollectionByPath(collectionPath, this.registry);
3270
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3564
+ const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3271
3565
  if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3272
3566
  }
3273
3567
  orderExpressions.push(desc(idField));
@@ -3284,6 +3578,7 @@ var FetchService = class {
3284
3578
  if (!options.startAfter) return [];
3285
3579
  const cursor = options.startAfter;
3286
3580
  if (options.orderBy) {
3581
+ if (options.orderBy === FetchService.SCORE_FIELD) throw ApiError.badRequest("Cursor pagination (`startAfter`) cannot be combined with `orderBy: \"_score\"`. Relevance is computed per query rather than stored, so it cannot key a cursor. Use `limit`/`offset` for relevance-ordered pages, or order by a column.", "SCORE_CURSOR_UNSUPPORTED", { field: FetchService.SCORE_FIELD });
3287
3582
  const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
3288
3583
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3289
3584
  if (orderByField) {
@@ -3354,9 +3649,11 @@ var FetchService = class {
3354
3649
  const qb = this.getQueryBuilder(tableName);
3355
3650
  if (qb) try {
3356
3651
  const withConfig = this.buildWithConfig(collection);
3652
+ const hidden = hiddenColumnsOption(getTableColumns(table), collection);
3357
3653
  const row = await qb.findFirst({
3358
3654
  where: eq(idField, parsedId),
3359
- with: withConfig
3655
+ with: withConfig,
3656
+ ...hidden ? { columns: hidden } : {}
3360
3657
  });
3361
3658
  if (!row) return void 0;
3362
3659
  const flatRow = toFlatRow(row, collection, this.registry);
@@ -3370,7 +3667,8 @@ var FetchService = class {
3370
3667
  if (reachedDatabase(e)) throw e;
3371
3668
  logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
3372
3669
  }
3373
- const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
3670
+ const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
3671
+ const result = await this.db.select(visibleOne).from(table).where(eq(idField, parsedId)).limit(1);
3374
3672
  if (result.length === 0) return void 0;
3375
3673
  const raw = result[0];
3376
3674
  const values = await parseDataFromServer(raw, collection, this.db, this.registry);
@@ -3426,14 +3724,21 @@ var FetchService = class {
3426
3724
  }
3427
3725
  let vectorMeta;
3428
3726
  if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
3727
+ const visible = visibleColumnProjection(getTableColumns(table), collection);
3728
+ const rankSelect = options.searchString ? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection) : void 0;
3729
+ const matchesSelect = options.searchString && options.searchExplain ? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection) : void 0;
3429
3730
  let query = vectorMeta ? this.db.select({
3430
- table_row: table,
3731
+ table_row: visible ?? table,
3431
3732
  _distance: vectorMeta.distanceSelect
3432
- }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
3733
+ }).from(table).$dynamic() : rankSelect ? this.db.select({
3734
+ table_row: visible ?? table,
3735
+ _score: rankSelect,
3736
+ ...matchesSelect ? { _matches: matchesSelect } : {}
3737
+ }).from(table).$dynamic() : visible ? this.db.select(visible).from(table).$dynamic() : this.db.select().from(table).$dynamic();
3433
3738
  const allConditions = [];
3434
3739
  if (scopeCondition) allConditions.push(scopeCondition);
3435
3740
  if (options.searchString) {
3436
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
3741
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3437
3742
  if (searchConditions.length === 0) return [];
3438
3743
  allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
3439
3744
  }
@@ -3453,7 +3758,7 @@ var FetchService = class {
3453
3758
  const orderExpressions = [];
3454
3759
  if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
3455
3760
  else if (options.orderBy) {
3456
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3761
+ const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3457
3762
  if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3458
3763
  }
3459
3764
  orderExpressions.push(desc(idField));
@@ -3473,6 +3778,10 @@ var FetchService = class {
3473
3778
  const results = vectorMeta ? rawResults.map((r) => ({
3474
3779
  ...r.table_row,
3475
3780
  _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
3781
+ })) : rankSelect ? rawResults.map((r) => ({
3782
+ ...r.table_row,
3783
+ _score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
3784
+ ...matchesSelect ? { _matches: r._matches ?? [] } : {}
3476
3785
  })) : rawResults;
3477
3786
  return this.processRowResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
3478
3787
  }
@@ -3564,7 +3873,7 @@ var FetchService = class {
3564
3873
  const allConditions = [];
3565
3874
  if (hop) allConditions.push(this.buildRelationScope(hop));
3566
3875
  if (options.searchString) {
3567
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
3876
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3568
3877
  if (searchConditions.length === 0) return 0;
3569
3878
  allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
3570
3879
  }
@@ -3709,7 +4018,8 @@ var FetchService = class {
3709
4018
  if (reachedDatabase(e)) throw e;
3710
4019
  logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
3711
4020
  }
3712
- const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
4021
+ const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
4022
+ const result = await this.db.select(visibleOne).from(table).where(eq(idField, parsedId)).limit(1);
3713
4023
  if (result.length === 0) return null;
3714
4024
  const flatEntity = { ...result[0] };
3715
4025
  if (!include || include.length === 0) return flatEntity;
@@ -3747,14 +4057,21 @@ var FetchService = class {
3747
4057
  const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
3748
4058
  let vectorMeta;
3749
4059
  if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
4060
+ const visible = visibleColumnProjection(getTableColumns(table), collection);
4061
+ const rankSelect = options.searchString ? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection) : void 0;
4062
+ const matchesSelect = options.searchString && options.searchExplain ? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection) : void 0;
3750
4063
  let query = vectorMeta ? this.db.select({
3751
- table_row: table,
4064
+ table_row: visible ?? table,
3752
4065
  _distance: vectorMeta.distanceSelect
3753
- }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
4066
+ }).from(table).$dynamic() : rankSelect ? this.db.select({
4067
+ table_row: visible ?? table,
4068
+ _score: rankSelect,
4069
+ ...matchesSelect ? { _matches: matchesSelect } : {}
4070
+ }).from(table).$dynamic() : visible ? this.db.select(visible).from(table).$dynamic() : this.db.select().from(table).$dynamic();
3754
4071
  const allConditions = [];
3755
4072
  if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
3756
4073
  if (options.searchString) {
3757
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
4074
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3758
4075
  if (searchConditions.length === 0) return [];
3759
4076
  allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
3760
4077
  }
@@ -3770,7 +4087,7 @@ var FetchService = class {
3770
4087
  const orderExpressions = [];
3771
4088
  if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
3772
4089
  else if (options.orderBy) {
3773
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
4090
+ const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3774
4091
  if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3775
4092
  }
3776
4093
  orderExpressions.push(desc(idField));
@@ -3783,6 +4100,11 @@ var FetchService = class {
3783
4100
  ...r.table_row,
3784
4101
  _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
3785
4102
  }));
4103
+ if (rankSelect) return rawResults.map((r) => ({
4104
+ ...r.table_row,
4105
+ _score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
4106
+ ...matchesSelect ? { _matches: r._matches ?? [] } : {}
4107
+ }));
3786
4108
  return rawResults;
3787
4109
  }
3788
4110
  /**
@@ -3796,51 +4118,6 @@ var FetchService = class {
3796
4118
  return !!this.getQueryBuilder(tableName);
3797
4119
  }
3798
4120
  /**
3799
- * Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
3800
- * for efficient JOIN-based relation loading.
3801
- * Returns null if the API is not available or the query fails.
3802
- * Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
3803
- */
3804
- async fetchWithDrizzleQuery(collectionPath, collection, options, include, idInfo, idInfoArray) {
3805
- try {
3806
- const table = getTableForCollection(collection, this.registry);
3807
- const tableName = getTableName(table);
3808
- const queryTarget = this.getQueryBuilder(tableName);
3809
- if (!queryTarget?.findMany) return null;
3810
- const resolvedRelations = resolveCollectionRelations(collection);
3811
- const withConfig = {};
3812
- for (const [key, relation] of Object.entries(resolvedRelations)) if (include[0] === "*" || include.includes(key)) {
3813
- const drizzleRelName = relation.relationName || key;
3814
- withConfig[drizzleRelName] = true;
3815
- }
3816
- const queryOpts = { with: withConfig };
3817
- if (options.limit) queryOpts.limit = options.limit;
3818
- if (options.filter) {
3819
- const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
3820
- if (filterConditions.length > 0) queryOpts.where = and(...filterConditions);
3821
- }
3822
- if (options.orderBy) {
3823
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3824
- if (orderByField) queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
3825
- }
3826
- return (await queryTarget.findMany(queryOpts)).map((row) => {
3827
- const flat = {};
3828
- for (const [k, v] of Object.entries(row)) if (Array.isArray(v)) flat[k] = v.map((item) => {
3829
- const keys = Object.keys(item);
3830
- const nestedObj = keys.find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
3831
- if (nestedObj && keys.length <= 3) return { ...item[nestedObj] };
3832
- return { ...item };
3833
- });
3834
- else if (typeof v === "object" && v !== null) flat[k] = { ...v };
3835
- else flat[k] = v;
3836
- return flat;
3837
- });
3838
- } catch (e) {
3839
- logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
3840
- return null;
3841
- }
3842
- }
3843
- /**
3844
4121
  * Fallback path used when db.query is unavailable.
3845
4122
  * The primary path uses db.query.findMany with `with` config, which
3846
4123
  * loads all relations in a single query.
@@ -4091,7 +4368,8 @@ var PersistService = class {
4091
4368
  if (error instanceof ApiError || error?.name === "ApiError") return error;
4092
4369
  const pgError = extractPgError(error);
4093
4370
  if (pgError) {
4094
- const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
4371
+ const { message, code } = pgErrorToFriendlyMessage(pgError, collectionSlug);
4372
+ if (/^2[23]/.test(code)) return code === "23505" ? ApiError.conflict(message, `PG_${code}`) : ApiError.badRequest(message, `PG_${code}`);
4095
4373
  return new Error(message);
4096
4374
  }
4097
4375
  const causeMessage = extractCauseMessage(error);
@@ -4316,6 +4594,7 @@ var BranchService = class {
4316
4594
  metadata JSONB DEFAULT '{}'
4317
4595
  );
4318
4596
  `));
4597
+ await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "branches")));
4319
4598
  }
4320
4599
  /**
4321
4600
  * Create a new branch database by templating the source database.
@@ -4423,43 +4702,49 @@ var BranchService = class {
4423
4702
  };
4424
4703
  //#endregion
4425
4704
  //#region src/security/rls-enforcement.ts
4705
+ var quoteIdent$1 = (name) => `"${name.replace(/"/g, "\"\"")}"`;
4706
+ /** DML the user role holds on managed tables (RLS still filters per row). */
4707
+ var USER_TABLE_PRIVILEGES = "SELECT, INSERT, UPDATE, DELETE";
4426
4708
  /**
4427
- * Unified RLS enforcement the "user context vs server context" model.
4709
+ * Warn when the connection role shares its name with an existing schema.
4428
4710
  *
4429
- * Every operation runs in one of two contexts:
4711
+ * Postgres resolves unqualified names through `search_path`, which defaults to
4712
+ * `"$user", public` — and `$user` is the connection ROLE. When a schema of that
4713
+ * name exists it sits ahead of `public`, so every unqualified statement
4714
+ * silently operates on it instead:
4430
4715
  *
4431
- * - **User context** a request authenticated (or anonymous) via
4432
- * `driver.withAuth(user)`. Runs as the restricted `rebase_user` role: a
4433
- * non-owner, NOSUPERUSER, NOBYPASSRLS role, so Postgres RLS binds *every*
4434
- * statement (SELECT, INSERT, UPDATE, DELETE). The collection's
4435
- * `securityRules` are the whole authorization model; app-layer callbacks
4436
- * are validation/side-effects, not a security boundary.
4716
+ * CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts
4437
4717
  *
4438
- * - **Server context** the base (owner) connection: auth flows, migrations,
4439
- * background jobs, and the explicit `rebase.dataAsAdmin` accessor. As table
4440
- * owner it bypasses RLS. This is the trusted plane, equivalent to
4441
- * Supabase's `service_role`.
4718
+ * Nothing errors. You get a second table of the same name in the wrong schema,
4719
+ * and reads that pin `public` cannot see it — which reads as "missing table" and
4720
+ * sends people to re-run a push that creates a *third* copy. The bootstrapper
4721
+ * has a whole branch dedicated to recognising the symptom after the fact.
4442
4722
  *
4443
- * This module provides the three pieces:
4723
+ * Rebase shipped straight into this: it creates a schema named `rebase` while
4724
+ * every template named the database role `rebase` too. The scaffold uses
4725
+ * `rebase_app` now, and every pool Rebase opens pins `search_path=public`
4726
+ * (`pinSearchPath`), which covers the paths the framework controls. This covers
4727
+ * the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,
4728
+ * a hand-written migration — because the hazard is a property of the two NAMES,
4729
+ * not of any one connection.
4444
4730
  *
4445
- * 1. {@link detectConnectionPosture} is the connection subject to RLS at
4446
- * all? (superuser / BYPASSRLS / table owner no)
4447
- * 2. {@link ensureAppRole} idempotently provision `rebase_user` with
4448
- * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future
4449
- * tables stay covered).
4450
- * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the
4451
- * policies read (`auth.uid()` etc.) and `SET LOCAL ROLE rebase_user` so
4452
- * RLS binds. Transaction-scoped, so it composes with poolers.
4453
- *
4454
- * Provisioning runs from the framework's own bootstrap/migrate (which already
4455
- * self-creates the `auth` schema and functions) — enforcement is default-on,
4456
- * not an operator opt-in.
4731
+ * A warning rather than a boot failure: the database works, the framework's own
4732
+ * traffic is pinned, and refusing to start over a naming choice a user may have
4733
+ * inherited would be worse than the risk.
4457
4734
  */
4458
- /** The restricted role every authenticated (user-context) request runs as. */
4459
- var REBASE_USER_ROLE = "rebase_user";
4460
- var quoteIdent$1 = (name) => `"${name.replace(/"/g, "\"\"")}"`;
4461
- /** DML the user role holds on managed tables (RLS still filters per row). */
4462
- var USER_TABLE_PRIVILEGES = "SELECT, INSERT, UPDATE, DELETE";
4735
+ async function warnOnRoleSchemaCollision(run) {
4736
+ try {
4737
+ const rows = await run(`
4738
+ SELECT current_user AS role,
4739
+ EXISTS (
4740
+ SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user
4741
+ ) AS collides
4742
+ `);
4743
+ if (rows[0]?.collides !== true) return;
4744
+ const role = String(rows[0]?.role ?? "the connection role");
4745
+ logger.warn(`⚠️ The database role "${role}" has the same name as a schema. Postgres resolves unqualified names through \`search_path\`, which defaults to \`"$user", public\` — so "${role}" is searched BEFORE public, and any unqualified \`CREATE TABLE\`/\`SELECT\` from a tool that does not pin the path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in "${role}" instead. Rebase's own connections pin \`search_path=public\`, so the server is unaffected. To remove the hazard entirely, connect as a role whose name is not also a schema — the scaffold uses "rebase_app".`);
4746
+ } catch {}
4747
+ }
4463
4748
  async function detectConnectionPosture(run) {
4464
4749
  const row = (await run(`
4465
4750
  SELECT current_user AS role,
@@ -4535,6 +4820,9 @@ async function ensureAppRole(run, schemas) {
4535
4820
  await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);
4536
4821
  await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);
4537
4822
  await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);
4823
+ await revokeInternalTableAccess(async (text) => {
4824
+ await run(text);
4825
+ }, schema, { onError: (table, error) => logger.warn(`🔐 [rls] Could not revoke "${REBASE_USER_ROLE}" access to "${schema}"."${table}" — it stays reachable by authenticated requests: ` + (error instanceof Error ? error.message : String(error))) });
4538
4826
  }
4539
4827
  logger.info(`🔐 [rls] User role "${REBASE_USER_ROLE}" provisioned (schemas: ${uniqueSchemas.join(", ")})`);
4540
4828
  }
@@ -4620,6 +4908,40 @@ function warnOnAnonymousGrants(collections) {
4620
4908
  logger.warn(`Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are true for everyone:\n\n` + problems.join("\n\n") + "\n");
4621
4909
  }
4622
4910
  /**
4911
+ * Name the collections whose raw policy SQL still calls the pre-1.0 helpers.
4912
+ *
4913
+ * The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the
4914
+ * database, so nothing is broken and no policy is wrong — which is exactly why
4915
+ * this has to be said out loud. A silent rewrite that works forever is not a
4916
+ * migration, it is a second supported spelling nobody wrote down, and the next
4917
+ * person to read those rules will copy the old one.
4918
+ *
4919
+ * Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,
4920
+ * `policy.rolesOverlap(...)`) compile from the model and were never affected.
4921
+ */
4922
+ function warnOnLegacyRlsFunctions(collections) {
4923
+ const sites = [];
4924
+ for (const collection of collections) for (const rule of collection.securityRules ?? []) {
4925
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
4926
+ if (![usingExpr, withCheckExpr].filter((e) => e !== null).some(containsLegacyRlsCall)) continue;
4927
+ const site = `${collection.slug ?? "(unnamed)"} → "${rule.name ?? "(unnamed rule)"}"`;
4928
+ if (!sites.includes(site)) sites.push(site);
4929
+ }
4930
+ if (sites.length === 0) return;
4931
+ logger.warn(`These security rules call the pre-1.0 RLS helpers (\`auth.uid()\`, \`auth.roles()\`, \`auth.jwt()\`). They still work — the compiler rewrites them — but the functions now live in the \`rebase\` schema, and the \`auth\` one is Supabase's. Update the raw SQL in these rules to \`${REBASE_SCHEMA}.uid()\` and friends, or switch them to the structured helpers (\`policy.authUid()\`, \`policy.rolesOverlap()\`), which never had to be spelled by hand:\n\n` + sites.map((s) => ` • ${s}`).join("\n") + "\n");
4932
+ }
4933
+ /** Whether any `raw` expression in the tree calls a pre-1.0 helper. */
4934
+ function containsLegacyRlsCall(expr) {
4935
+ switch (expr.kind) {
4936
+ case "raw": return usesLegacyRlsFunctions(expr.sql);
4937
+ case "and":
4938
+ case "or": return expr.operands.some(containsLegacyRlsCall);
4939
+ case "not": return containsLegacyRlsCall(expr.operand);
4940
+ case "existsIn": return containsLegacyRlsCall(expr.where);
4941
+ default: return false;
4942
+ }
4943
+ }
4944
+ /**
4623
4945
  * Reject `pgRoles` that this server can never satisfy.
4624
4946
  *
4625
4947
  * `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a
@@ -4749,6 +5071,25 @@ var PostgresBackendDriver = class PostgresBackendDriver {
4749
5071
  }
4750
5072
  };
4751
5073
  }
5074
+ /**
5075
+ * Build the context handed to every collection callback.
5076
+ *
5077
+ * Note `data: this.data` — `this` is whichever driver is running the
5078
+ * operation, so the callback's data plane inherits that driver's privilege.
5079
+ * On a user request `AuthenticatedPostgresBackendDriver.withTransaction`
5080
+ * constructs a fresh base driver bound to the RLS-scoped transaction and
5081
+ * runs the operation on it, so `this.data` speaks through that connection
5082
+ * and policies apply. On server-context work `this` is the base driver on
5083
+ * the owner connection, and they do not. Pinned by the
5084
+ * `"scopes context.data to the caller"` case in the `rls-enforcement` e2e
5085
+ * suite, because it is the kind of property that is easy to break from a
5086
+ * distance and impossible to notice.
5087
+ *
5088
+ * Previously returned through `as unknown as RebaseCallContext`, which
5089
+ * disabled checking for the whole object and let `driver` — documented in
5090
+ * the callbacks guide — sit on the runtime context while absent from the
5091
+ * contract. Both are declared now, so this is a plain typed return.
5092
+ */
4752
5093
  buildCallContext() {
4753
5094
  return {
4754
5095
  user: this.user,
@@ -5204,6 +5545,106 @@ var PostgresBackendDriver = class PostgresBackendDriver {
5204
5545
  return saved;
5205
5546
  });
5206
5547
  }
5548
+ /**
5549
+ * Update many rows through the same pipeline as {@link save}, in one
5550
+ * transaction.
5551
+ *
5552
+ * Structurally the mirror of {@link saveMany} — same tx-bound sub-driver,
5553
+ * same deferred notifications, same per-row error labelling — but it calls
5554
+ * `save` with an explicit `id` and `status: "existing"`, which is precisely
5555
+ * what `saveMany` cannot do: that one passes `status: "new"` and keeps the
5556
+ * key inside `values`, so it inserts or upserts and can never target a
5557
+ * particular row.
5558
+ *
5559
+ * All-or-nothing, so an id matching no row aborts the batch. A partial
5560
+ * update is the outcome with no good recovery: the caller cannot tell which
5561
+ * half landed without re-reading everything.
5562
+ */
5563
+ async updateMany({ path, updates, collection }) {
5564
+ return this.db.transaction(async (tx) => {
5565
+ const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
5566
+ txDriver.dataService = new DataService(tx, this.registry);
5567
+ txDriver.client = this.client;
5568
+ txDriver._deferNotifications = this._deferNotifications;
5569
+ txDriver._pendingNotifications = this._pendingNotifications;
5570
+ const saved = [];
5571
+ for (let i = 0; i < updates.length; i++) {
5572
+ const { id, values } = updates[i];
5573
+ try {
5574
+ if (!await txDriver.fetchOne({
5575
+ path,
5576
+ id: String(id),
5577
+ collection
5578
+ })) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
5579
+ statusCode: 404,
5580
+ code: "NOT_FOUND"
5581
+ });
5582
+ saved.push(await txDriver.save({
5583
+ path,
5584
+ id: String(id),
5585
+ values,
5586
+ collection,
5587
+ status: "existing"
5588
+ }));
5589
+ } catch (error) {
5590
+ throw Object.assign(new Error(`Update ${i} of ${updates.length} (id ${JSON.stringify(id)}) failed: ${error?.message ?? error}`, { cause: error }), {
5591
+ statusCode: error?.statusCode,
5592
+ code: error?.code,
5593
+ name: error?.name
5594
+ });
5595
+ }
5596
+ }
5597
+ return saved;
5598
+ });
5599
+ }
5600
+ /**
5601
+ * Delete many rows in one transaction, running the full delete pipeline —
5602
+ * `beforeDelete`, the delete, `afterDelete` — for each.
5603
+ *
5604
+ * Looping the single-row {@link delete} rather than emitting one
5605
+ * `DELETE ... WHERE id = ANY($1)` is the deliberate choice: a single
5606
+ * statement would be faster and would skip every callback, so a collection
5607
+ * relying on `beforeDelete` to veto or on `afterDelete` to clean up
5608
+ * dependents would behave differently depending on how many rows the caller
5609
+ * happened to delete at once. Same pipeline, one transaction.
5610
+ */
5611
+ async deleteMany({ path, ids, collection }) {
5612
+ await this.db.transaction(async (tx) => {
5613
+ const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
5614
+ txDriver.dataService = new DataService(tx, this.registry);
5615
+ txDriver.client = this.client;
5616
+ txDriver._deferNotifications = this._deferNotifications;
5617
+ txDriver._pendingNotifications = this._pendingNotifications;
5618
+ for (let i = 0; i < ids.length; i++) {
5619
+ const id = ids[i];
5620
+ try {
5621
+ const existing = await txDriver.fetchOne({
5622
+ path,
5623
+ id: String(id),
5624
+ collection
5625
+ });
5626
+ if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
5627
+ statusCode: 404,
5628
+ code: "NOT_FOUND"
5629
+ });
5630
+ await txDriver.delete({
5631
+ row: {
5632
+ id: String(id),
5633
+ path,
5634
+ values: existing
5635
+ },
5636
+ collection
5637
+ });
5638
+ } catch (error) {
5639
+ throw Object.assign(new Error(`Delete ${i} of ${ids.length} (id ${JSON.stringify(id)}) failed: ${error?.message ?? error}`, { cause: error }), {
5640
+ statusCode: error?.statusCode,
5641
+ code: error?.code,
5642
+ name: error?.name
5643
+ });
5644
+ }
5645
+ }
5646
+ });
5647
+ }
5207
5648
  async delete({ row, collection }) {
5208
5649
  const targetPath = row.path;
5209
5650
  const targetRow = { ...row.values ?? {} };
@@ -6168,11 +6609,13 @@ var computeSharedRelationName = (rel, sourceCollection, _collections) => {
6168
6609
  }
6169
6610
  return fallback;
6170
6611
  };
6171
- var generateSchema = async (collections, stripPolicies = false) => {
6612
+ var generateSchema = async (allCollections, stripPolicies = false) => {
6613
+ const collections = relationalCollections(allCollections);
6172
6614
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
6173
6615
  const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
6174
6616
  const hasVector = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "vector"));
6175
6617
  const hasBinary = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "binary"));
6618
+ const hasSearch = collections.some((c) => buildSearchColumnSpec(c) !== void 0);
6176
6619
  const pgCoreImports = [
6177
6620
  "primaryKey",
6178
6621
  "pgTable",
@@ -6197,7 +6640,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
6197
6640
  ];
6198
6641
  if (hasUuid) pgCoreImports.push("uuid");
6199
6642
  if (hasVector) pgCoreImports.push("vector");
6200
- if (hasBinary) pgCoreImports.push("customType");
6643
+ if (hasBinary || hasSearch) pgCoreImports.push("customType");
6201
6644
  const uniqueSchemas = Array.from(new Set(collections.map((c) => isPostgresCollectionConfig(c) ? c.schema : void 0).filter(Boolean)));
6202
6645
  if (uniqueSchemas.length > 0) pgCoreImports.push("pgSchema");
6203
6646
  schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
@@ -6279,6 +6722,11 @@ var generateSchema = async (collections, stripPolicies = false) => {
6279
6722
  const columnString = getDrizzleColumn(propName, prop, collection, collections);
6280
6723
  if (columnString) columns.add(columnString);
6281
6724
  });
6725
+ const searchSpec = buildSearchColumnSpec(collection);
6726
+ if (searchSpec) {
6727
+ columns.add(` ${searchSpec.column}: customType({ dataType() { return 'tsvector'; } })("${searchSpec.column}").generatedAlwaysAs(sql\`${searchSpec.expression}\`)`);
6728
+ if (searchSpec.fuzzy) columns.add(` ${searchSpec.fuzzy.column}: text("${searchSpec.fuzzy.column}").generatedAlwaysAs(sql\`${searchSpec.fuzzy.expression}\`)`);
6729
+ }
6282
6730
  if (!Array.from(columns).some((col) => col.includes(".primaryKey()"))) columns.add(" id: text(\"id\").primaryKey()");
6283
6731
  schemaContent += `${Array.from(columns).join(",\n")}`;
6284
6732
  const securityRules = getEffectiveSecurityRules(collection);
@@ -6982,6 +7430,8 @@ var ChannelHistoryStore = class {
6982
7430
  last_seq BIGINT NOT NULL
6983
7431
  )
6984
7432
  `);
7433
+ await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_messages")));
7434
+ await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_cursors")));
6985
7435
  this.tablesReady = true;
6986
7436
  logger.info(`✅ [ChannelHistory] Retained channels ready (${this.rules.length} rule(s)).`);
6987
7437
  }
@@ -7165,6 +7615,7 @@ var ChannelPresenceStore = class {
7165
7615
  CREATE INDEX IF NOT EXISTS idx_channel_presence_last_seen
7166
7616
  ON rebase.channel_presence (last_seen)
7167
7617
  `);
7618
+ await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence")));
7168
7619
  this.tablesReady = true;
7169
7620
  }
7170
7621
  /** Record (or refresh) a client's presence. */
@@ -7695,7 +8146,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
7695
8146
  limit: config.limit,
7696
8147
  startAfter: config.startAfter,
7697
8148
  databaseId: config.databaseId,
7698
- searchString: config.searchString
8149
+ searchString: config.searchString,
8150
+ searchExplain: config.searchExplain
7699
8151
  }
7700
8152
  });
7701
8153
  if (callback) this.subscriptionCallbacks.set(subscriptionId, callback);
@@ -7816,23 +8268,19 @@ var RealtimeService = class RealtimeService extends EventEmitter {
7816
8268
  path: request.path,
7817
8269
  collectionRequest: {
7818
8270
  filter: request.filter,
8271
+ logical: request.logical,
7819
8272
  orderBy: request.orderBy,
7820
8273
  order: request.order,
7821
8274
  limit: boundedLimit,
8275
+ offset: request.offset,
7822
8276
  startAfter: request.startAfter,
7823
8277
  databaseId: request.collection?.databaseId,
7824
- searchString: request.searchString
8278
+ searchString: request.searchString,
8279
+ searchExplain: request.searchExplain
7825
8280
  },
7826
8281
  authContext
7827
8282
  });
7828
- const rows = await this.fetchCollectionWithAuth(request.path, {
7829
- filter: request.filter,
7830
- orderBy: request.orderBy,
7831
- order: request.order,
7832
- limit: boundedLimit,
7833
- startAfter: request.startAfter,
7834
- searchString: request.searchString
7835
- }, authContext);
8283
+ const rows = await this.fetchCollectionWithAuth(request.path, this._subscriptions.get(subscriptionId).collectionRequest, authContext);
7836
8284
  this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
7837
8285
  } catch (error) {
7838
8286
  const sanitized = sanitizeErrorForClient(error, request.path);
@@ -8012,10 +8460,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8012
8460
  orderBy: collectionRequest.orderBy,
8013
8461
  order: collectionRequest.order,
8014
8462
  limit: collectionRequest.limit,
8015
- databaseId: collectionRequest.databaseId
8463
+ databaseId: collectionRequest.databaseId,
8464
+ searchExplain: collectionRequest.searchExplain
8016
8465
  });
8017
8466
  else fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
8018
8467
  filter: collectionRequest.filter,
8468
+ logical: collectionRequest.logical,
8019
8469
  orderBy: collectionRequest.orderBy,
8020
8470
  order: collectionRequest.order,
8021
8471
  limit: collectionRequest.limit,
@@ -9131,7 +9581,7 @@ function createBackupCron(config) {
9131
9581
  enabled: config.enabled ?? true,
9132
9582
  timeoutSeconds: 3600,
9133
9583
  async handler({ log }) {
9134
- const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-CD8o_1Sl.js").then((n) => n.r);
9584
+ const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-Bww-Lg0s.js").then((n) => n.r);
9135
9585
  const { destination } = config;
9136
9586
  if (destination.kind !== "local" && !config.storage) throw new Error(`Backup destination is ${destination.kind} but no storage controller was provided. Pass the backend's configured StorageController to createBackupCron({ storage }).`);
9137
9587
  log(`Starting backup of "${dbName}"…`);
@@ -9203,6 +9653,54 @@ var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
9203
9653
  /** `on.from` / `on.to` accept a single column or a composite tuple. */
9204
9654
  var asColumns = (value) => Array.isArray(value) ? value : [value];
9205
9655
  /**
9656
+ * Distinguish "this column name is wrong" from "the generated schema is old".
9657
+ *
9658
+ * They present identically here — a relation asks for a column the registered
9659
+ * table does not have — but they are opposite problems with opposite fixes, and
9660
+ * getting them the wrong way round is how the 0.12 → 0.13 upgrade bricked
9661
+ * projects.
9662
+ *
9663
+ * The registered table is not the database. It comes from the project's
9664
+ * checked-in `backend/src/schema.generated.ts`, and 0.13 changed the rule that
9665
+ * derives foreign-key names: `categories` yields `category_id` where it used to
9666
+ * yield `categorie_id`. Boot-ensure renames the database column to match, so by
9667
+ * the time this runs the *database* is correct and the *generated module* is the
9668
+ * stale one. Reporting "not a column" then points at the wrong artifact, and the
9669
+ * generic fix — "set `through.targetColumn` to one of: …", listing the legacy
9670
+ * name because that is what the stale module still has — talks the reader into
9671
+ * pinning a column that no longer exists.
9672
+ *
9673
+ * So when the wanted name is what the current rule derives, and the table
9674
+ * carries what the *previous* rule would have derived from the same source, say
9675
+ * that instead.
9676
+ *
9677
+ * @param wanted the column the relation asks for
9678
+ * @param available every column the registered table has
9679
+ * @param sources names the default could have been derived from (a slug, a
9680
+ * relation name) — checking against these rather than guessing
9681
+ * backwards from `wanted` keeps the match exact
9682
+ */
9683
+ function staleCodegenRename(wanted, available, sources) {
9684
+ for (const source of sources) {
9685
+ if (!source) continue;
9686
+ const current = generateForeignKeyName(source);
9687
+ const legacy = legacyForeignKeyName(source);
9688
+ if (current !== wanted || legacy === current) continue;
9689
+ if (available.has(legacy) && !available.has(current)) return {
9690
+ legacy,
9691
+ current
9692
+ };
9693
+ }
9694
+ return null;
9695
+ }
9696
+ /** The shared explanation, so every relation kind reports it identically. */
9697
+ function staleCodegenDefect(table, { legacy, current }) {
9698
+ return {
9699
+ problem: `the generated Drizzle schema still declares \`${legacy}\` on \`${table}\`, but this release derives \`${current}\` — the generated schema predates the foreign-key naming fix and no longer describes the database`,
9700
+ fix: `regenerate it with \`rebase schema generate\` (or \`pnpm run schema:generate\`). The database column has already been renamed for you at boot, so nothing else is needed. To keep \`${legacy}\` instead, name it explicitly on the relation and regenerate.`
9701
+ };
9702
+ }
9703
+ /**
9206
9704
  * Relations whose names do not resolve against the registered schema.
9207
9705
  *
9208
9706
  * Fails open wherever it cannot see enough to be sure — an unregistered source
@@ -9249,19 +9747,31 @@ function findRelationDefects(collections, registry) {
9249
9747
  const targetColumns = columnNames(targetTable);
9250
9748
  switch (relation.kind) {
9251
9749
  case "belongsTo":
9252
- if (!sourceColumns.has(relation.localKey)) defects.push({
9253
- ...at,
9254
- problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
9255
- fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
9256
- });
9750
+ if (!sourceColumns.has(relation.localKey)) {
9751
+ const stale = staleCodegenRename(relation.localKey, sourceColumns, [relation.relationName, targetCollection.slug]);
9752
+ defects.push(stale ? {
9753
+ ...at,
9754
+ ...staleCodegenDefect(sourceTableName, stale)
9755
+ } : {
9756
+ ...at,
9757
+ problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
9758
+ fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
9759
+ });
9760
+ }
9257
9761
  break;
9258
9762
  case "hasOne":
9259
9763
  case "hasMany":
9260
- if (!targetColumns.has(relation.foreignKeyOnTarget)) defects.push({
9261
- ...at,
9262
- problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
9263
- fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
9264
- });
9764
+ if (!targetColumns.has(relation.foreignKeyOnTarget)) {
9765
+ const stale = staleCodegenRename(relation.foreignKeyOnTarget, targetColumns, [collection.slug]);
9766
+ defects.push(stale ? {
9767
+ ...at,
9768
+ ...staleCodegenDefect(targetTableName, stale)
9769
+ } : {
9770
+ ...at,
9771
+ problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
9772
+ fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
9773
+ });
9774
+ }
9265
9775
  if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
9266
9776
  ...at,
9267
9777
  problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
@@ -9280,11 +9790,21 @@ function findRelationDefects(collections, registry) {
9280
9790
  break;
9281
9791
  }
9282
9792
  const junctionColumns = columnNames(junction);
9283
- for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]]) if (!junctionColumns.has(column)) defects.push({
9284
- ...at,
9285
- problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
9286
- fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
9287
- });
9793
+ const derivedFrom = {
9794
+ sourceColumn: [collection.slug],
9795
+ targetColumn: [targetCollection.slug]
9796
+ };
9797
+ for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]]) if (!junctionColumns.has(column)) {
9798
+ const stale = staleCodegenRename(column, junctionColumns, [...derivedFrom[label]]);
9799
+ defects.push(stale ? {
9800
+ ...at,
9801
+ ...staleCodegenDefect(table, stale)
9802
+ } : {
9803
+ ...at,
9804
+ problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
9805
+ fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
9806
+ });
9807
+ }
9288
9808
  break;
9289
9809
  }
9290
9810
  case "via": {
@@ -9356,7 +9876,16 @@ function assertRelationsResolve(collections, registry) {
9356
9876
  const defects = findRelationDefects(collections, registry);
9357
9877
  if (defects.length === 0) return;
9358
9878
  const lines = defects.map((d) => ` • ${d.collection}.${d.relationName} (${d.kind})\n ${d.problem}\n fix: ${d.fix}`);
9359
- throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against the database schema.\n\nEach of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
9879
+ throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against \`backend/src/schema.generated.ts\`.
9880
+
9881
+ Each of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
9882
+
9883
+ If the database was migrated recently — an upgrade, a \`db push\`, a restore — this file is
9884
+ probably older than the schema it describes. Regenerate it before changing anything else:
9885
+
9886
+ rebase schema generate
9887
+
9888
+ If it is already current, then the collection is what disagrees with it:
9360
9889
 
9361
9890
  ` + lines.join("\n\n") + "\n");
9362
9891
  }
@@ -9567,7 +10096,7 @@ async function ensureAuthTablesExist(db, collection) {
9567
10096
  if (idProp) {
9568
10097
  const isId = "isId" in idProp ? idProp.isId : void 0;
9569
10098
  if (isId === "uuid") userIdType = "UUID";
9570
- else if (isId === "autoincrement") userIdType = "INTEGER";
10099
+ else if (isId === "increment") userIdType = "INTEGER";
9571
10100
  }
9572
10101
  }
9573
10102
  try {
@@ -9600,43 +10129,42 @@ async function ensureAuthTablesExist(db, collection) {
9600
10129
  const emailLengthConstraint = `"${authIdentifier("email_length_check")}"`;
9601
10130
  const emailLowerUniqueIndex = authIdentifier("email_lower_key");
9602
10131
  const verificationTokenIndex = authIdentifier("email_verification_token_idx");
10132
+ const usersColumnDdl = AUTH_USERS_COLUMNS.map((spec) => spec.column === "email" ? `${spec.column} ${authUsersColumnSql(spec)} CONSTRAINT ${emailLengthConstraint} CHECK (length(email) <= 320)` : `${spec.column} ${authUsersColumnSql(spec)}`).join(",\n ");
9603
10133
  await db.execute(sql`
9604
10134
  CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (
9605
10135
  id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},
9606
- email TEXT NOT NULL CONSTRAINT ${sql.raw(emailLengthConstraint)} CHECK (length(email) <= 320),
9607
- display_name TEXT,
9608
- photo_url TEXT,
9609
- roles TEXT[] DEFAULT '{}' NOT NULL,
9610
- password_hash TEXT,
9611
- email_verified BOOLEAN DEFAULT FALSE NOT NULL,
9612
- email_verification_token TEXT,
9613
- email_verification_sent_at TIMESTAMP WITH TIME ZONE,
9614
- is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
9615
- metadata JSONB DEFAULT '{}' NOT NULL,
9616
- tokens_valid_after TIMESTAMP WITH TIME ZONE,
9617
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
9618
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
10136
+ ${sql.raw(usersColumnDdl)}
9619
10137
  )
9620
10138
  `);
9621
- await db.execute(sql`
9622
- CREATE OR REPLACE FUNCTION ${sql.raw(`"${authSchema}"`)}.sync_uid_user_id() RETURNS trigger AS $$
9623
- BEGIN
9624
- IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN
9625
- NEW.uid := NEW.user_id;
9626
- ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN
9627
- NEW.user_id := NEW.uid;
9628
- END IF;
9629
- RETURN NEW;
9630
- END $$ LANGUAGE plpgsql
9631
- `);
9632
- for (const authTable of [
10139
+ const legacyFkTables = [
9633
10140
  "user_identities",
9634
10141
  "refresh_tokens",
9635
10142
  "password_reset_tokens",
9636
10143
  "magic_link_tokens",
9637
10144
  "mfa_factors",
9638
10145
  "recovery_codes"
9639
- ]) {
10146
+ ];
10147
+ const legacyFkTableList = legacyFkTables.map((t) => `'${t}'`).join(", ");
10148
+ const legacyUserIdPresent = await db.execute(sql`
10149
+ SELECT 1
10150
+ FROM information_schema.columns
10151
+ WHERE table_schema = ${authSchema}
10152
+ AND table_name IN (${sql.raw(legacyFkTableList)})
10153
+ AND column_name = 'user_id'
10154
+ LIMIT 1
10155
+ `);
10156
+ if (legacyUserIdPresent.rows.length > 0) await db.execute(sql`
10157
+ CREATE OR REPLACE FUNCTION ${sql.raw(`"${authSchema}"`)}.sync_uid_user_id() RETURNS trigger AS $$
10158
+ BEGIN
10159
+ IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN
10160
+ NEW.uid := NEW.user_id;
10161
+ ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN
10162
+ NEW.user_id := NEW.uid;
10163
+ END IF;
10164
+ RETURN NEW;
10165
+ END $$ LANGUAGE plpgsql
10166
+ `);
10167
+ for (const authTable of legacyUserIdPresent.rows.length > 0 ? legacyFkTables : []) {
9640
10168
  const qualified = `"${authSchema}"."${authTable}"`;
9641
10169
  await db.execute(sql`
9642
10170
  DO $$
@@ -9757,54 +10285,50 @@ async function ensureAuthTablesExist(db, collection) {
9757
10285
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
9758
10286
  )
9759
10287
  `);
9760
- await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
9761
10288
  await db.transaction(async (tx) => {
9762
10289
  await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
9763
- await tx.execute(sql`
9764
- CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
9765
- SELECT COALESCE(
9766
- NULLIF(current_setting('app.uid', true), ''),
9767
- NULLIF(current_setting('app.user_id', true), '')
9768
- );
9769
- $$ LANGUAGE sql STABLE
9770
- `);
9771
- await tx.execute(sql`
9772
- CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb AS $$
9773
- SELECT COALESCE(
9774
- NULLIF(current_setting('app.jwt', true), ''),
9775
- '{}'
9776
- )::jsonb;
9777
- $$ LANGUAGE sql STABLE
9778
- `);
9779
- await tx.execute(sql`
9780
- CREATE OR REPLACE FUNCTION auth.roles() RETURNS text AS $$
9781
- SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
9782
- $$ LANGUAGE sql STABLE
9783
- `);
10290
+ for (const statement of RLS_BOOTSTRAP_STATEMENTS) await tx.execute(sql.raw(statement));
9784
10291
  });
9785
- for (const columnDef of [
9786
- "display_name TEXT",
9787
- "photo_url TEXT",
9788
- "roles TEXT[] DEFAULT '{}' NOT NULL",
9789
- "password_hash TEXT",
9790
- "email_verified BOOLEAN DEFAULT FALSE NOT NULL",
9791
- "email_verification_token TEXT",
9792
- "email_verification_sent_at TIMESTAMP WITH TIME ZONE",
9793
- "is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
9794
- "metadata JSONB DEFAULT '{}' NOT NULL",
9795
- "tokens_valid_after TIMESTAMP WITH TIME ZONE",
9796
- "created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
9797
- "updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
9798
- ]) await db.execute(sql`
10292
+ for (const spec of AUTH_USERS_COLUMNS) {
10293
+ if (spec.column === "email") continue;
10294
+ await db.execute(sql`
9799
10295
  ALTER TABLE ${sql.raw(usersTableName)}
9800
- ADD COLUMN IF NOT EXISTS ${sql.raw(columnDef)}
10296
+ ADD COLUMN IF NOT EXISTS ${sql.raw(`${spec.column} ${authUsersColumnSql(spec)}`)}
9801
10297
  `);
9802
- const usersColumns = await db.execute(sql`
9803
- SELECT column_name, data_type
10298
+ }
10299
+ const usersColumnRows = (await db.execute(sql`
10300
+ SELECT column_name, data_type, is_nullable, column_default
9804
10301
  FROM information_schema.columns
9805
10302
  WHERE table_schema = ${usersSchema} AND table_name = ${resolvedTable}
9806
- `);
9807
- const usersColumnTypes = new Map(usersColumns.rows.map((row) => [row.column_name, row.data_type]));
10303
+ `)).rows;
10304
+ const usersColumnTypes = new Map(usersColumnRows.map((row) => [row.column_name, row.data_type]));
10305
+ const usersColumnState = new Map(usersColumnRows.map((row) => [row.column_name, row]));
10306
+ for (const spec of AUTH_USERS_COLUMNS) {
10307
+ const state = usersColumnState.get(spec.column);
10308
+ if (!state) continue;
10309
+ if (spec.default !== void 0 && state.column_default === null) {
10310
+ await db.execute(sql`
10311
+ ALTER TABLE ${sql.raw(usersTableName)}
10312
+ ALTER COLUMN ${sql.raw(`"${spec.column}"`)} SET DEFAULT ${sql.raw(spec.default)}
10313
+ `);
10314
+ logger.info(`🔧 Restored the default on ${usersTableName}.${spec.column}`);
10315
+ }
10316
+ if (!spec.notNull || state.is_nullable !== "YES") continue;
10317
+ if (spec.default !== void 0) await db.execute(sql`
10318
+ UPDATE ${sql.raw(usersTableName)}
10319
+ SET ${sql.raw(`"${spec.column}"`)} = ${sql.raw(spec.default)}
10320
+ WHERE ${sql.raw(`"${spec.column}"`)} IS NULL
10321
+ `);
10322
+ try {
10323
+ await db.execute(sql`
10324
+ ALTER TABLE ${sql.raw(usersTableName)}
10325
+ ALTER COLUMN ${sql.raw(`"${spec.column}"`)} SET NOT NULL
10326
+ `);
10327
+ logger.info(`🔧 Restored NOT NULL on ${usersTableName}.${spec.column}`);
10328
+ } catch (err) {
10329
+ logger.warn(`⚠️ ${usersTableName}.${spec.column} should be NOT NULL but still holds NULLs, so the constraint was not applied. Fill or remove those rows and restart: ` + (err instanceof Error ? err.message : String(err)));
10330
+ }
10331
+ }
9808
10332
  for (const column of [
9809
10333
  "email",
9810
10334
  "display_name",
@@ -9986,10 +10510,12 @@ async function ensureAuthTablesExist(db, collection) {
9986
10510
  [authSchema, "user_identities"],
9987
10511
  [authSchema, "refresh_tokens"],
9988
10512
  [authSchema, "password_reset_tokens"],
10513
+ [authSchema, "magic_link_tokens"],
9989
10514
  [authSchema, "app_config"],
9990
10515
  [authSchema, "mfa_factors"],
9991
10516
  [authSchema, "mfa_challenges"],
9992
- [authSchema, "recovery_codes"]
10517
+ [authSchema, "recovery_codes"],
10518
+ [authSchema, "schema_meta"]
9993
10519
  ];
9994
10520
  for (const [schemaName, tableName] of authTablePairs) if ((await db.execute(sql`
9995
10521
  SELECT 1
@@ -10009,6 +10535,9 @@ async function ensureAuthTablesExist(db, collection) {
10009
10535
  logger.warn(`⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`);
10010
10536
  }
10011
10537
  await stampAuthSchemaVersion(db, authSchema);
10538
+ await revokeInternalTableAccess(async (text) => {
10539
+ await db.execute(sql.raw(text));
10540
+ }, authSchema, { onError: (table, err) => logger.warn(`🔐 Could not revoke authenticated-role access to "${authSchema}"."${table}": ` + (err instanceof Error ? err.message : String(err))) });
10012
10541
  logger.info("✅ Auth tables ready");
10013
10542
  } catch (error) {
10014
10543
  if (error instanceof AuthSchemaVersionError) throw error;
@@ -11337,6 +11866,7 @@ async function ensureHistoryTableExists(db) {
11337
11866
  CREATE INDEX IF NOT EXISTS idx_history_time
11338
11867
  ON rebase.entity_history(table_name, entity_id, updated_at DESC)
11339
11868
  `);
11869
+ await db.execute(sql.raw(revokeInternalTableSql("rebase", "entity_history")));
11340
11870
  logger.info("✅ Entity history table ready");
11341
11871
  } catch (error) {
11342
11872
  logger.error("❌ Failed to create row history table", { error });
@@ -12058,12 +12588,12 @@ function createPostgresBootstrapper(pgConfig) {
12058
12588
  const runSql = async (text) => {
12059
12589
  return (await schemaAwareDb.execute(sql.raw(text))).rows ?? [];
12060
12590
  };
12591
+ await warnOnRoleSchemaCollision(runSql);
12061
12592
  const posture = await detectConnectionPosture(runSql);
12062
12593
  if (posture.privileged) {
12063
12594
  await ensureAppRole(runSql, [
12064
12595
  "public",
12065
12596
  "rebase",
12066
- "auth",
12067
12597
  ...registry.getCollections().map((c) => c.schema).filter((s) => typeof s === "string")
12068
12598
  ]);
12069
12599
  driver.rlsUserRole = REBASE_USER_ROLE;
@@ -12073,6 +12603,7 @@ function createPostgresBootstrapper(pgConfig) {
12073
12603
  } else logger.info(`🔐 RLS enforcement: connection role "${posture.role}" is subject to RLS natively; no role switch needed.`);
12074
12604
  await validatePolicyPgRoles(runSql, registry.getCollections(), driver.rlsUserRole ?? posture.role);
12075
12605
  warnOnAnonymousGrants(registry.getCollections());
12606
+ warnOnLegacyRlsFunctions(registry.getCollections());
12076
12607
  }
12077
12608
  if (driver.branchService) try {
12078
12609
  await driver.branchService.ensureBranchMetadataTable();
@@ -12288,7 +12819,7 @@ function createPostgresBootstrapper(pgConfig) {
12288
12819
  */
12289
12820
  async ensureCollectionSchema(collections, driverResult, log) {
12290
12821
  const internals = driverResult.internals;
12291
- const { ensureCollectionTables } = await import("./ensure-collection-tables-CBQdOETu.js");
12822
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-DRxaUG96.js");
12292
12823
  const plan = await ensureCollectionTables({ async query(text) {
12293
12824
  const result = await internals.db.execute(sql.raw(text));
12294
12825
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
@@ -12313,13 +12844,24 @@ function createPostgresBootstrapper(pgConfig) {
12313
12844
  */
12314
12845
  async ensureCollectionPolicies(collections, driverResult, log) {
12315
12846
  const internals = driverResult.internals;
12316
- const { ensureCollectionPolicies } = await import("./ensure-collection-policies-ViG8XiPn.js");
12847
+ const { ensureCollectionPolicies } = await import("./ensure-collection-policies-CwYUliAa.js");
12317
12848
  const outcome = await ensureCollectionPolicies({ async query(text) {
12318
12849
  const result = await internals.db.execute(sql.raw(text));
12319
12850
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
12320
12851
  } }, collections, log);
12321
12852
  for (const skip of outcome.skipped) logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
12322
12853
  for (const failure of outcome.failures) logger.warn(`🔐 [rls] Could not fully apply policies to "${failure.table}" — it stays locked (denies) until this is resolved: ${failure.error}`);
12854
+ try {
12855
+ const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-Bpv3nUZo.js").then((n) => n.n);
12856
+ await dropLegacyAuthSchema(async (text) => {
12857
+ return (await internals.db.execute(sql.raw(text))).rows ?? [];
12858
+ }, {
12859
+ info: (m) => logger.info(m),
12860
+ warn: (m) => logger.warn(m)
12861
+ });
12862
+ } catch (err) {
12863
+ logger.info("Left the legacy `auth` schema in place: " + (err instanceof Error ? err.message : String(err)));
12864
+ }
12323
12865
  return { applied: outcome.policiesApplied };
12324
12866
  },
12325
12867
  getAdmin(driverResult) {
@@ -12327,7 +12869,7 @@ function createPostgresBootstrapper(pgConfig) {
12327
12869
  },
12328
12870
  mountRoutes(app, basePath, driverResult) {},
12329
12871
  async initializeWebsockets(server, realtimeService, driver, config, adapter) {
12330
- const { createPostgresWebSocket } = await import("./websocket-B2LsrINK.js").then((n) => n.n);
12872
+ const { createPostgresWebSocket } = await import("./websocket-D0TBU3ia.js").then((n) => n.n);
12331
12873
  createPostgresWebSocket(server, realtimeService, driver, config, adapter);
12332
12874
  }
12333
12875
  };