@rebasepro/server-postgres 0.14.0 → 0.14.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.
Files changed (43) hide show
  1. package/dist/PostgresBackendDriver.d.ts +1 -1
  2. package/dist/PostgresBootstrapper.d.ts +19 -0
  3. package/dist/auth/services.d.ts +10 -0
  4. package/dist/{auth-users-columns-BfQHf9JE.js → auth-users-columns-C-FDnL_e.js} +245 -15
  5. package/dist/auth-users-columns-C-FDnL_e.js.map +1 -0
  6. package/dist/data_driver-ULAyJEi9.js.map +1 -1
  7. package/dist/{ensure-collection-policies-8vuu-n4r.js → ensure-collection-policies-DoHwhVf8.js} +3 -3
  8. package/dist/{ensure-collection-policies-8vuu-n4r.js.map → ensure-collection-policies-DoHwhVf8.js.map} +1 -1
  9. package/dist/{ensure-collection-tables-CbvaGuVn.js → ensure-collection-tables-DT2eq859.js} +45 -5
  10. package/dist/{ensure-collection-tables-CbvaGuVn.js.map → ensure-collection-tables-DT2eq859.js.map} +1 -1
  11. package/dist/index.es.js +543 -105
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/{rls-enforcement-BJ_3wxwg.js → rls-enforcement-gUNDfm7l.js} +2 -2
  14. package/dist/{rls-enforcement-BJ_3wxwg.js.map → rls-enforcement-gUNDfm7l.js.map} +1 -1
  15. package/dist/schema/drizzle-ddl.d.ts +9 -0
  16. package/dist/services/FetchService.d.ts +81 -5
  17. package/dist/services/RelationService.d.ts +3 -3
  18. package/dist/services/channel-presence.d.ts +16 -1
  19. package/dist/services/dataService.d.ts +6 -4
  20. package/dist/services/realtimeService.d.ts +54 -10
  21. package/dist/src-DCdn3Val.js.map +1 -1
  22. package/dist/utils/drizzle-conditions.d.ts +25 -0
  23. package/dist/{websocket-C8ZqVBiV.js → websocket-D2jXv0Ds.js} +29 -2
  24. package/dist/websocket-D2jXv0Ds.js.map +1 -0
  25. package/package.json +6 -6
  26. package/src/PostgresBackendDriver.ts +7 -3
  27. package/src/PostgresBootstrapper.ts +105 -41
  28. package/src/auth/services.ts +26 -5
  29. package/src/schema/drizzle-ddl.ts +33 -0
  30. package/src/schema/ensure-collection-tables.test.ts +99 -0
  31. package/src/schema/ensure-collection-tables.ts +65 -3
  32. package/src/schema/generate-drizzle-schema-logic.ts +19 -1
  33. package/src/services/FetchService.ts +310 -63
  34. package/src/services/RelationService.ts +3 -3
  35. package/src/services/channel-history.ts +38 -6
  36. package/src/services/channel-presence.ts +31 -7
  37. package/src/services/dataService.ts +6 -4
  38. package/src/services/pg-notify-listener.ts +14 -0
  39. package/src/services/realtimeService.ts +161 -41
  40. package/src/utils/drizzle-conditions.ts +155 -5
  41. package/src/websocket.ts +44 -1
  42. package/dist/auth-users-columns-BfQHf9JE.js.map +0 -1
  43. package/dist/websocket-C8ZqVBiV.js.map +0 -1
package/dist/index.es.js CHANGED
@@ -3,16 +3,16 @@ 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
5
  import { n as resolveClientListLimit, t as ListLimitError } from "./data_driver-ULAyJEi9.js";
6
- import { A as getEnumVarName, B as createRelationRefWithData, C as getEffectiveSecurityRules, D as fieldKeyForColumn, F as getDeclaredPrimaryKeys, G as legacyForeignKeyName, H as updateDateAutoValues, I as isAddressableId, J as getPolicyNamesForRule, L as parseIdValues, M as getTableVarName, N as resolveCollectionRelations, O as findRelation, P as buildCompositeId, Q as toSnakeCase, R as sortCollectionsBySlug, S as resolveJunctionSpecs, T as securityRuleToConditions, U as firstFreeKey, V as normalizeToEntityRelation, W as generateForeignKeyName, X as mergeDeep, Y as isPrototypePollutingKey, Z as camelCase, _ as CollectionRegistry, b as getJunctionCollectionConfig, g as buildSdkData, h as visibleColumnProjection, j as getTableName$1, k as getColumnName, l as buildSearchColumnSpec, nt as isManyToMany, q as toWireKey, r as authUsersColumnSql, rt as Vector, s as SEARCH_UNACCENT_FN, t as AUTH_USERS_COLUMNS, tt as hasForeignKeyOnTarget, u as hiddenColumnsOption, v as relationalCollections, w as policyToPostgres, x as getJunctionSecurityRules, y as resolveStringColumnLength, z as createRelationRef } from "./auth-users-columns-BfQHf9JE.js";
6
+ import { $ as mergeDeep, A as fieldKeyForColumn, B as parseIdValues, C as getJunctionCollectionConfig, D as policyToPostgres, E as getEffectiveSecurityRules, F as getTableVarName, G as updateDateAutoValues, H as createRelationRef, I as resolveCollectionRelations, J as legacyForeignKeyName, K as firstFreeKey, L as buildCompositeId, M as getColumnName, N as getEnumVarName, O as securityRuleToConditions, P as getTableName$1, Q as isPrototypePollutingKey, R as getDeclaredPrimaryKeys, S as resolveStringColumnLength, T as resolveJunctionSpecs, U as createRelationRefWithData, V as sortCollectionsBySlug, W as normalizeToEntityRelation, X as toWireKey, Z as getPolicyNamesForRule, _ as OrderBySpecError, at as isManyToMany, b as CollectionRegistry, et as camelCase, g as buildSdkData, h as visibleColumnProjection, it as hasForeignKeyOnTarget, j as findRelation, l as buildSearchColumnSpec, ot as Vector, q as generateForeignKeyName, r as authUsersColumnSql, s as SEARCH_UNACCENT_FN, t as AUTH_USERS_COLUMNS, tt as toSnakeCase, u as hiddenColumnsOption, v as normalizeDriverOrderBy, w as getJunctionSecurityRules, x as relationalCollections, y as parseOrderBySpecStrict, z as isAddressableId } from "./auth-users-columns-C-FDnL_e.js";
7
7
  import { c as isPostgresCollectionConfig, f as ALL_WHERE_FILTER_OPS, l as isRelationalCollectionConfig } from "./src-DCdn3Val.js";
8
- import { t as createPostgresWebSocket } from "./websocket-C8ZqVBiV.js";
9
- import { a as warnOnAnonymousGrants, c as REBASE_USER_ROLE, i as validatePolicyPgRoles, l as revokeInternalTableAccess, n as detectConnectionPosture, o as warnOnLegacyRlsFunctions, r as ensureAppRole, s as warnOnRoleSchemaCollision, t as applyAuthContext, u as revokeInternalTableSql } from "./rls-enforcement-BJ_3wxwg.js";
8
+ import { t as createPostgresWebSocket } from "./websocket-D2jXv0Ds.js";
9
+ import { a as warnOnAnonymousGrants, c as REBASE_USER_ROLE, i as validatePolicyPgRoles, l as revokeInternalTableAccess, n as detectConnectionPosture, o as warnOnLegacyRlsFunctions, r as ensureAppRole, s as warnOnRoleSchemaCollision, t as applyAuthContext, u as revokeInternalTableSql } from "./rls-enforcement-gUNDfm7l.js";
10
10
  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-BH0Dzo_h.js";
11
11
  import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-69hYT8nr.js";
12
12
  import { Client, Pool } from "pg";
13
13
  import { drizzle } from "drizzle-orm/node-postgres";
14
- import { ApiError, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
15
- import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, isTable, lt, notInArray, or, relations, sql } from "drizzle-orm";
14
+ import { ApiError, createDdlBootstrapper, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
15
+ import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, isNotNull, isNull, isTable, lt, notInArray, or, relations, sql } from "drizzle-orm";
16
16
  import { PgArray, PgTable, bigint, boolean, char, cidr, customType, date, doublePrecision, geometry, getTableConfig, index, inet, integer, interval, json, jsonb, line, macaddr, macaddr8, numeric, pgSchema, pgTable, point, primaryKey, real, smallint, text, time, timestamp, unique, uuid, varchar, vector } from "drizzle-orm/pg-core";
