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

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 (85) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/auth/services.d.ts +19 -0
  3. package/dist/{src-DlPBctw_.js → auth-users-columns-CBEOeYqa.js} +835 -63
  4. package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
  5. package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
  6. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
  7. package/dist/cli-helpers.d.ts +57 -1
  8. package/dist/data-transformer.d.ts +7 -2
  9. package/dist/data_driver-ULAyJEi9.js +193 -0
  10. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  11. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-B_JMGa5K.js} +2 -2
  12. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-B_JMGa5K.js.map} +1 -1
  13. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DzeTEvMv.js} +170 -20
  14. package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
  15. package/dist/index.es.js +1365 -338
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  18. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  19. package/dist/schema/auth-schema.d.ts +102 -0
  20. package/dist/schema/auth-users-columns.d.ts +97 -0
  21. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  22. package/dist/schema/doctor.d.ts +23 -25
  23. package/dist/schema/ensure-collection-tables.d.ts +61 -7
  24. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  25. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  26. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  27. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  28. package/dist/schema/search-column.d.ts +248 -0
  29. package/dist/security/rls-enforcement.d.ts +61 -5
  30. package/dist/services/FetchService.d.ts +34 -7
  31. package/dist/services/RelationService.d.ts +30 -0
  32. package/dist/services/collection-helpers.d.ts +26 -0
  33. package/dist/services/dataService.d.ts +5 -0
  34. package/dist/services/realtimeService.d.ts +131 -21
  35. package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
  36. package/dist/src-C_wvdMnl.js.map +1 -0
  37. package/dist/utils/drizzle-conditions.d.ts +124 -2
  38. package/dist/{websocket-B2LsrINK.js → websocket-D1qbmLZ2.js} +75 -18
  39. package/dist/websocket-D1qbmLZ2.js.map +1 -0
  40. package/package.json +9 -8
  41. package/src/PostgresBackendDriver.ts +172 -6
  42. package/src/PostgresBootstrapper.ts +55 -6
  43. package/src/auth/ensure-tables.ts +207 -86
  44. package/src/auth/services.ts +69 -5
  45. package/src/cli-helpers.ts +129 -10
  46. package/src/cli.ts +232 -30
  47. package/src/collections/validate-relations.ts +124 -17
  48. package/src/data-transformer.ts +120 -17
  49. package/src/history/ensure-history-table.ts +7 -0
  50. package/src/schema/auth-schema.ts +17 -1
  51. package/src/schema/auth-users-columns.ts +131 -0
  52. package/src/schema/doctor-cli.ts +12 -63
  53. package/src/schema/doctor-policy-checks.ts +105 -0
  54. package/src/schema/doctor.ts +135 -76
  55. package/src/schema/ensure-collection-tables.ts +374 -32
  56. package/src/schema/generate-drizzle-schema-logic.ts +132 -42
  57. package/src/schema/generate-postgres-ddl-logic.ts +294 -16
  58. package/src/schema/generate-postgres-ddl.ts +25 -2
  59. package/src/schema/generated-schema-staleness.ts +169 -0
  60. package/src/schema/introspect-db-logic.ts +66 -34
  61. package/src/schema/non-sql-collections.test.ts +131 -0
  62. package/src/schema/rls-bootstrap-sql.ts +288 -0
  63. package/src/schema/search-column.ts +643 -0
  64. package/src/security/anonymous-grants.test.ts +4 -2
  65. package/src/security/rls-enforcement.ts +149 -6
  66. package/src/services/BranchService.ts +5 -0
  67. package/src/services/FetchService.ts +175 -108
  68. package/src/services/PersistService.ts +38 -2
  69. package/src/services/RelationService.ts +110 -67
  70. package/src/services/channel-history.ts +14 -0
  71. package/src/services/channel-presence.ts +13 -0
  72. package/src/services/collection-helpers.ts +54 -1
  73. package/src/services/dataService.ts +5 -0
  74. package/src/services/realtimeService.ts +344 -79
  75. package/src/utils/drizzle-conditions.ts +365 -23
  76. package/src/utils/pg-error-utils.ts +8 -3
  77. package/src/websocket.ts +113 -16
  78. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  79. package/dist/policy-CeA1JcxP.js +0 -105
  80. package/dist/policy-CeA1JcxP.js.map +0 -1
  81. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  82. package/dist/src-DlPBctw_.js.map +0 -1
  83. package/dist/src-DoU9yPqq.js.map +0 -1
  84. package/dist/websocket-B2LsrINK.js.map +0 -1
  85. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -2,8 +2,9 @@ import { createRequire as __createRequire } from "module";
2
2
  import "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { l as __require, s as __commonJSMin } from "./connection-BuZ97wsr.js";
5
- import { a as getDataSourceCapabilities, c as toCanonicalOp, n as isPostgresCollectionConfig, o as NULL_OPS, r as isRelationalCollectionConfig, s as REST_TO_CANONICAL, t as getDeclaredSubcollections } from "./src-DoU9yPqq.js";
6
- import { n as ANONYMOUS_USER_IDS, r as policy, t as ANONYMOUS_USER_ID } from "./policy-CeA1JcxP.js";
5
+ import { a as policy, i as ANONYMOUS_USER_IDS, r as ANONYMOUS_USER_ID } from "./data_driver-ULAyJEi9.js";
6
+ import { a as rewriteLegacyRlsFunctions, c as isPostgresCollectionConfig, d as getDataSourceCapabilities, f as NULL_OPS, i as RLS_UID_SQL, l as isRelationalCollectionConfig, m as toCanonicalOp, p as REST_TO_CANONICAL, r as RLS_ROLES_SQL, s as getDeclaredSubcollections } from "./src-C_wvdMnl.js";
7
+ import { createHash } from "node:crypto";
7
8
  //#region ../types/src/types/entities.ts
8
9
  /**
9
10
  * Class used to create a reference to a entity in a different path
@@ -1007,6 +1008,35 @@ function camelCase(str) {
1007
1008
  });
1008
1009
  })))();
1009
1010
  /**
1011
+ * Segments that reach the prototype chain rather than a property of the object.
1012
+ *
1013
+ * The twin of this function in `@rebasepro/forms` could be made to write onto
1014
+ * `Object.prototype` through a path of `__proto__.x`. This copy survives the
1015
+ * write by accident — its `clone` always spreads into a fresh object, while the
1016
+ * form engine's has a "preserve class instances" branch that hands back
1017
+ * `Object.prototype` itself — but `getIn` still *reads* through the chain, and
1018
+ * handing back `Object.prototype` is how a polluted value is read out again.
1019
+ *
1020
+ * Closed on both sides here, so the two implementations agree.
1021
+ */
1022
+ var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
1023
+ "__proto__",
1024
+ "constructor",
1025
+ "prototype"
1026
+ ]);
1027
+ /**
1028
+ * Whether writing this single key with `obj[key] = …` would reach the prototype
1029
+ * chain instead of creating a property.
1030
+ *
1031
+ * The single-key counterpart of {@link pathTraversesPrototype}, for the many
1032
+ * places that copy an object one key at a time. `JSON.parse` creates
1033
+ * `__proto__` as an *own* property, so it survives `hasOwnProperty` — and then
1034
+ * `target[key] = value` invokes the setter and replaces the target's prototype.
1035
+ */
1036
+ function isPrototypePollutingKey(key) {
1037
+ return UNSAFE_PATH_SEGMENTS.has(key);
1038
+ }
1039
+ /**
1010
1040
  * Deep clone a value, preserving function references and class instances.
1011
1041
  * Unlike structuredClone, this handles objects that contain functions
1012
1042
  * (e.g. CollectionConfig with target(), childCollections(), callbacks).
@@ -1360,6 +1390,28 @@ function legacyForeignKeyName(name) {
1360
1390
  const snake = toSnakeCase(name);
1361
1391
  return `${snake.endsWith("s") ? snake.slice(0, -1) : snake}_id`;
1362
1392
  }
1393
+ /**
1394
+ * Truncate an identifier to what Postgres will actually store.
1395
+ *
1396
+ * Postgres silently truncates identifiers at NAMEDATALEN-1 = 63 **bytes**, so a
1397
+ * name generated longer than that is not the name the database ends up holding.
1398
+ * Anything that later looks the object up by the name it generated then misses.
1399
+ *
1400
+ * Byte length, not string length: NAMEDATALEN is a byte bound, and a multi-byte
1401
+ * character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.
1402
+ *
1403
+ * `TextEncoder` rather than `Buffer`, which is not a matter of taste: `Buffer`
1404
+ * is a Node global, and this package is imported by browser-facing ones. It
1405
+ * typechecked only where `@types/node` happened to be in scope, so
1406
+ * `packages/codegen` — whose tsconfig is `lib: ["ESNext", "dom"]` — could not
1407
+ * compile the file at all, and both of its suites failed to run. `TextEncoder`
1408
+ * and `TextDecoder` are standard in both runtimes and need no ambient types.
1409
+ */
1410
+ function toPostgresIdentifier(name) {
1411
+ const bytes = new TextEncoder().encode(name);
1412
+ if (bytes.byteLength <= 63) return name;
1413
+ return new TextDecoder("utf-8").decode(bytes.subarray(0, 63)).replace(/�+$/, "");
1414
+ }
1363
1415
  //#endregion