17
17
  import fs, { promises } from "fs";
18
18
  import path from "path";
@@ -602,6 +602,31 @@ function getUnknownFilterFieldsMode() {
602
602
  return defaultUnknownFilterFieldsMode;
603
603
  }
604
604
  /**
605
+ * Split `metadata->address->>city` into its column and its path.
606
+ *
607
+ * The arrows are PostgREST's spelling and Postgres's own, so the filter reads
608
+ * the same as the SQL it becomes — and, more usefully, the same as what someone
609
+ * would have written by hand in the SQL console while working out what to ask
610
+ * for. A field with no arrow is not a JSON path and returns `undefined`, which
611
+ * leaves every existing filter on exactly the path it took before.
612
+ *
613
+ * Both arrows are accepted and mean the same thing here: the extraction is
614
+ * always compiled to `->>` (text) at the leaf, because that is the only form a
615
+ * comparison can be made against. `->` is allowed because people write it out
616
+ * of habit, and refusing it would be pedantry about a distinction this layer
617
+ * erases anyway.
618
+ */
619
+ function parseJsonFieldPath(field) {
620
+ if (!field.includes("->")) return void 0;
621
+ const segments = field.split(/->>?/).map((s) => s.trim()).filter(Boolean);
622
+ if (segments.length < 2) return void 0;
623
+ const [columnKey, ...path] = segments;
624
+ return {
625
+ columnKey,
626
+ path
627
+ };
628
+ }
629
+ /**
605
630
  * Filter values may arrive as relation wire objects — `EntityRelation`
606
631
  * instances or their JSON form `{ __type: "relation", id, path }` — e.g. when
607
632
  * the admin filters a relation column. SQL comparisons need the raw id, so
@@ -762,6 +787,19 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
762
787
  kind: "column",
763
788
  column: direct
764
789
  };
790
+ const jsonPath = parseJsonFieldPath(field);
791
+ if (jsonPath) {
792
+ const base = columnAt(jsonPath.columnKey);
793
+ if (base) {
794
+ const meta = getColumnMeta(base);
795
+ if (meta.dataType !== "json" && meta.columnType !== "PgJsonb" && meta.columnType !== "PgJson") throw ApiError.badRequest(`Cannot filter inside "${jsonPath.columnKey}" — it is not a json or jsonb column.`, "INVALID_FILTER_FIELD");
796
+ return {
797
+ kind: "json",
798
+ column: base,
799
+ path: jsonPath.path
800
+ };
801
+ }
802
+ }
765
803
  if (collection) {
766
804
  const relation = resolveCollectionRelations(collection)[field];
767
805
  if (relation?.kind === "belongsTo") {
@@ -841,7 +879,71 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
841
879
  }
842
880
  /** Dispatch a resolved filter field onto the shape it actually compiles to. */
843
881
  static compileFilterTarget(target, op, value, field, collectionPath) {
844
- return target.kind === "column" ? this.buildSingleFilterCondition(target.column, op, value) : this.buildRelationFilterCondition(target.relation, op, value, target.sourceIdColumn, target.registry, field, collectionPath);
882
+ if (target.kind === "column") return this.buildSingleFilterCondition(target.column, op, value);
883
+ if (target.kind === "json") return this.buildJsonPathCondition(target.column, target.path, op, value);
884
+ return this.buildRelationFilterCondition(target.relation, op, value, target.sourceIdColumn, target.registry, field, collectionPath);
885
+ }
886
+ /**
887
+ * A comparison against a value extracted from a json/jsonb column.
888
+ *
889
+ * The path is walked with `->` and the leaf taken with `->>`, so what comes
890
+ * out is always **text**. That is the whole of the type story, and it is
891
+ * the part worth being explicit about, because the alternatives are all
892
+ * worse:
893
+ *
894
+ * - text comparison alone makes `["<", 100]` compare lexically, where
895
+ * `"9"` is greater than `"100"`;
896
+ * - casting unconditionally makes every filter on a non-numeric value a
897
+ * runtime `invalid input syntax for type numeric` — a 500 on a row whose
898
+ * JSON simply holds a string.
899
+ *
900
+ * So the *filter value* decides. A number on an ordering comparison casts
901
+ * both sides to numeric; everything else compares as text, with booleans
902
+ * rendered the way `->>` renders them (`"true"` / `"false"`). A row whose
903
+ * JSON holds a non-numeric value at a path being compared numerically is
904
+ * excluded rather than fatal, which is what `IS NOT NULL`-style filtering
905
+ * means everywhere else in this file.
906
+ *
907
+ * The path segments are bound as parameters, never interpolated: they come
908
+ * from a query string, and `->>` takes a text parameter perfectly well.
909
+ */
910
+ static buildJsonPathCondition(column, path, op, value) {
911
+ let expr = sql`${column}`;
912
+ for (const key of path.slice(0, -1)) expr = sql`${expr} -> ${key}`;
913
+ const leaf = sql`${expr} ->> ${path[path.length - 1]}`;
914
+ if (typeof value === "number" && (op === ">" || op === ">=" || op === "<" || op === "<=")) {
915
+ const numeric = sql`CASE WHEN ${leaf} ~ '^-?[0-9]+(\\.[0-9]+)?$' THEN (${leaf})::numeric END`;
916
+ switch (op) {
917
+ case ">": return sql`${numeric} > ${value}`;
918
+ case ">=": return sql`${numeric} >= ${value}`;
919
+ case "<": return sql`${numeric} < ${value}`;
920
+ case "<=": return sql`${numeric} <= ${value}`;
921
+ }
922
+ }
923
+ const asText = (v) => typeof v === "boolean" ? String(v) : String(v);
924
+ switch (op) {
925
+ case "==": return value === null || value === void 0 ? sql`${leaf} IS NULL` : sql`${leaf} = ${asText(value)}`;
926
+ case "!=": return value === null || value === void 0 ? sql`${leaf} IS NOT NULL` : sql`${leaf} != ${asText(value)}`;
927
+ case ">": return sql`${leaf} > ${asText(value)}`;
928
+ case ">=": return sql`${leaf} >= ${asText(value)}`;
929
+ case "<": return sql`${leaf} < ${asText(value)}`;
930
+ case "<=": return sql`${leaf} <= ${asText(value)}`;
931
+ case "like": return sql`${leaf} LIKE ${asText(value)}`;
932
+ case "ilike": return sql`${leaf} ILIKE ${asText(value)}`;
933
+ case "not-like": return sql`${leaf} NOT LIKE ${asText(value)}`;
934
+ case "not-ilike": return sql`${leaf} NOT ILIKE ${asText(value)}`;
935
+ case "is-null": return sql`${leaf} IS NULL`;
936
+ case "is-not-null": return sql`${leaf} IS NOT NULL`;
937
+ case "in":
938
+ case "not-in": {
939
+ if (value === null || value === void 0) return op === "in" ? sql`${leaf} IS NULL` : sql`${leaf} IS NOT NULL`;
940
+ const values = toMembershipList(value).map(asText);
941
+ if (values.length === 0) return op === "in" ? sql`FALSE` : sql`TRUE`;
942
+ const list = sql.join(values.map((v) => sql`${v}`), sql`, `);
943
+ return op === "in" ? sql`${leaf} IN (${list})` : sql`${leaf} NOT IN (${list})`;
944
+ }
945
+ default: throw ApiError.badRequest(`Operator "${op}" is not supported on a JSON path. Use it on the column itself.`, "INVALID_FILTER_OPERATOR");
946
+ }
845
947
  }