1364
1416
  //#region ../common/src/util/entities.ts
1365
1417
  /**
@@ -1383,10 +1435,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
1383
1435
  * have `id` and `path` fields — these are relation-shaped objects from
1384
1436
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
1385
1437
  *
1438
+ * When `targetPath` is given, also accepts a bare id. A relation column is a
1439
+ * foreign key, and the REST layer returns it as the scalar it is; only some
1440
+ * fetch paths hydrate it into an object. Which form a caller sees therefore
1441
+ * depends on how the row was loaded, and a caller that only accepted objects
1442
+ * reported half of its own data as a type error. The declared target is the
1443
+ * missing half: with it, an id is a relation that has not been fetched yet.
1444
+ *
1386
1445
  * Returns null if the value cannot be coerced.
1387
1446
  */
1388
- function normalizeToEntityRelation(value, propertyType) {
1447
+ function normalizeToEntityRelation(value, propertyType, targetPath) {
1389
1448
  if (value instanceof EntityRelation) return value;
1449
+ if (targetPath && (typeof value === "string" || typeof value === "number")) {
1450
+ if (value === "") return null;
1451
+ return new EntityRelation(value, targetPath);
1452
+ }
1390
1453
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1391
1454
  const obj = value;
1392
1455
  if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
@@ -1458,6 +1521,25 @@ function createRelationRefWithData(id, path, data) {
1458
1521
  data
1459
1522
  };
1460
1523
  }
1524
+ //#endregion
1525
+ //#region ../common/src/util/collections.ts
1526
+ /**
1527
+ * A copy of `collections` ordered by slug.
1528
+ *
1529
+ * Every generator that turns collections into a file is order-dependent, and
1530
+ * every one of them is compared against its own output — `rebase doctor`
1531
+ * regenerates in memory and diffs, `generate-sdk && git diff --exit-code` gates
1532
+ * CI. While only the *writers* sorted, a project whose `readdirSync` order
1533
+ * differed from its slug order was reported permanently out of date, and the
1534
+ * fix the message printed rewrote the file in the order it was already in. The
1535
+ * generators sort themselves now, so no caller can get this wrong.
1536
+ *
1537
+ * A slug-less collection is left to the generator's own validation, which names
1538
+ * the offending collection; sorting must not throw first.
1539
+ */
1540
+ function sortCollectionsBySlug(collections) {
1541
+ return [...collections].sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""));
1542
+ }
1461
1543
  /** The eight-four-four-four-twelve shape of a UUID, any version. */
1462
1544
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1463
1545
  /** Whether one address part can be a value of the column it addresses. */
@@ -1924,9 +2006,9 @@ function isKeywordAt(upper, i, keyword) {
1924
2006
  *
1925
2007
  * This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
1926
2008
  * `AND` inside
1927
- * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = auth.uid())`
2009
+ * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
1928
2010
  * split the expression, and re-emitting the halves produced
1929
- * `(EXISTS (...) AND m.user_id = auth.uid())`
2011
+ * `(EXISTS (...) AND m.user_id = rebase.uid())`
1930
2012
  * where `m` is no longer in scope — SQL that Postgres rejects outright with
1931
2013
  * "missing FROM-clause entry for table". Returning null instead keeps such a
1932
2014
  * clause as a `raw` expression, which round-trips verbatim.
@@ -2000,15 +2082,15 @@ function stripOuterParens(sql) {
2000
2082
  }
2001
2083
  }
2002
2084
  function sqlToPolicy(sql) {
2003
- const trimmed = stripOuterParens(sql.trim());
2085
+ const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
2004
2086
  if (trimmed.toLowerCase() === "true") return policy.true();
2005
2087
  if (trimmed.toLowerCase() === "false") return policy.false();
2006
- const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
2088
+ const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
2007
2089
  if (overlapMatch) {
2008
2090
  const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
2009
2091
  return policy.rolesOverlap(roles);
2010
2092
  }
2011
- const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
2093
+ const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
2012
2094
  if (containMatch) {
2013
2095
  const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
2014
2096
  return policy.rolesContain(roles);
@@ -2024,10 +2106,10 @@ function sqlToPolicy(sql) {
2024
2106
  const right = parseOperand(rightStr.trim());
2025
2107
  if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
2026
2108
  }
2027
- return policy.raw(sql);
2109
+ return policy.raw(trimmed);
2028
2110
  }
2029
2111
  /**
2030
- * Literals from other BaaS platforms that people compare `auth.uid()` against
2112
+ * Literals from other BaaS platforms that people compare `rebase.uid()` against
2031
2113
  * out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
2032
2114
  * `pgRoles`, one surface over: the same muscle memory inside a `using:` string
2033
2115
  * is the more dangerous spelling, because it inverts a rule instead of
@@ -2038,18 +2120,25 @@ var FOREIGN_CONVENTION_UIDS = {
2038
2120
  authenticated: "Supabase",
2039
2121
  service_role: "Supabase"
2040
2122
  };
2041
- /** `auth.uid() IS NOT NULL` in raw SQL, the clause that is always true. */
2042
- var UID_NOT_NULL = /auth\.uid\(\)\s+IS\s+NOT\s+NULL/i;
2123
+ /**
2124
+ * `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
2125
+ *
2126
+ * Both schema spellings, because this runs over policy bodies read back from a
2127
+ * database, and one migrated by a pre-1.0 release still holds `auth.uid()`.
2128
+ * A security check that stops recognising a dangerous clause because the
2129
+ * framework renamed a function is a check that silently turns off.
2130
+ */
2131
+ var UID_NOT_NULL = /\b(?:rebase|auth)\.uid\(\)\s+IS\s+NOT\s+NULL/i;
2043
2132
  /**
2044
2133
  * Find clauses that read as "signed-in users only" but admit anonymous callers.
2045
2134
  *
2046
- * Both spellings come from the same place — Supabase, where `auth.uid()` really
2047
- * is NULL for an anonymous request. Rebase substitutes
2135
+ * Both spellings come from the same place — Supabase, where its own `auth.uid()`
2136
+ * really is NULL for an anonymous request. Rebase substitutes
2048
2137
  * {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
2049
2138
  * is how the trusted *server* context is recognised), so:
2050
2139
  *
2051
- * - `auth.uid() IS NOT NULL` is a tautology on the user path, and
2052
- * - `auth.uid() != 'anon'` excludes one spelling of anonymous and admits the
2140
+ * - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
2141
+ * - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
2053
2142
  * other. This one is not hypothetical and was not only a foreign habit:
2054
2143
  * rebase's own request path reported `'anon'` while everything that compiled
2055
2144
  * or checked a policy used `'anonymous'`, so whichever literal an author
@@ -2081,7 +2170,7 @@ function findAnonymousGrants(expr) {
2081
2170
  if (UID_NOT_NULL.test(e.sql)) found.push({
2082
2171
  pattern: "uid-not-null",
2083
2172
  detail: e.sql,
2084
- explanation: `\`auth.uid() IS NOT NULL\` is true for every request that came from a client, including anonymous ones — they carry '${ANONYMOUS_USER_ID}', not NULL. Use \`condition: policy.authenticated()\` to mean "signed in".`
2173
+ explanation: `\`rebase.uid() IS NOT NULL\` is true for every request that came from a client, including anonymous ones — they carry '${ANONYMOUS_USER_ID}', not NULL. Use \`condition: policy.authenticated()\` to mean "signed in".`
2085
2174
  });
2086
2175
  return;
2087
2176
  case "compare": {
@@ -2103,12 +2192,43 @@ function findAnonymousGrants(expr) {
2103
2192
  return found;
2104
2193
  }
2105
2194
  function parseOperand(str) {
2106
- if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return policy.authUid();
2107
- const stringMatch = str.match(/^'(.+)'$/);
2108
- if (stringMatch) return policy.literal(stringMatch[1]);
2109
- if (/^\w+$/.test(str)) return policy.field(str);
2195
+ if (/^current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)$/i.test(str) || /^rebase\.uid\(\)$/i.test(str)) return policy.authUid();
2196
+ const literal = parseSingleQuoted(str);
2197
+ if (literal !== null) return policy.literal(literal);
2198
+ if (/^-?\d+$/.test(str)) return policy.literal(Number(str));
2199
+ if (/^-?\d*\.\d+$/.test(str)) return policy.literal(Number(str));
2200
+ if (/^true$/i.test(str)) return policy.literal(true);
2201
+ if (/^false$/i.test(str)) return policy.literal(false);
2202
+ if (/^null$/i.test(str)) return policy.literal(null);
2203
+ if (/^\w+$/.test(str) && toSnakeCase(str) !== "") return policy.field(str);
2110
2204
  return null;
2111
2205
  }
2206
+ /**
2207
+ * Decode a single-quoted SQL literal, or null when `str` is not exactly one.
2208
+ *
2209
+ * Rejecting is as important as decoding: `'a' = 'b'` is two literals and an
2210
+ * operator, not one literal whose body contains a quote, and a regex anchored
2211
+ * on the outer quotes would happily read it as the latter. Every interior quote
2212
+ * must therefore be part of a `''` pair.
2213
+ */
2214
+ function parseSingleQuoted(str) {
2215
+ if (str.length < 2 || !str.startsWith("'") || !str.endsWith("'")) return null;
2216
+ const body = str.slice(1, -1);
2217
+ let out = "";
2218
+ for (let i = 0; i < body.length; i++) {
2219
+ if (body[i] !== "'") {
2220
+ out += body[i];
2221
+ continue;
2222
+ }
2223
+ if (body[i + 1] === "'") {
2224
+ out += "'";
2225
+ i++;
2226
+ continue;
2227
+ }
2228
+ return null;
2229
+ }
2230
+ return out;
2231
+ }
2112
2232
  //#endregion