846
948
  /**
847
949
  * A filter on a relation that owns no column on this row — `EXISTS` over
@@ -3273,6 +3375,43 @@ var FetchService = class FetchService {
3273
3375
  }
3274
3376
  return this.resolveOrderByField(table, orderBy, collection);
3275
3377
  }
3378
+ /**
3379
+ * Resolve every sort key to the expression it orders by, in order of
3380
+ * significance.
3381
+ *
3382
+ * A key that resolves to nothing is dropped rather than skipping the rest:
3383
+ * `resolveOrderByField` only *returns* undefined under the lenient
3384
+ * unknown-field mode, where dropping is the configured answer, and dropping
3385
+ * one key of several still honours the ones that did resolve.
3386
+ */
3387
+ resolveOrderKeys(table, keys, collection, searchString) {
3388
+ const resolved = [];
3389
+ for (const [field, direction] of keys) {
3390
+ const target = this.resolveOrderTarget(table, field, collection, searchString);
3391
+ if (target) resolved.push({
3392
+ field,
3393
+ direction,
3394
+ target
3395
+ });
3396
+ }
3397
+ return resolved;
3398
+ }
3399
+ /**
3400
+ * The full `ORDER BY`: the caller's keys, then the id.
3401
+ *
3402
+ * The id is always last and always descending. It is not decoration — it is
3403
+ * what makes the ordering *total*, and a cursor over a non-total order
3404
+ * repeats and skips rows among the ties. Every keyset comparison built by
3405
+ * {@link buildCursorConditions} ends on the same `id DESC`, and the two have
3406
+ * to agree: they did not, and an ascending sort paged with `id >` against an
3407
+ * `ORDER BY … , id DESC`, so rows sharing a sort value were dropped from
3408
+ * every page after the first.
3409
+ */
3410
+ buildOrderExpressions(keys, idField) {
3411
+ const expressions = keys.map(({ direction, target }) => direction === "asc" ? asc(target) : desc(target));
3412
+ expressions.push(desc(idField));
3413
+ return expressions;
3414
+ }
3276
3415
  resolveOrderByField(table, orderBy, collection) {
3277
3416
  const columnAt = (key) => (key in table ? table[key] : void 0) || void 0;
3278
3417
  const direct = columnAt(orderBy);
@@ -3434,14 +3573,9 @@ var FetchService = class FetchService {
3434
3573
  if (cursorConditions.length > 0) allConditions.push(...cursorConditions);
3435
3574
  }
3436
3575
  if (allConditions.length > 0) queryOpts.where = and(...allConditions);
3437
- const orderExpressions = [];
3438
- if (options.orderBy) {
3439
- const collection = getCollectionByPath(collectionPath, this.registry);
3440
- const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3441
- if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3442
- }
3443
- orderExpressions.push(desc(idField));
3444
- if (orderExpressions.length > 0) queryOpts.orderBy = orderExpressions;
3576
+ const orderKeys = normalizeDriverOrderBy(options.orderBy, options.order);
3577
+ const resolvedOrder = orderKeys ? this.resolveOrderKeys(table, orderKeys, getCollectionByPath(collectionPath, this.registry), options.searchString) : [];
3578
+ queryOpts.orderBy = this.buildOrderExpressions(resolvedOrder, idField);
3445
3579
  const limitValue = options.searchString ? options.limit || 50 : options.limit;
3446
3580
  if (limitValue) queryOpts.limit = limitValue;
3447
3581
  if (options.offset && options.offset > 0) queryOpts.offset = options.offset;
@@ -3449,19 +3583,25 @@ var FetchService = class FetchService {
3449
3583
  }
3450
3584
  /**
3451
3585
  * Extract cursor pagination conditions from startAfter options.
3586
+ *
3587
+ * "Every row that sorts after this one", written out as a comparison over
3588
+ * the same keys the `ORDER BY` uses and ending on the same `id DESC`. With
3589
+ * one key that is the familiar `k > v OR (k = v AND id < cursorId)`; with
3590
+ * several it nests, each key's tie handing the decision to the next.
3452
3591
  */
3453
3592
  buildCursorConditions(table, idField, idInfo, options, collectionPath) {
3454
3593
  if (!options.startAfter) return [];
3455
3594
  const cursor = options.startAfter;
3456
- if (options.orderBy) {
3457
- 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 });
3595
+ const keys = normalizeDriverOrderBy(options.orderBy, options.order);
3596
+ if (keys) {
3597
+ if (keys.some(([field]) => field === 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 });
3458
3598
  const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
3459
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3460
- if (orderByField) {
3461
- const startAfterOrderValue = cursor.values?.[options.orderBy] ?? cursor[options.orderBy];
3462
- const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
3463
- if (startAfterOrderValue !== void 0 && startAfterId !== void 0) if (options.order === "asc") return [or(gt(orderByField, startAfterOrderValue), and(eq(orderByField, startAfterOrderValue), gt(idField, startAfterId)))];
3464
- else return [or(lt(orderByField, startAfterOrderValue), and(eq(orderByField, startAfterOrderValue), lt(idField, startAfterId)))];
3599
+ const resolved = this.resolveOrderKeys(table, keys, collection);
3600
+ const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
3601
+ if (resolved.length > 0 && startAfterId !== void 0) {
3602
+ const cursorValues = cursor.values;
3603
+ const values = resolved.map(({ field }) => cursorValues && field in cursorValues ? cursorValues[field] : cursor[field]);
3604
+ if (values.every((value) => value !== void 0)) return [this.buildKeysetComparison(resolved, values, idField, startAfterId)];
3465
3605
  }
3466
3606
  } else {
3467
3607
  const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
@@ -3470,6 +3610,28 @@ var FetchService = class FetchService {
3470
3610
  return [];
3471
3611
  }
3472
3612
  /**
3613
+ * "Sorts strictly after the cursor row", over `keys` and then the id.
3614
+ *
3615
+ * Built by recursion rather than as a row-value comparison — `(a, b) > (x, y)`
3616
+ * would be shorter, but it is only correct when every key runs the same
3617
+ * direction, and `roles ASC, created_at DESC` is exactly the case this
3618
+ * exists to serve.
3619
+ *
3620
+ * NULLs are compared by the rule Postgres sorts them under (last ascending,
3621
+ * first descending) rather than by `>`/`<`, which answer *unknown* against
3622
+ * NULL and therefore match nothing. Ordering by a nullable column and paging
3623
+ * used to drop every row whose sort value was NULL from page two onward.
3624
+ */
3625
+ buildKeysetComparison(keys, values, idField, cursorId, index = 0) {
3626
+ if (index >= keys.length) return lt(idField, cursorId);
3627
+ const { direction } = keys[index];
3628
+ const target = keys[index].target;
3629
+ const value = values[index];
3630
+ const rest = this.buildKeysetComparison(keys, values, idField, cursorId, index + 1);
3631
+ if (value === null) return direction === "asc" ? and(isNull(target), rest) : or(isNotNull(target), and(isNull(target), rest));
3632
+ return direction === "asc" ? or(gt(target, value), isNull(target), and(eq(target, value), rest)) : or(lt(target, value), and(eq(target, value), rest));
3633
+ }
3634
+ /**
3473
3635
  * Compile "rows reachable from this parent" into a `WHERE` condition on the
3474
3636
  * target table, so a nested listing can run as an ordinary collection query.
3475
3637
  */
@@ -3632,14 +3794,8 @@ var FetchService = class FetchService {
3632
3794
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
3633
3795
  if (finalCondition) query = query.where(finalCondition);
3634
3796
  }
3635
- const orderExpressions = [];
3636
- if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
3637
- else if (options.orderBy) {
3638
- const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3639
- if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3640
- }
3641
- orderExpressions.push(desc(idField));
3642
- if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
3797
+ const orderExpressions = vectorMeta ? [asc(vectorMeta.orderBy), desc(idField)] : this.buildOrderExpressions(this.resolveOrderKeys(table, normalizeDriverOrderBy(options.orderBy, options.order) ?? [], collection, options.searchString), idField);
3798
+ query = query.orderBy(...orderExpressions);
3643
3799
  if (options.startAfter) {
3644
3800
  const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
3645
3801
  if (cursorConditions.length > 0) {
@@ -3764,6 +3920,10 @@ var FetchService = class FetchService {
3764
3920
  const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, effectivePath, this.filterContext(effectivePath, table));
3765
3921
  if (logicalCondition) allConditions.push(logicalCondition);
3766
3922
  }
3923
+ if (options.vectorSearch) {
3924
+ const vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
3925
+ if (vectorMeta.filter) allConditions.push(vectorMeta.filter);
3926
+ }
3767
3927
  if (allConditions.length > 0) {
3768
3928
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
3769
3929
  if (finalCondition) query = query.where(finalCondition);
@@ -3772,6 +3932,92 @@ var FetchService = class FetchService {
3772
3932
  return Number(result[0]?.count || 0);
3773
3933
  }
3774
3934
  /**
3935
+ * `count`/`sum`/`avg`/`min`/`max`, optionally grouped.
3936
+ *
3937
+ * The gap this fills is narrow and constant: every dashboard wants "revenue
3938
+ * by status" and "orders per day", and without it the options were a custom
3939
+ * function holding hand-written SQL, or fetching every row and reducing in
3940
+ * JavaScript — which is wrong at any size that matters, and silently wrong
3941
+ * under a `limit`.
3942
+ *
3943
+ * It runs through the same request-scoped handle as every other read, so
3944
+ * **RLS applies to the rows being aggregated**. That is the property worth
3945
+ * protecting here: an aggregate is an effective way to read data you cannot
3946
+ * select, and `count(*)` over a table whose policies would return nothing
3947
+ * has to be zero.
3948
+ */
3949
+ async aggregate(collectionPath, options) {
3950
+ const collection = getCollectionByPath(collectionPath, this.registry);
3951
+ const table = getTableForCollection(collection, this.registry);
3952
+ const columns = getTableColumns(table);
3953
+ const columnFor = (field, forWhat) => {
3954
+ const column = columns[field];
3955
+ if (!column) throw ApiError.badRequest(`Unknown field '${field}' in ${forWhat}. Valid fields: ${Object.keys(columns).sort().join(", ")}`, "UNKNOWN_AGGREGATE_FIELD");
3956
+ return column;
3957
+ };
3958
+ const selection = {};
3959
+ for (const aggregate of options.aggregates) {
3960
+ if (aggregate.fn === "count" && !aggregate.field) {
3961
+ selection[aggregate.alias] = sql`count(*)`;
3962
+ continue;
3963
+ }
3964
+ const column = columnFor(aggregate.field, `${aggregate.fn}()`);
3965
+ switch (aggregate.fn) {
3966
+ case "count":
3967
+ selection[aggregate.alias] = sql`count(${column})`;
3968
+ break;
3969
+ case "sum":
3970
+ selection[aggregate.alias] = sql`sum(${column})::numeric`;
3971
+ break;
3972
+ case "avg":
3973
+ selection[aggregate.alias] = sql`avg(${column})::numeric`;
3974
+ break;
3975
+ case "min":
3976
+ selection[aggregate.alias] = sql`min(${column})`;
3977
+ break;
3978
+ case "max":
3979
+ selection[aggregate.alias] = sql`max(${column})`;
3980
+ break;
3981
+ }
3982
+ }
3983
+ const groupColumns = (options.groupBy ?? []).map((field) => ({
3984
+ field,
3985
+ column: columnFor(field, "groupBy")
3986
+ }));
3987
+ for (const group of groupColumns) selection[group.field] = sql`${group.column}`;
3988
+ let query = this.db.select(selection).from(table).$dynamic();
3989
+ const conditions = [];
3990
+ if (options.searchString) {
3991
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3992
+ if (searchConditions.length === 0) return [];
3993
+ conditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
3994
+ }
3995
+ if (options.filter) conditions.push(...this.buildFilterConditions(options.filter, table, collectionPath));
3996
+ if (options.logical) {
3997
+ const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
3998
+ if (logicalCondition) conditions.push(logicalCondition);
3999
+ }
4000
+ if (conditions.length > 0) {
4001
+ const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(conditions);
4002
+ if (finalCondition) query = query.where(finalCondition);
4003
+ }
4004
+ if (groupColumns.length > 0) {
4005
+ query = query.groupBy(...groupColumns.map((g) => g.column));
4006
+ if (options.limit) query = query.limit(options.limit);
4007
+ }
4008
+ const rows = await query;
4009
+ const numericAliases = new Set(options.aggregates.filter((a) => a.fn === "count" || a.fn === "sum" || a.fn === "avg").map((a) => a.alias));
4010
+ return rows.map((row) => {
4011
+ const out = { ...row };
4012
+ for (const alias of numericAliases) {
4013
+ if (out[alias] === null || out[alias] === void 0) continue;
4014
+ const parsed = Number(out[alias]);
4015
+ if (!Number.isNaN(parsed)) out[alias] = parsed;
4016
+ }
4017
+ return out;
4018
+ });
4019
+ }
4020
+ /**
3775
4021
  * Check if a field value is unique
3776
4022
  */
3777
4023
  async checkUniqueField(collectionPath, fieldName, value, excludeEntityId, _databaseId) {
@@ -3970,14 +4216,8 @@ var FetchService = class FetchService {
3970
4216
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
3971
4217
  if (finalCondition) query = query.where(finalCondition);
3972
4218
  }
3973
- const orderExpressions = [];
3974
- if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
3975
- else if (options.orderBy) {
3976
- const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3977
- if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3978
- }
3979
- orderExpressions.push(desc(idField));
3980
- if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
4219
+ const orderExpressions = vectorMeta ? [asc(vectorMeta.orderBy), desc(idField)] : this.buildOrderExpressions(this.resolveOrderKeys(table, normalizeDriverOrderBy(options.orderBy, options.order) ?? [], collection, options.searchString), idField);
4220
+ query = query.orderBy(...orderExpressions);
3981
4221
  const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
3982
4222
  if (limitValue) query = query.limit(limitValue);
3983
4223
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
@@ -5605,11 +5845,12 @@ var PostgresBackendDriver = class PostgresBackendDriver {
5605
5845
  async checkUniqueField(path, name, value, id, collection) {
5606
5846
  return this.dataService.checkUniqueField(path, name, value, id, collection?.databaseId);
5607
5847
  }
5608
- async count({ path, collection, filter, logical, searchString }) {
5848
+ async count({ path, collection, filter, logical, searchString, vectorSearch }) {
5609
5849
  return this.dataService.count(path, {
5610
5850
  filter,
5611
5851
  logical,
5612
- searchString
5852
+ searchString,
5853
+ vectorSearch
5613
5854
  });
5614
5855
  }
5615
5856
  getTargetDb(databaseName) {
@@ -6464,8 +6705,25 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
6464
6705
  };
6465
6706
  /**
6466
6707
  * Wraps a compiled SQL clause in a Drizzle `sql\`...\`` template literal.
6708
+ *
6709
+ * The clause is SQL being written into a TypeScript file, so it has to survive
6710
+ * being read back as a template literal. Three characters do not:
6711
+ *
6712
+ * - `` ` `` closes the template early, and the rest of the clause becomes code.
6713
+ * - `${` opens an interpolation — the file stops compiling, or worse, compiles
6714
+ * against whatever identifier happens to be in scope.
6715
+ * - `\` is an escape, and Drizzle's `sql` tag reads the *cooked* strings, not
6716
+ * `.raw`. So a policy written as `email ~ '^admin\.user@corp\.com$'` reaches
6717
+ * the database as `^admin.user@corp.com$`, where every `\.` now matches any
6718
+ * character. A `USING` clause is a security boundary and that one silently
6719
+ * widened it — the SQL file emitted by the DDL generator kept the backslashes
6720
+ * while this path dropped them, so the two disagreed about who could read the
6721
+ * table.
6722
+ *
6723
+ * Escaping here rather than in the compiler: the clause is correct SQL, and it
6724
+ * is only this destination that has an opinion about backslashes.
6467
6725
  */
6468
- var wrapSql = (clause) => `sql\`${clause}\``;
6726
+ var wrapSql = (clause) => `sql\`${clause.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}\``;
6469
6727
  var generatePolicyCode = (collection, rule, index, resolveCollection) => {
6470
6728
  const tableName = getTableName$1(collection);
6471
6729
  const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
@@ -7112,8 +7370,10 @@ var PgNotifyListener = class {
7112
7370
  }
7113
7371
  async connect({ initial = false } = {}) {
7114
7372
  const { connectionString, channel, onPayload, logLabel } = this.options;
7373
+ let pending;
7115
7374
  try {
7116
7375
  const client = new Client({ connectionString });
7376
+ pending = client;
7117
7377
  client.on("error", (err) => {
7118
7378
  logger.error(`❌ ${logLabel} LISTEN client error`, { detail: err.message });
7119
7379
  this.scheduleReconnect();
@@ -7131,8 +7391,12 @@ var PgNotifyListener = class {
7131
7391
  await client.connect();
7132
7392
  await client.query(`LISTEN ${channel}`);
7133
7393
  this.client = client;
7394
+ pending = void 0;
7134
7395
  logger.debug(`📡 ${logLabel} Listening on channel "${channel}".`);
7135
7396
  } catch (err) {
7397
+ if (pending) try {
7398
+ await pending.end();
7399
+ } catch {}
7136
7400
  if (initial) throw err;
7137
7401
  logger.error(`❌ ${logLabel} Failed to connect LISTEN client`, { error: err });
7138
7402
  this.scheduleReconnect();
@@ -7229,6 +7493,29 @@ var CdcListener = class {
7229
7493
  }
7230
7494
  };
7231
7495
  //#endregion
7496
+ //#region src/schema/drizzle-ddl.ts
7497
+ /**
7498
+ * The server's DDL bootstrapper, over a Drizzle handle.
7499
+ *
7500
+ * `createDdlBootstrapper` in `@rebasepro/server` wants a plain
7501
+ * `(sql: string) => Promise<rows>`; the driver's internal stores hold a Drizzle
7502
+ * database. This is the adapter between them, and it exists so the retry policy
7503
+ * has exactly one definition. A second copy of the SQLSTATE list living in the
7504
+ * driver is how the two drift apart, and the drift is invisible: both versions
7505
+ * work perfectly on every single-instance deployment.
7506
+ */
7507
+ /**
7508
+ * A {@link DdlBootstrapper} that runs its statements through `db.execute`.
7509
+ *
7510
+ * @param db the Drizzle handle the calling store already holds
7511
+ * @param scope log prefix identifying the caller, e.g. `"channel-presence"`
7512
+ */
7513
+ function drizzleDdlBootstrapper(db, scope) {
7514
+ return createDdlBootstrapper(async (statement) => {
7515
+ return (await db.execute(sql.raw(statement))).rows ?? [];
7516
+ }, scope);
7517
+ }
7518
+ //#endregion
7232
7519
  //#region src/services/channel-history.ts
7233
7520
  /**
7234
7521
  * Ordered, replayable per-channel message history.
@@ -7364,8 +7651,9 @@ var ChannelHistoryStore = class {
7364
7651
  */
7365
7652
  async ensureTables() {
7366
7653
  if (!this.enabled || this.tablesReady) return;
7367
- await this.db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
7368
- await this.db.execute(sql`
7654
+ const ddl = drizzleDdlBootstrapper(this.db, "channel-history");
7655
+ await ddl.ensureObject("rebase schema", "CREATE SCHEMA IF NOT EXISTS rebase");
7656
+ await ddl.ensureObject("channel_messages table", `
7369
7657
  CREATE TABLE IF NOT EXISTS rebase.channel_messages (
7370
7658
  channel TEXT NOT NULL,
7371
7659
  seq BIGINT NOT NULL,
@@ -7376,18 +7664,23 @@ var ChannelHistoryStore = class {
7376
7664
  PRIMARY KEY (channel, seq)
7377
7665
  )
7378
7666
  `);
7379
- await this.db.execute(sql`
7667
+ await ddl.ensureObject("channel_messages created_at index", `
7380
7668
  CREATE INDEX IF NOT EXISTS idx_channel_messages_created
7381
7669
  ON rebase.channel_messages (created_at)
7382
7670
  `);
7383
- await this.db.execute(sql`
7671
+ await ddl.ensureObject("channel_cursors table", `
7384
7672
  CREATE TABLE IF NOT EXISTS rebase.channel_cursors (
7385
7673
  channel TEXT PRIMARY KEY,
7386
7674
  last_seq BIGINT NOT NULL
7387
7675
  )
7388
7676
  `);
7389
- await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_messages")));
7390
- await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_cursors")));
7677
+ const [messagesReady, cursorsReady] = await Promise.all([ddl.isReadable("rebase.channel_messages"), ddl.isReadable("rebase.channel_cursors")]);
7678
+ if (messagesReady) await ddl.step("channel_messages revoke", () => this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_messages"))));
7679
+ if (cursorsReady) await ddl.step("channel_cursors revoke", () => this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_cursors"))));
7680
+ if (!messagesReady || !cursorsReady) {
7681
+ logger.warn("[ChannelHistory] Retained-channel tables are not both present; history is not ready yet.");
7682
+ return;
7683
+ }
7391
7684
  this.tablesReady = true;
7392
7685
  logger.info(`✅ [ChannelHistory] Retained channels ready (${this.rules.length} rule(s)).`);
7393
7686
  }
@@ -7553,11 +7846,27 @@ var ChannelPresenceStore = class {
7553
7846
  this.db = db;
7554
7847
  this.instanceId = instanceId;
7555
7848
  }
7556
- /** Create the roster table. Idempotent. */
7849
+ /**
7850
+ * Create the roster table. Idempotent, and safe to run on every instance at
7851
+ * once.
7852
+ *
7853
+ * Written as separate contained steps rather than one straight sequence for
7854
+ * a reason that only bites with more than one replica, which is exactly the
7855
+ * deployment shape this table exists to serve: `CREATE … IF NOT EXISTS`
7856
+ * reads the catalog and then writes to it non-atomically, so peers booting
7857
+ * together collide, and the loser used to abandon everything after it —
7858
+ * including the trailing `REVOKE`. That revoke is the only thing keeping the
7859
+ * roster off the end-user role, so losing a boot race silently left the
7860
+ * whole channel roster readable by every signed-in user.
7861
+ *
7862
+ * `tablesReady` is now set from a probe of what exists, not from having been
7863
+ * the instance that created it.
7864
+ */
7557
7865
  async ensureTables() {
7558
7866
  if (this.tablesReady) return;
7559
- await this.db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
7560
- await this.db.execute(sql`
7867
+ const ddl = drizzleDdlBootstrapper(this.db, "channel-presence");
7868
+ await ddl.ensureObject("rebase schema", "CREATE SCHEMA IF NOT EXISTS rebase");
7869
+ await ddl.ensureObject("channel_presence table", `
7561
7870
  CREATE TABLE IF NOT EXISTS rebase.channel_presence (
7562
7871
  channel TEXT NOT NULL,
7563
7872
  client_id TEXT NOT NULL,
@@ -7567,12 +7876,14 @@ var ChannelPresenceStore = class {
7567
7876
  PRIMARY KEY (channel, client_id)
7568
7877
  )
7569
7878
  `);
7570
- await this.db.execute(sql`
7879
+ await ddl.ensureObject("channel_presence last_seen index", `
7571
7880
  CREATE INDEX IF NOT EXISTS idx_channel_presence_last_seen
7572
7881
  ON rebase.channel_presence (last_seen)
7573
7882
  `);
7574
- await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence")));
7575
- this.tablesReady = true;
7883
+ if (await ddl.isReadable("rebase.channel_presence")) {
7884
+ await ddl.step("channel_presence revoke", () => this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence"))));
7885
+ this.tablesReady = true;
7886
+ }
7576
7887
  }
7577
7888
  /** Record (or refresh) a client's presence. */
7578
7889
  async track(channel, clientId, state) {
@@ -8098,9 +8409,40 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8098
8409
  get subscriptions() {
8099
8410
  return this._subscriptions;
8100
8411
  }
8412
+ /**
8413
+ * Claim a delivery slot for a subscription, before doing the work.
8414
+ *
8415
+ * Returns the check to run immediately before delivering. It refuses in
8416
+ * three cases, all of which used to deliver:
8417
+ *
8418
+ * - **Out of order.** A newer refetch has already delivered, so this one is
8419
+ * stale — the subscriber would go back to the state before the change.
8420
+ * - **Unsubscribed.** The subscription was cancelled while the fetch was in
8421
+ * flight. The `has(subscriptionId)` check the debounced refetches ran
8422
+ * *before* the await cannot answer this; only a check after it can.
8423
+ * - **Replaced.** The same id can name a *different* subscription by the
8424
+ * time a fetch lands — a re-subscribe overwrites the map entry, and the
8425
+ * old filter's rows would be delivered to the new subscriber.
8426
+ *
8427
+ * The last two are identity, not presence: the map has to still hold *this
8428
+ * exact object*, not merely something under this id.
8429
+ */
8430
+ beginDelivery(subscriptionId, subscription) {
8431
+ const seq = ++subscription.started;
8432
+ return () => {
8433
+ if (this._subscriptions.get(subscriptionId) !== subscription) return false;
8434
+ if (seq <= subscription.delivered) return false;
8435
+ subscription.delivered = seq;
8436
+ return true;
8437
+ };
8438
+ }
8101
8439
  registerDataDriverSubscription(subscriptionId, subscription) {
8102
8440
  this.debugLog("📋 [RealtimeService] Registering DataDriver subscription:", subscriptionId, subscription.authContext ? "(with auth)" : "(no auth)");
8103
- this._subscriptions.set(subscriptionId, subscription);
8441
+ this._subscriptions.set(subscriptionId, {
8442
+ ...subscription,
8443
+ started: 0,
8444
+ delivered: 0
8445
+ });
8104
8446
  }
8105
8447
  addSubscriptionCallback(subscriptionId, callback) {
8106
8448
  this.debugLog("📋 [RealtimeService] Adding callback for subscription:", subscriptionId);
@@ -8127,7 +8469,9 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8127
8469
  databaseId: config.databaseId,
8128
8470
  searchString: config.searchString,
8129
8471
  searchExplain: config.searchExplain
8130
- }
8472
+ },
8473
+ started: 0,
8474
+ delivered: 0
8131
8475
  });
8132
8476
  if (callback) this.subscriptionCallbacks.set(subscriptionId, callback);
8133
8477
  }
@@ -8139,7 +8483,9 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8139
8483
  clientId: config.clientId,
8140
8484
  type: "single",
8141
8485
  path: config.path,
8142
- id: config.id
8486
+ id: config.id,
8487
+ started: 0,
8488
+ delivered: 0
8143
8489
  });
8144
8490
  if (callback) this.subscriptionCallbacks.set(subscriptionId, callback);
8145
8491
  }
@@ -8242,14 +8588,23 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8242
8588
  this.sendError(clientId, e.message, subscriptionId, "INVALID_LIMIT");
8243
8589
  return;
8244
8590
  }
8245
- this._subscriptions.set(subscriptionId, {
8591
+ let orderBy;
8592
+ try {
8593
+ orderBy = parseOrderBySpecStrict(request.orderBy, request.order);
8594
+ } catch (e) {
8595
+ if (!(e instanceof OrderBySpecError)) throw e;
8596
+ logger.warn(`[RealtimeService] Refused subscription to '${request.path}': ${e.message}`);
8597
+ this.sendError(clientId, e.message, subscriptionId, e.code);
8598
+ return;
8599
+ }
8600
+ const subscription = {
8246
8601
  clientId,
8247
8602
  type: "collection",
8248
8603
  path: request.path,
8249
8604
  collectionRequest: {
8250
8605
  filter: request.filter,
8251
8606
  logical: request.logical,
8252
- orderBy: request.orderBy,
8607
+ orderBy,
8253
8608
  order: request.order,
8254
8609
  limit: boundedLimit,
8255
8610
  offset: request.offset,
@@ -8258,10 +8613,14 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8258
8613
  searchString: request.searchString,
8259
8614
  searchExplain: request.searchExplain
8260
8615
  },
8261
- authContext
8262
- });
8263
- const rows = await this.fetchCollectionWithAuth(request.path, this._subscriptions.get(subscriptionId).collectionRequest, authContext);
8264
- this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
8616
+ authContext,
8617
+ started: 0,
8618
+ delivered: 0
8619
+ };
8620
+ this._subscriptions.set(subscriptionId, subscription);
8621
+ const canDeliver = this.beginDelivery(subscriptionId, subscription);
8622
+ const rows = await this.fetchCollectionWithAuth(request.path, subscription.collectionRequest, authContext);
8623
+ if (canDeliver()) this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
8265
8624
  } catch (error) {
8266
8625
  const sanitized = sanitizeErrorForClient(error, request.path);
8267
8626
  this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
@@ -8277,15 +8636,19 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8277
8636
  this.sendError(clientId, msg, subscriptionId);
8278
8637
  return;
8279
8638
  }
8280
- this._subscriptions.set(subscriptionId, {
8639
+ const subscription = {
8281
8640
  clientId,
8282
8641
  type: "single",
8283
8642
  path: request.path,
8284
8643
  id: request.id,
8285
- authContext
8286
- });
8644
+ authContext,
8645
+ started: 0,
8646
+ delivered: 0
8647
+ };
8648
+ this._subscriptions.set(subscriptionId, subscription);
8649
+ const canDeliver = this.beginDelivery(subscriptionId, subscription);
8287
8650
  const row = await this.fetchEntityWithAuth(request.path, String(request.id), authContext);
8288
- this.sendSingleUpdate(clientId, subscriptionId, row || null);
8651
+ if (canDeliver()) this.sendSingleUpdate(clientId, subscriptionId, row || null);
8289
8652
  } catch (error) {
8290
8653
  const sanitized = sanitizeErrorForClient(error, request.path);
8291
8654
  this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
@@ -8416,10 +8779,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8416
8779
  if (existing) clearTimeout(existing);
8417
8780
  this.refetchTimers.set(timerKey, setTimeout(async () => {
8418
8781
  this.refetchTimers.delete(timerKey);
8419
- if (!this._subscriptions.has(subscriptionId)) return;
8782
+ if (this._subscriptions.get(subscriptionId) !== subscription) return;
8783
+ const canDeliver = this.beginDelivery(subscriptionId, subscription);
8420
8784
  try {
8421
8785
  const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext);
8422
- this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath);
8786
+ if (canDeliver()) this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath);
8423
8787
  } catch (error) {
8424
8788
  const sanitized = sanitizeErrorForClient(error, notifyPath);
8425
8789
  this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
@@ -8435,9 +8799,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8435
8799
  if (existing) clearTimeout(existing);
8436
8800
  this.refetchTimers.set(timerKey, setTimeout(async () => {
8437
8801
  this.refetchTimers.delete(timerKey);
8438
- if (!this._subscriptions.has(subscriptionId)) return;
8802
+ if (this._subscriptions.get(subscriptionId) !== subscription) return;
8803
+ const canDeliver = this.beginDelivery(subscriptionId, subscription);
8439
8804
  try {
8440
- callback(await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext));
8805
+ const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext);
8806
+ if (canDeliver()) callback(rows);
8441
8807
  } catch (error) {
8442
8808
  logger.error(`❌ [RealtimeService] Error in debounced driver refetch for ${subscriptionId}`, { error });
8443
8809
  }
@@ -8553,10 +8919,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8553
8919
  if (existing) clearTimeout(existing);
8554
8920
  this.refetchTimers.set(timerKey, setTimeout(async () => {
8555
8921
  this.refetchTimers.delete(timerKey);
8556
- if (!this._subscriptions.has(subscriptionId)) return;
8922
+ if (this._subscriptions.get(subscriptionId) !== subscription) return;
8923
+ const canDeliver = this.beginDelivery(subscriptionId, subscription);
8557
8924
  try {
8558
8925
  const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);
8559
- this.sendSingleUpdate(subscription.clientId, subscriptionId, row || null);
8926
+ if (canDeliver()) this.sendSingleUpdate(subscription.clientId, subscriptionId, row || null);
8560
8927
  } catch (error) {
8561
8928
  const sanitized = sanitizeErrorForClient(error, notifyPath);
8562
8929
  this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
@@ -8572,9 +8939,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8572
8939
  if (existing) clearTimeout(existing);
8573
8940
  this.refetchTimers.set(timerKey, setTimeout(async () => {
8574
8941
  this.refetchTimers.delete(timerKey);
8575
- if (!this._subscriptions.has(subscriptionId)) return;
8942
+ if (this._subscriptions.get(subscriptionId) !== subscription) return;
8943
+ const canDeliver = this.beginDelivery(subscriptionId, subscription);
8576
8944
  try {
8577
- callback(await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext) || null);
8945
+ const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);
8946
+ if (canDeliver()) callback(row || null);
8578
8947
  } catch (error) {
8579
8948
  logger.error(`❌ [RealtimeService] Error in debounced row driver refetch for ${subscriptionId}`, { error });
8580
8949
  }
@@ -8849,7 +9218,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8849
9218
  if (this.memoryBusWarned) return;
8850
9219
  if (this.bus.kind !== "memory" || !this.foreignInstanceSeen) return;
8851
9220
  this.memoryBusWarned = true;
8852
- logger.warn("⚠️ [ChannelBus] Channels are in use with the in-memory bus, but notifications from another instance have been seen — this deployment runs more than one process. Broadcast and presence reach only the clients connected to this one. Set `realtime.bus` (or REBASE_REALTIME_BUS=postgres) to make channels cross-instance.");
9221
+ logger.warn("⚠️ [ChannelBus] Channels are in use with the in-memory bus, but notifications from another instance have been seen — this deployment runs more than one process. Broadcast and presence reach only the clients connected to this one. Set `realtime.bus` (or REALTIME_CHANNEL_BUS=postgres) to make channels cross-instance.");
8853
9222
  }
8854
9223
  /** Leave a broadcast channel */
8855
9224
  leaveChannel(clientId, channel) {
@@ -9511,8 +9880,10 @@ var RealtimeService = class RealtimeService extends EventEmitter {
9511
9880
  */
9512
9881
  async connectListenClient() {
9513
9882
  if (!this.listenConnectionString) return;
9883
+ let pending;
9514
9884
  try {
9515
9885
  const client = new Client({ connectionString: this.listenConnectionString });
9886
+ pending = client;
9516
9887
  client.on("error", (err) => {
9517
9888
  logger.error("❌ [RealtimeService] LISTEN client error", { detail: err.message });
9518
9889
  this.scheduleReconnect();
@@ -9551,8 +9922,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
9551
9922
  await client.connect();
9552
9923
  await client.query(`LISTEN ${PG_NOTIFY_CHANNEL}`);
9553
9924
  this.listenClient = client;
9925
+ pending = void 0;
9554
9926
  this.debugLog(`📡 [RealtimeService] LISTEN client connected on channel "${PG_NOTIFY_CHANNEL}"`);
9555
9927
  } catch (err) {
9928
+ if (pending) try {
9929
+ await pending.end();
9930
+ } catch {}
9556
9931
  logger.error("❌ [RealtimeService] Failed to connect LISTEN client", { error: err });
9557
9932
  this.scheduleReconnect();
9558
9933
  }
@@ -10873,10 +11248,25 @@ var UserService = class {
10873
11248
  if (metadataKey in this.usersTable) payload[metadataKey] = remainingMetadata;
10874
11249
  return payload;
10875
11250
  }
11251
+ /**
11252
+ * @see UserRepository.createUser — an email already in use is a 409.
11253
+ *
11254
+ * The route checks first and answers 409; this is the same answer for the
11255
+ * requests that get past the check, which two clicks on a signup button
11256
+ * are enough to produce. `PersistService` has mapped `23505` to a conflict
11257
+ * for collection writes since the layer that holds the SQLSTATE was made
11258
+ * responsible for saying whose fault a failure is; the auth writes never
11259
+ * got the same treatment and reached the client as "Internal Server Error".
11260
+ */
10876
11261
  async createUser(data) {
10877
11262
  const payload = this.mapPayload(data);
10878
- const [row] = await this.withServerContext(async (db) => await db.insert(this.usersTable).values(payload).returning());
10879
- return this.mapRowToUser(row);
11263
+ try {
11264
+ const [row] = await this.withServerContext(async (db) => await db.insert(this.usersTable).values(payload).returning());
11265
+ return this.mapRowToUser(row);
11266
+ } catch (error) {
11267
+ if (extractPgError(error)?.code === "23505") throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
11268
+ throw error;
11269
+ }
10880
11270
  }
10881
11271
  async getUserById(id) {
10882
11272
  const idCol = getColumn(this.usersTable, "id");
@@ -12693,6 +13083,43 @@ function resolveDriftCheckName(col, registeredTableNames) {
12693
13083
  return (isRelationalCollectionConfig(col) ? col.table : void 0) ?? registeredTableNames.find((k) => k === col.slug) ?? col.slug;
12694
13084
  }
12695
13085
  /**
13086
+ * Why the tables this backend serves are not in the database — the part of the
13087
+ * drift warning that has to be true rather than merely plausible.
13088
+ *
13089
+ * The three answers need three different actions, and only the caller knows
13090
+ * which one applies. This warning used to assert the first ("this runtime
13091
+ * applies the collection schema at boot unless REBASE_MIGRATE_ON_BOOT=none")
13092
+ * and then point at that variable and at driver-version skew. For an app whose
13093
+ * boot path contained no provisioning step at all, every word of that was a
13094
+ * dead end: nothing read the variable, and the driver was current. The advice
13095
+ * cost an investigation, which is a strictly worse outcome than saying less.
13096
+ *
13097
+ * Exported for its own test: the surrounding check needs a live pool and a real
13098
+ * database, and this is the part that was wrong.
13099
+ */
13100
+ function describeSchemaDriftCause(provisioning) {
13101
+ if (provisioning === void 0) return [
13102
+ " This runtime could not determine whether a schema-creation step ran",
13103
+ " before this check (the caller predates that signal).",
13104
+ " • Look for a \"Collection schema:\" line above. No such line at all",
13105
+ " means nothing tried to create these tables in this process."
13106
+ ];
13107
+ if (provisioning.attempted) return [
13108
+ " A schema-creation step DID run this boot and these tables are still",
13109
+ " missing, so it did not create them — check the \"schema:\" lines above",
13110
+ " for what it did instead, and for DDL errors.",
13111
+ " • A collection routed to another engine or data source is not",
13112
+ " created here; that is reported separately at boot.",
13113
+ " • Otherwise this is a bug worth reporting, with those lines."
13114
+ ];
13115
+ return [
13116
+ " No schema-creation step ran this boot:",
13117
+ ` ${provisioning.reason ?? "no reason was given."}`,
13118
+ " Resolve that reason — the drift is its consequence, not a separate",
13119
+ " problem, and re-running a migration tool will not change it."
13120
+ ];
13121
+ }
13122
+ /**
12696
13123
  * Is this the local database `rebase init` scaffolds — i.e. the one case where
12697
13124
  * "you are connected as a superuser" is not news?
12698
13125
  *
@@ -12742,10 +13169,34 @@ function isScaffoldedLocalDatabase(connectionString) {
12742
13169
  */
12743
13170
  function createPostgresBootstrapper(pgConfig) {
12744
13171
  if (pgConfig.unknownFilterFields) configureUnknownFilterFields(pgConfig.unknownFilterFields);
13172
+ /**
13173
+ * The handle the schema/policy hooks issue their DDL through.
13174
+ *
13175
+ * Both hooks run BEFORE `initializeDriver`, so `driverResult` is a stand-in
13176
+ * the caller may not have: the bundle path can synthesize one from the
13177
+ * connection its coordinator opened, but an application that built this
13178
+ * adapter itself never handed the framework a connection — it handed it to
13179
+ * *us*, as `pgConfig.connection`. Falling back to that is what lets a
13180
+ * self-built adapter provision at all; requiring the argument is what left
13181
+ * those apps with no tables and a 500 on every data route.
13182
+ *
13183
+ * Either handle is equivalent here. The driver's `schemaAwareDb` differs
13184
+ * only by the drizzle schema object registered on it — relevant to the query
13185
+ * builder, not to `execute(sql.raw(...))` — and every statement these hooks
13186
+ * emit is schema-qualified DDL, so neither depends on `search_path`.
13187
+ */
13188
+ const provisioningQueryable = (driverResult) => {
13189
+ const db = (driverResult?.internals)?.db ?? pgConfig.connection;
13190
+ if (!db) throw new Error("Cannot provision the collection schema: this Postgres adapter was created without a `connection`, and no initialized driver was supplied to fall back on. Pass `connection` to `createPostgresAdapter` (see `createPostgresDatabaseConnection`).");
13191
+ return { async query(text) {
13192
+ const result = await db.execute(sql.raw(text));
13193
+ return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
13194
+ } };
13195
+ };
12745
13196
  return {
12746
13197
  type: "postgres",
12747
13198
  async initializeDriver(config) {
12748
- const { collections, collectionRegistry, introspectCollections, baas } = config;
13199
+ const { collections, collectionRegistry, introspectCollections, baas, schemaProvisioning } = config;
12749
13200
  const unprotectedTables = baas?.unprotectedTables ?? "exclude";
12750
13201
  const connection = pgConfig.connection;
12751
13202
  const rawClient = connection && typeof connection === "object" && "$client" in connection ? connection.$client : connection;
@@ -12962,22 +13413,18 @@ function createPostgresBootstrapper(pgConfig) {
12962
13413
  " (`?options=-c%20search_path%3Dpublic`).",
12963
13414
  ""
12964
13415
  ];
13416
+ const cause = describeSchemaDriftCause(schemaProvisioning);
12965
13417
  logger.warn([
12966
13418
  "",
12967
13419
  "⚠️ SCHEMA DRIFT — the database is missing tables this backend serves:",
12968
13420
  ...lines,
12969
13421
  "",
12970
13422
  ...misplacedHelp,
12971
- " This runtime applies the collection schema at boot unless",
12972
- " REBASE_MIGRATE_ON_BOOT=none. Check the \"Collection schema\" / \"policies\"",
12973
- " log lines above — this drift means that step was off, skipped, or failed.",
12974
- " • Managed cloud: redeploy with REBASE_MIGRATE_ON_BOOT unset or",
12975
- " \"ensure\"; the runtime applies the schema to the tenant DB.",
12976
- " If the log above says the driver does not implement collection-table",
12977
- " creation, THIS driver is too old to do it. A driver is installed from",
12978
- " your bundle's dependencies, not supplied by the platform image, so a",
12979
- " newer runtime will not update it: bump \"@rebasepro/server-postgres\"",
12980
- " in your project's package.json and redeploy.",
13423
+ ...cause,
13424
+ "",
13425
+ " To apply this project's schema:",
13426
+ " • Managed cloud: the runtime creates tables and RLS at boot. `rebase db",
13427
+ " push` cannot reach a tenant's in-cluster database redeploy instead.",
12981
13428
  " • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod)",
12982
13429
  " against DATABASE_URL.",
12983
13430
  ""
@@ -13065,12 +13512,8 @@ function createPostgresBootstrapper(pgConfig) {
13065
13512
  * proved it can bootstrap with.
13066
13513
  */
13067
13514
  async ensureCollectionSchema(collections, driverResult, log) {
13068
- const internals = driverResult.internals;
13069
- const { ensureCollectionTables } = await import("./ensure-collection-tables-CbvaGuVn.js");
13070
- const plan = await ensureCollectionTables({ async query(text) {
13071
- const result = await internals.db.execute(sql.raw(text));
13072
- return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
13073
- } }, collections, log);
13515
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-DT2eq859.js");
13516
+ const plan = await ensureCollectionTables(provisioningQueryable(driverResult), collections, log);
13074
13517
  for (const failure of plan.failures) logger.warn(failure.kind === "comment-column" ? `🔍 [schema] Could not record the search fingerprint on "${failure.target}" — search works, but a later change to the \`search\` block will not be detected: ${failure.error}` : `🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the collection still serves, but rows are not policed by this constraint: ${failure.error}`);
13075
13518
  return { applied: plan.actions.length - plan.failures.length };
13076
13519
  },
@@ -13090,12 +13533,9 @@ function createPostgresBootstrapper(pgConfig) {
13090
13533
  * stays RLS-enabled, so it denies rather than leaks.
13091
13534
  */
13092
13535
  async ensureCollectionPolicies(collections, driverResult, log) {
13093
- const internals = driverResult.internals;
13094
- const { ensureCollectionPolicies } = await import("./ensure-collection-policies-8vuu-n4r.js");
13095
- const outcome = await ensureCollectionPolicies({ async query(text) {
13096
- const result = await internals.db.execute(sql.raw(text));
13097
- return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
13098
- } }, collections, log);
13536
+ const { ensureCollectionPolicies } = await import("./ensure-collection-policies-DoHwhVf8.js");
13537
+ const queryable = provisioningQueryable(driverResult);
13538
+ const outcome = await ensureCollectionPolicies(queryable, collections, log);
13099
13539
  for (const skip of outcome.skipped) logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
13100
13540
  for (const failure of outcome.failures) logger.warn(`🔐 [rls] Could not fully apply policies to "${failure.table}" — RLS is on, so it denies until this is resolved: ${failure.error}`);
13101
13541
  const unrevoked = outcome.unsecured.filter((u) => !u.grantWithdrawn);
@@ -13103,9 +13543,7 @@ function createPostgresBootstrapper(pgConfig) {
13103
13543
  if (unrevoked.length > 0) throw new Error("Refusing to start: row-level security could not be enabled on " + unrevoked.map((u) => `"${u.table}" (${u.error})`).join(", ") + `, and the privileges granted to ${REBASE_USER_ROLE} could not be revoked either. The table would be served with no row filtering. Fix the database permissions (the connection role must own these tables, or be able to ALTER them) and boot again.`);
13104
13544
  try {
13105
13545
  const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-69hYT8nr.js").then((n) => n.n);
13106
- await dropLegacyAuthSchema(async (text) => {
13107
- return (await internals.db.execute(sql.raw(text))).rows ?? [];
13108
- }, {
13546
+ await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
13109
13547
  info: (m) => logger.info(m),
13110
13548
  warn: (m) => logger.warn(m)
13111
13549
  });
@@ -13119,7 +13557,7 @@ function createPostgresBootstrapper(pgConfig) {
13119
13557
  },
13120
13558
  mountRoutes(app, basePath, driverResult) {},
13121
13559
  async initializeWebsockets(server, realtimeService, driver, config, adapter) {
13122
- const { createPostgresWebSocket } = await import("./websocket-C8ZqVBiV.js").then((n) => n.n);
13560
+ const { createPostgresWebSocket } = await import("./websocket-D2jXv0Ds.js").then((n) => n.n);
13123
13561
  createPostgresWebSocket(server, realtimeService, driver, config, adapter);
13124
13562
  }
13125
13563
  };
@@ -13159,6 +13597,6 @@ function createPostgresAdapter(pgConfig) {
13159
13597
  };
13160
13598
  }
13161
13599
  //#endregion
13162
- export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, CHANNEL_BUS_NOTIFY_CHANNEL, DEFAULT_BATCH_WINDOW_MS, DatabasePoolManager, DrizzleConditionBuilder, MemoryChannelBus, PG_NOTIFY_MAX_PAYLOAD_BYTES, PostgresBackendDriver, PostgresChannelBus, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, applyGlobals, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgDumpallGlobalsArgs, buildPgRestoreArgs, buildPgRestoreListArgs, buildRowSecurityPgOptions, checkToolServerCompatibility, configureUnknownFilterFields, createAuthSchema, createBackupCron, createChannelBus, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, diagnoseRowSecurityDumpFailure, ensureDatabaseExists, escapeLikePattern, frameByteLength, generateSchema, getDrizzleColumn, getServerVersionMajor, getUnknownFilterFieldsMode, globalsFileForDump, guardPoolAgainstDirtyRelease, isChannelBusInstance, isScaffoldedLocalDatabase, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseChannelBusFrame, parseChannelBusPayload, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, pinSearchPath, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveChannelBusSetting, resolveConnectionString, resolveDriftCheckName, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, splitGlobalsStatements, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, validateDump, withDatabaseName };
13600
+ export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, CHANNEL_BUS_NOTIFY_CHANNEL, DEFAULT_BATCH_WINDOW_MS, DatabasePoolManager, DrizzleConditionBuilder, MemoryChannelBus, PG_NOTIFY_MAX_PAYLOAD_BYTES, PostgresBackendDriver, PostgresChannelBus, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, applyGlobals, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgDumpallGlobalsArgs, buildPgRestoreArgs, buildPgRestoreListArgs, buildRowSecurityPgOptions, checkToolServerCompatibility, configureUnknownFilterFields, createAuthSchema, createBackupCron, createChannelBus, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, describeSchemaDriftCause, detectToolMajor, diagnoseRowSecurityDumpFailure, ensureDatabaseExists, escapeLikePattern, frameByteLength, generateSchema, getDrizzleColumn, getServerVersionMajor, getUnknownFilterFieldsMode, globalsFileForDump, guardPoolAgainstDirtyRelease, isChannelBusInstance, isScaffoldedLocalDatabase, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseChannelBusFrame, parseChannelBusPayload, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, pinSearchPath, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveChannelBusSetting, resolveConnectionString, resolveDriftCheckName, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, splitGlobalsStatements, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, validateDump, withDatabaseName };
13163
13601
 
13164
13602
  //# sourceMappingURL=index.es.js.map