2113
2233
  //#region ../common/src/util/policy/securityRuleToConditions.ts
2114
2234
  /**
@@ -2185,12 +2305,12 @@ function compile(expr, scope) {
2185
2305
  const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
2186
2306
  return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
2187
2307
  }
2188
- case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
2189
- case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
2190
- case "authenticated": return `auth.uid() IS NOT NULL AND auth.uid() NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
2191
- case "serverContext": return "auth.uid() IS NULL";
2308
+ case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
2309
+ case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
2310
+ case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
2311
+ case "serverContext": return `${RLS_UID_SQL} IS NULL`;
2192
2312
  case "existsIn": return compileExistsIn(expr, scope);
2193
- case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
2313
+ case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
2194
2314
  }
2195
2315
  }
2196
2316
  /**
@@ -2227,8 +2347,8 @@ function operandToSql(operand, scope) {
2227
2347
  case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
2228
2348
  case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
2229
2349
  case "literal": return quoteLiteral(operand.value);
2230
- case "authUid": return "auth.uid()";
2231
- case "authRoles": return "string_to_array(auth.roles(), ',')";
2350
+ case "authUid": return RLS_UID_SQL;
2351
+ case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
2232
2352
  }
2233
2353
  }
2234
2354
  /**
@@ -2266,8 +2386,14 @@ function rolesArraySql(roles) {
2266
2386
  * Rebase's enforcement model is unified: authenticated (user-context) requests
2267
2387
  * run under the restricted `rebase_user` role, so Postgres RLS binds *every*
2268
2388
  * statement — reads and writes. A collection's `securityRules` are the whole
2269
- * authorization model. The server context (auth flows, migrations,
2270
- * `dataAsAdmin`) runs as the owner and bypasses RLS.
2389
+ * authorization model. The server context (auth flows, migrations, raw
2390
+ * `rebase.sql`) runs as the owner and bypasses RLS.
2391
+ *
2392
+ * `rebase.dataAsAdmin` is **not** in that set, despite the name: it is scoped as
2393
+ * `{ uid: "service", roles: ["admin"] }`, so it runs as `rebase_user` like any
2394
+ * other caller and clears the baseline below through the *admin* arm, not the
2395
+ * server arm. Which is why `disableDefaultPolicies` plus a lone
2396
+ * `policy.serverContext()` rule locks it out too.
2271
2397
  *
2272
2398
  * Because RLS default-denies, every collection is **locked by default**: with
2273
2399
  * no rules, only the server context and admins can touch it. The generator
@@ -2308,7 +2434,7 @@ var DEFAULT_GUARDED_OPS = [
2308
2434
  "delete"
2309
2435
  ];
2310
2436
  /** Whether a collection is flagged as an authentication collection. */
2311
- function isAuthCollection(collection) {
2437
+ function isAuthCollection$1(collection) {
2312
2438
  const auth = collection.auth;
2313
2439
  return auth === true || typeof auth === "object" && auth?.enabled === true;
2314
2440
  }
@@ -2341,7 +2467,7 @@ function getEffectiveSecurityRules(collection) {
2341
2467
  condition: SERVER_OR_ADMIN_EXPR$1,
2342
2468
  check: SERVER_OR_ADMIN_EXPR$1
2343
2469
  });
2344
- if (isAuthCollection(collection)) {
2470
+ if (isAuthCollection$1(collection)) {
2345
2471
  injected.push({
2346
2472
  name: `${tableName}_default_self_read`,
2347
2473
  operations: ["select"],
@@ -3308,6 +3434,46 @@ function resolveDataSource(collection, registry) {
3308
3434
  capabilities: getDataSourceCapabilities(engine)
3309
3435
  };
3310
3436
  }
3437
+ /**
3438
+ * Does a SQL toolchain own this collection's storage?
3439
+ *
3440
+ * "Owns the storage" means: something generates a table for it, pushes that
3441
+ * table to a database, plans its RLS policies, and reports it as drifted when
3442
+ * the two disagree. That is true of a Postgres collection and false of a
3443
+ * Firestore or MongoDB one, whose documents live in a store Rebase never
3444
+ * migrates — and the two were never told apart. Every stage of the SQL
3445
+ * toolchain took "the collections" to mean *all* of them, so a Firestore
3446
+ * collection declared next to the Postgres ones got a `pgTable` in the
3447
+ * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
3448
+ * `db push` include list — where its name shielding a same-named real table
3449
+ * from Atlas's exclude list is the one that can lose data.
3450
+ *
3451
+ * The answer is the resolved engine's {@link DataSourceCapabilities}, not a
3452
+ * name check: an engine registered through `registerDataSourceCapabilities`
3453
+ * gets the same treatment as the built-in ones.
3454
+ *
3455
+ * Deliberately answers **true** for an engine nobody has heard of. Build-time
3456
+ * tooling (the CLI, the schema generator) has no data-source registry to
3457
+ * resolve a `dataSource` key against, so an unknown key resolves to an unknown
3458
+ * engine — and the cost of the two mistakes is not symmetric. Wrongly
3459
+ * including a collection generates a table nothing writes to; wrongly excluding
3460
+ * one silently stops generating a table the app is serving from. Declare
3461
+ * `engine` on a collection that is not SQL-backed and this is exact.
3462
+ */
3463
+ function isRelationalCollection(collection, registry) {
3464
+ return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
3465
+ }
3466
+ /**
3467
+ * The subset of `collections` a SQL toolchain owns — see
3468
+ * {@link isRelationalCollection}.
3469
+ *
3470
+ * Every stage that generates SQL from collections starts by calling this, so
3471
+ * the rule lives in one place rather than being re-decided per generator. It
3472
+ * keeps the input order.
3473
+ */
3474
+ function relationalCollections(collections, registry) {
3475
+ return collections.filter((collection) => isRelationalCollection(collection, registry));
3476
+ }
3311
3477
  //#endregion
3312
3478
  //#region ../common/src/collections/CollectionRegistry.ts
3313
3479
  var CollectionRegistry = class {
@@ -3602,8 +3768,24 @@ var QueryBuilder = class {
3602
3768
  /**
3603
3769
  * Set a free-text search string if supported by the backend.
3604
3770
  */
3605
- search(searchString) {
3771
+ search(searchString, options) {
3606
3772
  this.params.searchString = searchString;
3773
+ if (options?.explain !== void 0) this.params.searchExplain = options.explain;
3774
+ return this;
3775
+ }
3776
+ /**
3777
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
3778
+ *
3779
+ * Postgres only, over a property declared as `type: "vector"`. Rows come
3780
+ * back with a `_distance`; `where` filters before the ordering.
3781
+ */
3782
+ vectorSearch(property, vector, options) {
3783
+ this.params.vectorSearch = {
3784
+ property,
3785
+ vector,
3786
+ ...options?.distance !== void 0 && { distance: options.distance },
3787
+ ...options?.threshold !== void 0 && { threshold: options.threshold }
3788
+ };
3607
3789
  return this;
3608
3790
  }
3609
3791
  /**
@@ -3660,6 +3842,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
3660
3842
  Object.setPrototypeOf(this, RebasePaginationError.prototype);
3661
3843
  }
3662
3844
  };
3845
+ /**
3846
+ * Resolve `limit`/`offset`/`page` into the window a read will actually use.
3847
+ *
3848
+ * Lives here, next to the walk, for the reason at the top of this file: every
3849
+ * transport has to mean the same thing by "page two". Four of them did not —
3850
+ * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
3851
+ * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
3852
+ * the published type documented a fourth number. Pages that overlap or skip
3853
+ * rows are the mildest of those outcomes.
3854
+ *
3855
+ * `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
3856
+ * is the value to hand a driver: it stays `undefined` when the caller named no
3857
+ * offset, because keyset pagination seeks with a `where` clause and must not
3858
+ * look like it is paging by offset.
3859
+ */
3860
+ function resolveFindWindow(params) {
3861
+ const limit = params?.limit ?? 50;
3862
+ const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
3863
+ return {
3864
+ limit,
3865
+ offset,
3866
+ driverOffset: params?.page != null ? offset : params?.offset
3867
+ };
3868
+ }
3663
3869
  function normalizePageSize(raw) {
3664
3870
  if (raw === void 0 || !Number.isFinite(raw)) return 200;
3665
3871
  return Math.max(1, Math.floor(raw));
@@ -3779,21 +3985,39 @@ async function collectAllPages(find, params, label = "collection") {
3779
3985
  * metadata, so type coercion is the responsibility of the server-side data
3780
3986
  * driver which has access to the collection schema.
3781
3987
  *
3782
- * Commas inside list values are backslash-escaped (`\,`), and literal
3783
- * backslashes are escaped as `\\`.
3988
+ * Structural characters inside a value are backslash-escaped: `,` `\,`,
3989
+ * `(` `\(`, `)` → `\)`, and a literal backslash as `\\`. Decoding is
3990
+ * deliberately conservative — only those four sequences are decoded, so a
3991
+ * backslash that arrives unescaped from an older client survives intact.
3784
3992
  *
3785
3993
  * @module
3786
3994
  */
3787
3995
  /**
3788
- * Unescape a single list item from the wire format.
3789
- * `\\` → `\`, `\,` → `,`
3996
+ * Unescape a wire-format value.
3997
+ *
3998
+ * **Conservative**, and deliberately so: only the four sequences
3999
+ * {@link escapeWireValue} actually produces are decoded. A backslash followed
4000
+ * by anything else is left exactly as it is.
4001
+ *
4002
+ * This used to consume the backslash before *any* character, which is
4003
+ * indistinguishable for anything this codec emitted — it only ever emits those
4004
+ * four — but not for input arriving from elsewhere. A client on an older
4005
+ * release sends a Windows path or a LIKE pattern with a literal `C:\x`
4006
+ * unescaped, and greedy unescaping silently turned it into `C:x`, changing
4007
+ * which rows matched. Decoding only what the encoder can produce makes the two
4008
+ * directions agree across versions.
3790
4009
  */
3791
- function unescapeListItem(value) {
4010
+ function unescapeWireValue(value) {
3792
4011
  let result = "";
3793
- for (let i = 0; i < value.length; i++) if (value[i] === "\\" && i + 1 < value.length) {
3794
- result += value[i + 1];
3795
- i++;
3796
- } else result += value[i];
4012
+ for (let i = 0; i < value.length; i++) {
4013
+ const next = value[i + 1];
4014
+ if (value[i] === "\\" && (next === "\\" || next === "," || next === "(" || next === ")")) {
4015
+ result += next;
4016
+ i++;
4017
+ continue;
4018
+ }
4019
+ result += value[i];
4020
+ }
3797
4021
  return result;
3798
4022
  }
3799
4023
  /**
@@ -3811,10 +4035,10 @@ function splitListItems(inner) {
3811
4035
  current += inner[i] + inner[i + 1];
3812
4036
  i++;
3813
4037
  } else if (inner[i] === ",") {
3814
- items.push(unescapeListItem(current));
4038
+ items.push(unescapeWireValue(current));
3815
4039
  current = "";
3816
4040
  } else current += inner[i];
3817
- items.push(unescapeListItem(current));
4041
+ items.push(unescapeWireValue(current));
3818
4042
  return items;
3819
4043
  }
3820
4044
  var REST_OP_LOOKUP = REST_TO_CANONICAL;
@@ -3909,10 +4133,12 @@ function createPrimaryKeyResolver(options) {
3909
4133
  * than postgres still serve rows with one, and this keeps them working.
3910
4134
  */
3911
4135
  function rowToEntity(row, slug, primaryKeys = []) {
4136
+ const { _matches, ...values } = row;
3912
4137
  return {
3913
4138
  id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
3914
4139
  path: slug,
3915
- values: row
4140
+ values,
4141
+ ..._matches ? { searchMatches: _matches } : {}
3916
4142
  };
3917
4143
  }
3918
4144
  /**
@@ -3955,21 +4181,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3955
4181
  const accessor = {
3956
4182
  async find(params) {
3957
4183
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3958
- const limit = params?.limit ?? 20;
3959
- const offset = params?.offset ?? 0;
4184
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3960
4185
  const fetchService = driver.restFetchService;
3961
4186
  const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
3962
4187
  filter,
3963
- limit: params?.limit,
3964
- offset: params?.offset,
4188
+ logical: params?.logical,
4189
+ limit,
4190
+ offset: driverOffset,
3965
4191
  orderBy: params?.orderBy?.[0],
3966
4192
  order: params?.orderBy?.[1],
3967
4193
  searchString: params?.searchString
3968
4194
  }, params?.include) : await driver.fetchCollection({
3969
4195
  path: slug,
3970
- limit: params?.limit,
3971
- offset: params?.offset,
4196
+ limit,
4197
+ offset: driverOffset,
3972
4198
  filter,
4199
+ logical: params?.logical,
3973
4200
  orderBy: params?.orderBy?.[0],
3974
4201
  order: params?.orderBy?.[1],
3975
4202
  searchString: params?.searchString
@@ -3979,7 +4206,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3979
4206
  if (driver.count) {
3980
4207
  total = await driver.count({
3981
4208
  path: slug,
3982
- filter
4209
+ filter,
4210
+ logical: params?.logical,
4211
+ searchString: params?.searchString
3983
4212
  });
3984
4213
  hasMore = offset + rows.length < total;
3985
4214
  }
@@ -4031,30 +4260,48 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4031
4260
  values: {}
4032
4261
  } });
4033
4262
  },
4263
+ updateMany: driver.updateMany ? async (updates) => {
4264
+ return (await driver.updateMany({
4265
+ path: slug,
4266
+ updates: updates.map((u) => ({
4267
+ id: u.id,
4268
+ values: u.data
4269
+ }))
4270
+ })).map((row) => rowToEntity(row, slug, getPks()));
4271
+ } : void 0,
4272
+ deleteMany: driver.deleteMany ? async (ids) => {
4273
+ await driver.deleteMany({
4274
+ path: slug,
4275
+ ids
4276
+ });
4277
+ } : void 0,
4034
4278
  count: driver.count ? async (params) => {
4035
4279
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
4036
4280
  return driver.count({
4037
4281
  path: slug,
4038
- filter
4282
+ filter,
4283
+ logical: params?.logical,
4284
+ searchString: params?.searchString
4039
4285
  });
4040
4286
  } : void 0,
4041
4287
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
4042
- const limit = params?.limit ?? 20;
4043
- const offset = params?.offset ?? 0;
4288
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
4044
4289
  const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
4045
4290
  return driver.listenCollection({
4046
4291
  path: slug,
4047
- limit: params?.limit,
4048
- offset: params?.offset,
4292
+ limit,
4293
+ offset: driverOffset,
4049
4294
  filter: params?.where,
4295
+ logical: params?.logical,
4050
4296
  orderBy: params?.orderBy?.[0],
4051
4297
  order: params?.orderBy?.[1],
4052
4298
  searchString: params?.searchString,
4299
+ searchExplain: params?.searchExplain,
4053
4300
  onUpdate: (entities) => {
4054
4301
  onUpdate({
4055
4302
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
4056
4303
  meta: {
4057
- total: entities.length,
4304
+ total: offset + entities.length,
4058
4305
  limit,
4059
4306
  offset,
4060
4307
  hasMore: entities.length >= limit
@@ -4087,8 +4334,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4087
4334
  offset(count) {
4088
4335
  return new QueryBuilder(accessor).offset(count);
4089
4336
  },
4090
- search(searchString) {
4091
- return new QueryBuilder(accessor).search(searchString);
4337
+ search(searchString, options) {
4338
+ return new QueryBuilder(accessor).search(searchString, options);
4339
+ },
4340
+ vectorSearch(property, vector, options) {
4341
+ return new QueryBuilder(accessor).vectorSearch(property, vector, options);
4092
4342
  },
4093
4343
  include(...relations) {
4094
4344
  return new QueryBuilder(accessor).include(...relations);
@@ -4176,8 +4426,18 @@ var SdkQueryBuilder = class {
4176
4426
  this.params.offset = count;
4177
4427
  return this;
4178
4428
  }
4179
- search(searchString) {
4429
+ search(searchString, options) {
4180
4430
  this.params.searchString = searchString;
4431
+ if (options?.explain !== void 0) this.params.searchExplain = options.explain;
4432
+ return this;
4433
+ }
4434
+ vectorSearch(property, vector, options) {
4435
+ this.params.vectorSearch = {
4436
+ property,
4437
+ vector,
4438
+ ...options?.distance !== void 0 && { distance: options.distance },
4439
+ ...options?.threshold !== void 0 && { threshold: options.threshold }
4440
+ };
4181
4441
  return this;
4182
4442
  }
4183
4443
  include(...relations) {
@@ -4231,9 +4491,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
4231
4491
  async update(id, data) {
4232
4492
  return entityToRow(await snap.update(id, data));
4233
4493
  },
4494
+ async updateMany(updates) {
4495
+ if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
4496
+ if (updates.length === 0) return [];
4497
+ if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
4498
+ return (await snap.updateMany(updates.map((u) => ({
4499
+ id: u.id,
4500
+ data: u.data
4501
+ })))).map(entityToRow);
4502
+ },
4234
4503
  delete(id) {
4235
4504
  return snap.delete(id);
4236
4505
  },
4506
+ async deleteMany(ids) {
4507
+ if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
4508
+ if (ids.length === 0) return;
4509
+ if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
4510
+ await snap.deleteMany(ids);
4511
+ },
4237
4512
  count: snap.count ? (params) => snap.count(params) : void 0,
4238
4513
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
4239
4514
  data: res.data.map(entityToRow),
@@ -4249,6 +4524,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
4249
4524
  limit: (count) => new SdkQueryBuilder(client).limit(count),
4250
4525
  offset: (count) => new SdkQueryBuilder(client).offset(count),
4251
4526
  search: (searchString) => new SdkQueryBuilder(client).search(searchString),
4527
+ vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
4252
4528
  include: (...relations) => new SdkQueryBuilder(client).include(...relations)
4253
4529
  };
4254
4530
  return client;
@@ -4294,6 +4570,502 @@ function buildSdkData(driver) {
4294
4570
  return wrapAsSdkData(buildRebaseData(driver));
4295
4571
  }
4296
4572
  //#endregion
4297
- export { toSnakeCase as A, normalizeToEntityRelation as C, getPolicyNamesForRule as D, legacyForeignKeyName as E, Vector as F, DEFAULT_ONE_OF_VALUE as M, hasForeignKeyOnTarget as N, mergeDeep as O, isManyToMany as P, createRelationRefWithData as S, generateForeignKeyName as T, buildCompositeId as _, getJunctionSecurityRules as a, parseIdValues as b, policyToPostgres as c, findRelation as d, getColumnName as f, resolveCollectionRelations as g, getTableVarName as h, getJunctionCollectionConfig as i, DEFAULT_ONE_OF_TYPE as j, camelCase as k, securityRuleToConditions as l, getTableName as m, CollectionRegistry as n, resolveJunctionSpecs as o, getEnumVarName as p, resolveStringColumnLength as r, getEffectiveSecurityRules as s, buildSdkData as t, findAnonymousGrants as u, getDeclaredPrimaryKeys as v, updateDateAutoValues as w, createRelationRef as x, isAddressableId as y };
4573
+ //#region src/schema/search-column.ts
4574
+ /**
4575
+ * The one place a collection's `search` block becomes SQL.
4576
+ *
4577
+ * Four things describe a Postgres table in this codebase — the DDL generator,
4578
+ * the Drizzle schema generator, the runtime table builder for BaaS mode, and
4579
+ * the boot-time schema ensure — and each of them has, at some point, described
4580
+ * a column differently from the others. The `varchar(255)` note in
4581
+ * `generate-postgres-ddl-logic` is one such scar: the same property produced a
4582
+ * capped column down one path and an uncapped one down the other, and nothing
4583
+ * failed until a user hit the cap.
4584
+ *
4585
+ * So the search column is not implemented four times. It is computed once,
4586
+ * here, and every generator renders the same {@link SearchColumnSpec}. There is
4587
+ * a test asserting exactly that (`search-column-contract.test.ts`); the point of
4588
+ * this module is that the test has something to assert *about*.
4589
+ *
4590
+ * ## Why the expressions look the way they do
4591
+ *
4592
+ * A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
4593
+ * Postgres is stricter here than intuition. Verified against PostgreSQL 18:
4594
+ *
4595
+ * | expression | immutable |
4596
+ * |-----------------------------------------|-----------|
4597
+ * | `to_tsvector('spanish', col)` | yes |
4598
+ * | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
4599
+ * | `array_to_string(col, ' ')` | **no** |
4600
+ * | `col::text` on `text[]` | **no** |
4601
+ * | `to_jsonb(col)` | **no** |
4602
+ * | `unaccent(col)` | **no** — dictionary lookup is STABLE |
4603
+ * | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
4604
+ * | `setweight(...) || setweight(...)` | yes |
4605
+ *
4606
+ * Three of the four things a real search column needs are therefore unavailable
4607
+ * directly, which is why {@link searchHelperFunctions} exists: each wraps a
4608
+ * stable built-in in an SQL function declared IMMUTABLE. That declaration is a
4609
+ * promise, and it is a true one for these three — array joining, JSON string
4610
+ * extraction and accent folding are all deterministic for a given input; the
4611
+ * built-ins are marked stable only because they must account for element types
4612
+ * and dictionaries in general.
4613
+ *
4614
+ * The alternative was to skip `unaccent` and text arrays entirely. That is not
4615
+ * a real option in an accented language: Postgres stems `auditoría` to
4616
+ * `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
4617
+ * typed without accents misses every row that carries them.
4618
+ */
4619
+ /** Schema-qualified so a collection outside `public` still resolves them. */
4620
+ var HELPER_SCHEMA = "public";
4621
+ /**
4622
+ * Names of the helper functions. Frozen: they are recorded in the stored
4623
+ * generation expression of every search column ever created, so renaming one
4624
+ * orphans every table that already has a search column.
4625
+ */
4626
+ var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
4627
+ var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
4628
+ /** Raised when a `search` block names something that cannot be searched. */
4629
+ var SearchConfigError = class extends Error {
4630
+ constructor(message) {
4631
+ super(message);
4632
+ this.name = "SearchConfigError";
4633
+ }
4634
+ };
4635
+ /** The `search` block of a collection, or undefined when it has none. */
4636
+ var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
4637
+ /**
4638
+ * Refuse a `search` block on a collection this engine does not store.
4639
+ *
4640
+ * The type only permits one on a `PostgresCollectionConfig`, so TypeScript
4641
+ * already stops the ordinary case. This catches the rest — a JS config, a cast,
4642
+ * a collection whose `engine` was changed after the block was written — because
4643
+ * the alternative is the exact failure the block exists to prevent: a developer
4644
+ * who declared what to index, saw no error, and got the substring fallback.
4645
+ *
4646
+ * Called with *every* collection, before the Postgres ones are filtered out.
4647
+ */
4648
+ var assertSearchIsPostgresOnly = (collections) => {
4649
+ for (const collection of collections) {
4650
+ if (isPostgresCollectionConfig(collection)) continue;
4651
+ if (!collection.search) continue;
4652
+ const engine = collection.engine ?? "non-postgres";
4653
+ throw new SearchConfigError(`${collection.slug}.search: full-text search is a Postgres feature, and this collection is served by \`${engine}\`. Remove the block — it would otherwise look configured while \`.search()\` kept using the default substring match.`);
4654
+ }
4655
+ };
4656
+ var columnNameOf = (propName, prop) => prop && "columnName" in prop && typeof prop.columnName === "string" ? prop.columnName : toSnakeCase(propName);
4657
+ /**
4658
+ * Classify a property for search purposes.
4659
+ *
4660
+ * Deliberately narrower than `getSqlColumnType`: search only cares whether a
4661
+ * value reaches text, and the mapping from property to *physical* type is
4662
+ * asserted against `getSqlColumnType` in the contract test rather than
4663
+ * duplicated here.
4664
+ *
4665
+ * Returns null for anything that is not text-bearing, which the caller turns
4666
+ * into a boot error naming the property.
4667
+ */
4668
+ var classify = (prop) => {
4669
+ switch (prop.type) {
4670
+ case "string": {
4671
+ const sp = prop;
4672
+ if (sp.enum) return {
4673
+ kind: "text",
4674
+ reason: "enum"
4675
+ };
4676
+ if (sp.isId === "uuid" || sp.columnType === "uuid") return {
4677
+ kind: "text",
4678
+ reason: "uuid"
4679
+ };
4680
+ return { kind: "text" };
4681
+ }
4682
+ case "map":
4683
+ if (prop.columnType === "json") return {
4684
+ kind: "jsonb",
4685
+ reason: "json"
4686
+ };
4687
+ return { kind: "jsonb" };
4688
+ case "array": {
4689
+ const ap = prop;
4690
+ let colType = ap.columnType;
4691
+ if (!colType && ap.of && !Array.isArray(ap.of)) {
4692
+ const of = ap.of;
4693
+ if (of.type === "string") colType = "text[]";
4694
+ else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
4695
+ else if (of.type === "boolean") colType = "boolean[]";
4696
+ }
4697
+ if (colType === "text[]") return { kind: "text_array" };
4698
+ if (colType === "json") return {
4699
+ kind: "jsonb",
4700
+ reason: "json"
4701
+ };
4702
+ if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
4703
+ kind: "text_array",
4704
+ reason: "non_text_array"
4705
+ };
4706
+ return { kind: "jsonb" };
4707
+ }
4708
+ default: return null;
4709
+ }
4710
+ };
4711
+ var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
4712
+ /** SQL reading one field as plain text, before normalization. */
4713
+ var rawTextSql = (field) => {
4714
+ const col = `"${field.column}"`;
4715
+ if (field.kind === "text") return `coalesce(${col}, '')`;
4716
+ if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
4717
+ return `${SEARCH_TEXT_FN}(coalesce(${field.jsonPath.length === 0 ? col : field.jsonPath.length === 1 ? `${col} -> ${quote(field.jsonPath[0])}` : `${col} #> ${quote(`{${field.jsonPath.join(",")}}`)}`}, '{}'::jsonb))`;
4718
+ };
4719
+ var quote = (v) => `'${v.replace(/'/g, "''")}'`;
4720
+ /**
4721
+ * Resolve and validate one declared field path.
4722
+ *
4723
+ * A path that does not resolve throws. The whole point of an explicit block is
4724
+ * that the author knows what is indexed; a silently dropped field would make it
4725
+ * a guess again, and the failure — a search that returns nothing for content
4726
+ * that is plainly in the row — is invisible from the outside.
4727
+ */
4728
+ var resolveField = (entry, collection, cfg) => {
4729
+ const path = typeof entry === "string" ? entry : entry.path;
4730
+ const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? "B";
4731
+ const where = `${collection.slug}.search`;
4732
+ if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
4733
+ const [head, ...rest] = path.split(".");
4734
+ const prop = collection.properties?.[head];
4735
+ if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
4736
+ const classified = classify(prop);
4737
+ if (!classified) throw new SearchConfigError(`${where}: "${path}" is a \`${prop.type}\` property, which holds no text to search. Searchable kinds are \`string\`, \`string[]\` and \`map\` (or a path inside one).`);
4738
+ if (classified.reason === "enum") throw new SearchConfigError(`${where}: "${path}" is an enum. Enums are a fixed vocabulary — filter on them with \`where\` instead, which is exact and uses an index.`);
4739
+ if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
4740
+ if (classified.reason === "json") throw new SearchConfigError(`${where}: "${path}" is a \`json\` column, and the cast from \`json\` to \`jsonb\` is not immutable, so it cannot feed a generated column. Declare the property as \`jsonb\` (the default) to search it.`);
4741
+ if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
4742
+ if (rest.length > 0 && classified.kind !== "jsonb") throw new SearchConfigError(`${where}: "${path}" addresses a path inside "${head}", but "${head}" is a \`${prop.type}\` property, not a \`map\`. Only map properties have paths inside them.`);
4743
+ const column = columnNameOf(head, prop);
4744
+ const textSql = normalize(rawTextSql({
4745
+ column,
4746
+ jsonPath: rest,
4747
+ kind: classified.kind
4748
+ }), cfg.unaccent === true);
4749
+ const language = cfg.language ?? "simple";
4750
+ return {
4751
+ path,
4752
+ column,
4753
+ jsonPath: rest,
4754
+ kind: classified.kind,
4755
+ weight,
4756
+ sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
4757
+ textSql
4758
+ };
4759
+ };
4760
+ /**
4761
+ * Build the full spec for a collection, or undefined when it has not opted in.
4762
+ *
4763
+ * Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
4764
+ * at boot surface that as a startup failure — a search block that half-works is
4765
+ * worse than one that refuses.
4766
+ */
4767
+ var buildSearchColumnSpec = (collection) => {
4768
+ const cfg = getSearchConfig(collection);
4769
+ if (!cfg) return void 0;
4770
+ if (!Array.isArray(cfg.fields) || cfg.fields.length === 0) throw new SearchConfigError(`${collection.slug}.search: \`fields\` is empty. Name the properties to index, or remove the \`search\` block to keep the default ILIKE behaviour.`);
4771
+ const table = getTableName(collection);
4772
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
4773
+ const column = cfg.column ?? "search_vector";
4774
+ if (collection.properties?.[column]) throw new SearchConfigError(`${collection.slug}.search: the generated column "${column}" collides with a declared property of the same name. Set \`search.column\` to something else.`);
4775
+ const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
4776
+ const seen = /* @__PURE__ */ new Set();
4777
+ for (const f of fields) {
4778
+ if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
4779
+ seen.add(f.path);
4780
+ }
4781
+ const extensions = [];
4782
+ if (cfg.unaccent) extensions.push("unaccent");
4783
+ if (cfg.fuzzy) extensions.push("pg_trgm");
4784
+ const spec = {
4785
+ schema,
4786
+ table,
4787
+ column,
4788
+ language: cfg.language ?? "simple",
4789
+ unaccent: cfg.unaccent === true,
4790
+ fields,
4791
+ expression: fields.map((f) => f.sql).join(" || "),
4792
+ indexName: toPostgresIdentifier(`${table}_${column}_gin`),
4793
+ extensions
4794
+ };
4795
+ if (cfg.fuzzy) {
4796
+ const fuzzyColumn = `${column}_text`;
4797
+ if (collection.properties?.[fuzzyColumn]) throw new SearchConfigError(`${collection.slug}.search: \`fuzzy\` needs the column "${fuzzyColumn}", which collides with a declared property. Set \`search.column\` to something else.`);
4798
+ spec.fuzzy = {
4799
+ column: fuzzyColumn,
4800
+ expression: fields.map((f) => f.textSql).join(" || ' ' || "),
4801
+ indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
4802
+ threshold: cfg.fuzzyThreshold ?? .3
4803
+ };
4804
+ }
4805
+ return spec;
4806
+ };
4807
+ /**
4808
+ * The IMMUTABLE wrappers the generated expressions call.
4809
+ *
4810
+ * `CREATE OR REPLACE` so a boot against an existing database is a no-op rather
4811
+ * than an error, and idempotent for the same reason every other boot-time DDL
4812
+ * statement here is.
4813
+ *
4814
+ * The bodies are stable built-ins wrapped in an immutable promise — see the
4815
+ * module comment for why that promise is sound. `STRICT` matters: it makes NULL
4816
+ * in mean NULL out without executing the body, which is what the `coalesce` at
4817
+ * each call site then absorbs.
4818
+ */
4819
+ var searchHelperFunctions = (spec) => {
4820
+ const statements = [`CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(text[]) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT array_to_string($1, ' ') $$;`, `CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(jsonb) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT coalesce(string_agg(v, ' '), '')\n FROM jsonb_array_elements_text(jsonb_path_query_array($1, 'strict $.**?(@.type() == "string")')) AS v $$;`];
4821
+ if (spec.unaccent) statements.push(`CREATE OR REPLACE FUNCTION ${SEARCH_UNACCENT_FN}(text) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT ${HELPER_SCHEMA}.unaccent('${HELPER_SCHEMA}.unaccent'::regdictionary, $1) $$;`);
4822
+ return statements;
4823
+ };
4824
+ /**
4825
+ * `CREATE EXTENSION` statements the spec's expressions depend on.
4826
+ *
4827
+ * `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified
4828
+ * `CREATE EXTENSION` installs into the first schema on `search_path`, which
4829
+ * defaults to `"$user", public` — and the scaffold's database role is named
4830
+ * `rebase`, the same as the schema the generator creates one statement earlier.
4831
+ * So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
4832
+ * dictionary in `rebase`, and every reference to `public.unaccent` below fails
4833
+ * with "text search dictionary does not exist". Observed, not theorised.
4834
+ */
4835
+ var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
4836
+ /**
4837
+ * Index statements for the spec.
4838
+ *
4839
+ * `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
4840
+ * SQL file replayed as one unit — a migration, or `search.sql` — where a
4841
+ * concurrent build is not allowed. The boot-time ensure path runs statement by
4842
+ * statement against tables that are live and populated, and uses the
4843
+ * concurrent form instead; see `ensureSearchColumns`.
4844
+ */
4845
+ var searchIndexStatements = (spec) => {
4846
+ const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
4847
+ if (spec.fuzzy) statements.push(`CREATE INDEX IF NOT EXISTS "${spec.fuzzy.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.fuzzy.column}" ${HELPER_SCHEMA}.gin_trgm_ops);`);
4848
+ return statements;
4849
+ };
4850
+ /**
4851
+ * Marker on the comment of every generated search column this module creates.
4852
+ *
4853
+ * Versioned because the fingerprint below is only comparable against itself: a
4854
+ * future change to how it is computed has to read as "not stamped by this
4855
+ * version" rather than as drift on every existing column.
4856
+ */
4857
+ var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
4858
+ /**
4859
+ * A stable fingerprint of one generated column's expression.
4860
+ *
4861
+ * Why a stamp rather than reading the expression back: Postgres stores a
4862
+ * generated column's expression *parsed*, and hands it back deparsed — casts
4863
+ * made explicit, identifiers requoted, schema qualifications added or dropped
4864
+ * according to `search_path`. Comparing that text to the text we generated
4865
+ * would report drift on wording, and this comparison decides whether a boot
4866
+ * refuses, so a false positive is an outage. The stamp is written by the same
4867
+ * code that writes the column, so equality means what it says.
4868
+ */
4869
+ var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
4870
+ /**
4871
+ * The stamps for a spec's generated columns — one per column, never shared.
4872
+ *
4873
+ * Per column on purpose: turning `fuzzy` on adds a second column and changes
4874
+ * nothing about the first, and a spec-wide fingerprint would report the
4875
+ * untouched `tsvector` column as drifted and refuse a boot over a change that
4876
+ * is purely additive.
4877
+ */
4878
+ var searchColumnStamps = (spec) => {
4879
+ const stamp = (column, expression) => {
4880
+ const fingerprint = searchExpressionFingerprint(expression);
4881
+ return {
4882
+ column,
4883
+ expression,
4884
+ fingerprint,
4885
+ sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
4886
+ };
4887
+ };
4888
+ const stamps = [stamp(spec.column, spec.expression)];
4889
+ if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
4890
+ return stamps;
4891
+ };
4892
+ /**
4893
+ * The generated column names a collection's search block adds, if any.
4894
+ *
4895
+ * These are physical columns on the table, so `SELECT *` returns them. They are
4896
+ * an index in column form — a list of lexeme positions, or a concatenation of
4897
+ * every searchable field on the row — and nothing outside the query planner has
4898
+ * any use for them. Left in, every list response carries a second, larger copy
4899
+ * of the row's text.
4900
+ */
4901
+ var searchColumnNames = (collection) => {
4902
+ let spec;
4903
+ try {
4904
+ spec = buildSearchColumnSpec(collection);
4905
+ } catch {
4906
+ return [];
4907
+ }
4908
+ if (!spec) return [];
4909
+ return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
4910
+ };
4911
+ /**
4912
+ * True for a column whose type only ever holds a search index.
4913
+ *
4914
+ * Independent of any collection config on purpose: an introspected database
4915
+ * (BaaS mode) can carry a `tsvector` column this framework never created —
4916
+ * Pagila's `film.fulltext` is the canonical one — and it should not be returned
4917
+ * to callers either. `isDerivedIndexColumn` already keeps such a column out of
4918
+ * the *properties*; this keeps it out of the *rows*.
4919
+ */
4920
+ var isSearchIndexColumn = (column) => {
4921
+ const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
4922
+ return sqlType === "tsvector" || sqlType === "tsquery";
4923
+ };
4924
+ /**
4925
+ * A drizzle select projection over `table` with the search columns dropped.
4926
+ *
4927
+ * Returns undefined when nothing needs dropping, so the common case keeps using
4928
+ * a plain `select()` and this stays invisible in the generated SQL.
4929
+ */
4930
+ var visibleColumnProjection = (tableColumns, collection) => {
4931
+ const excluded = excludedColumnNames(tableColumns, collection);
4932
+ if (!tableColumns || excluded.length === 0) return void 0;
4933
+ const projection = {};
4934
+ for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
4935
+ return projection;
4936
+ };
4937
+ /** The same exclusion as a drizzle `db.query` `columns` denylist. */
4938
+ var hiddenColumnsOption = (tableColumns, collection) => {
4939
+ const excluded = excludedColumnNames(tableColumns, collection);
4940
+ if (excluded.length === 0) return void 0;
4941
+ return Object.fromEntries(excluded.map((name) => [name, false]));
4942
+ };
4943
+ /**
4944
+ * The columns to keep out of a response, by name.
4945
+ *
4946
+ * `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
4947
+ * for anything that is not a real drizzle table — a stub in a test, a derived
4948
+ * or nested path with no table behind it. Nothing to exclude is the right
4949
+ * answer there, and it has to be an answer rather than a throw: this runs on
4950
+ * the read path of every collection, opted in or not.
4951
+ */
4952
+ var excludedColumnNames = (tableColumns, collection) => {
4953
+ if (!tableColumns || typeof tableColumns !== "object") return [];
4954
+ const byName = new Set(collection ? searchColumnNames(collection) : []);
4955
+ return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
4956
+ };
4957
+ //#endregion
4958
+ //#region src/schema/auth-users-columns.ts
4959
+ /**
4960
+ * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
4961
+ * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
4962
+ * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
4963
+ * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
4964
+ * table that already holds a longer row.
4965
+ */
4966
+ var AUTH_USERS_COLUMNS = [
4967
+ {
4968
+ column: "email",
4969
+ type: "TEXT",
4970
+ notNull: true
4971
+ },
4972
+ {
4973
+ column: "display_name",
4974
+ type: "TEXT"
4975
+ },
4976
+ {
4977
+ column: "photo_url",
4978
+ type: "TEXT"
4979
+ },
4980
+ {
4981
+ column: "roles",
4982
+ type: "TEXT[]",
4983
+ default: "'{}'",
4984
+ notNull: true
4985
+ },
4986
+ {
4987
+ column: "password_hash",
4988
+ type: "TEXT"
4989
+ },
4990
+ {
4991
+ column: "email_verified",
4992
+ type: "BOOLEAN",
4993
+ default: "FALSE",
4994
+ notNull: true
4995
+ },
4996
+ {
4997
+ column: "email_verification_token",
4998
+ type: "TEXT"
4999
+ },
5000
+ {
5001
+ column: "email_verification_sent_at",
5002
+ type: "TIMESTAMP WITH TIME ZONE"
5003
+ },
5004
+ {
5005
+ column: "is_anonymous",
5006
+ type: "BOOLEAN",
5007
+ default: "FALSE",
5008
+ notNull: true
5009
+ },
5010
+ {
5011
+ column: "metadata",
5012
+ type: "JSONB",
5013
+ default: "'{}'",
5014
+ notNull: true
5015
+ },
5016
+ {
5017
+ column: "tokens_valid_after",
5018
+ type: "TIMESTAMP WITH TIME ZONE"
5019
+ },
5020
+ {
5021
+ column: "created_at",
5022
+ type: "TIMESTAMP WITH TIME ZONE",
5023
+ default: "NOW()",
5024
+ notNull: true
5025
+ },
5026
+ {
5027
+ column: "updated_at",
5028
+ type: "TIMESTAMP WITH TIME ZONE",
5029
+ default: "NOW()",
5030
+ notNull: true
5031
+ }
5032
+ ];
5033
+ var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
5034
+ /** Type + inline constraints, as they appear after the column name. */
5035
+ function authUsersColumnSql(spec) {
5036
+ return [
5037
+ spec.type,
5038
+ spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
5039
+ spec.notNull ? "NOT NULL" : ""
5040
+ ].filter(Boolean).join(" ");
5041
+ }
5042
+ /**
5043
+ * The auth-owned definition for a physical column name, or `undefined` when
5044
+ * auth does not own it.
5045
+ *
5046
+ * Callers pass the RESOLVED column name (after `columnName` mapping), because
5047
+ * that is the only name the three creators agree on: the scaffold's users
5048
+ * collection spells the property `displayName` and the column `display_name`.
5049
+ */
5050
+ function authUsersColumnDefinition(column) {
5051
+ const spec = BY_COLUMN.get(column);
5052
+ return spec ? authUsersColumnSql(spec) : void 0;
5053
+ }
5054
+ /**
5055
+ * Whether a collection is an auth collection, i.e. whether the definitions in
5056
+ * this module apply to its table at all.
5057
+ *
5058
+ * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
5059
+ * that one takes a `CollectionConfig`, this one is called from DDL code paths
5060
+ * that hold looser objects, and both spellings must accept `auth: true` as well
5061
+ * as `auth: { enabled: true }`.
5062
+ */
5063
+ function isAuthCollection(collection) {
5064
+ const auth = collection?.auth;
5065
+ if (auth === true) return true;
5066
+ return typeof auth === "object" && auth !== null && auth.enabled === true;
5067
+ }
5068
+ //#endregion
5069
+ export { isManyToMany as $, getTableName as A, normalizeToEntityRelation as B, getEffectiveSecurityRules as C, findRelation as D, findAnonymousGrants as E, isAddressableId as F, getPolicyNamesForRule as G, generateForeignKeyName as H, parseIdValues as I, camelCase as J, isPrototypePollutingKey as K, sortCollectionsBySlug as L, resolveCollectionRelations as M, buildCompositeId as N, getColumnName as O, getDeclaredPrimaryKeys as P, hasForeignKeyOnTarget as Q, createRelationRef as R, resolveJunctionSpecs as S, securityRuleToConditions as T, legacyForeignKeyName as U, updateDateAutoValues as V, toPostgresIdentifier as W, DEFAULT_ONE_OF_TYPE as X, toSnakeCase as Y, DEFAULT_ONE_OF_VALUE as Z, CollectionRegistry as _, SEARCH_STAMP_PREFIX as a, getJunctionCollectionConfig as b, assertSearchIsPostgresOnly as c, searchColumnStamps as d, Vector as et, searchExtensionStatements as f, buildSdkData as g, visibleColumnProjection as h, isAuthCollection as i, getTableVarName as j, getEnumVarName as k, buildSearchColumnSpec as l, searchIndexStatements as m, authUsersColumnDefinition as n, SEARCH_TEXT_FN as o, searchHelperFunctions as p, mergeDeep as q, authUsersColumnSql as r, SEARCH_UNACCENT_FN as s, AUTH_USERS_COLUMNS as t, hiddenColumnsOption as u, relationalCollections as v, policyToPostgres as w, getJunctionSecurityRules as x, resolveStringColumnLength as y, createRelationRefWithData as z };
4298
5070
 
4299
- //# sourceMappingURL=src-DlPBctw_.js.map
5071
+ //# sourceMappingURL=auth-users-columns-CBEOeYqa.js.map