@rebasepro/server-postgres 0.13.1-canary.gcef69d4 → 0.13.1-canary.gf57a27e

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 (66) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/{src-Bs2ZzSi4.js → auth-users-columns-Dt9g712t.js} +631 -34
  3. package/dist/auth-users-columns-Dt9g712t.js.map +1 -0
  4. package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
  5. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
  6. package/dist/cli-helpers.d.ts +57 -1
  7. package/dist/{ensure-collection-policies-25nZOd87.js → ensure-collection-policies-CwYUliAa.js} +2 -2
  8. package/dist/{ensure-collection-policies-25nZOd87.js.map → ensure-collection-policies-CwYUliAa.js.map} +1 -1
  9. package/dist/{ensure-collection-tables-DRkgVBV5.js → ensure-collection-tables-DRxaUG96.js} +86 -15
  10. package/dist/ensure-collection-tables-DRxaUG96.js.map +1 -0
  11. package/dist/index.es.js +658 -133
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/{policy-CeA1JcxP.js → policy-CPkCqVTz.js} +4 -4
  14. package/dist/policy-CPkCqVTz.js.map +1 -0
  15. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  16. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  17. package/dist/schema/auth-users-columns.d.ts +97 -0
  18. package/dist/schema/ensure-collection-tables.d.ts +2 -2
  19. package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
  20. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  21. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  22. package/dist/schema/search-column.d.ts +199 -0
  23. package/dist/security/rls-enforcement.d.ts +53 -2
  24. package/dist/services/FetchService.d.ts +15 -0
  25. package/dist/services/dataService.d.ts +1 -0
  26. package/dist/services/realtimeService.d.ts +2 -0
  27. package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
  28. package/dist/src-C_wvdMnl.js.map +1 -0
  29. package/dist/utils/drizzle-conditions.d.ts +71 -2
  30. package/dist/{websocket-B2LsrINK.js → websocket-D0TBU3ia.js} +3 -3
  31. package/dist/websocket-D0TBU3ia.js.map +1 -0
  32. package/package.json +8 -7
  33. package/src/PostgresBackendDriver.ts +165 -3
  34. package/src/PostgresBootstrapper.ts +41 -2
  35. package/src/auth/ensure-tables.ts +185 -86
  36. package/src/cli-helpers.ts +129 -10
  37. package/src/cli.ts +153 -30
  38. package/src/data-transformer.ts +18 -0
  39. package/src/history/ensure-history-table.ts +7 -0
  40. package/src/schema/auth-users-columns.ts +131 -0
  41. package/src/schema/doctor.ts +7 -5
  42. package/src/schema/ensure-collection-tables.ts +165 -20
  43. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  44. package/src/schema/generate-postgres-ddl-logic.ts +266 -15
  45. package/src/schema/generate-postgres-ddl.ts +25 -2
  46. package/src/schema/generated-schema-staleness.ts +7 -2
  47. package/src/schema/non-sql-collections.test.ts +131 -0
  48. package/src/schema/rls-bootstrap-sql.ts +288 -0
  49. package/src/schema/search-column.ts +558 -0
  50. package/src/security/anonymous-grants.test.ts +4 -2
  51. package/src/security/rls-enforcement.ts +141 -3
  52. package/src/services/BranchService.ts +5 -0
  53. package/src/services/FetchService.ts +138 -15
  54. package/src/services/RelationService.ts +2 -1
  55. package/src/services/channel-history.ts +8 -0
  56. package/src/services/channel-presence.ts +6 -0
  57. package/src/services/dataService.ts +1 -0
  58. package/src/services/realtimeService.ts +10 -4
  59. package/src/utils/drizzle-conditions.ts +223 -2
  60. package/dist/ensure-collection-tables-DRkgVBV5.js.map +0 -1
  61. package/dist/policy-CeA1JcxP.js.map +0 -1
  62. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  63. package/dist/src-Bs2ZzSi4.js.map +0 -1
  64. package/dist/src-DoU9yPqq.js.map +0 -1
  65. package/dist/websocket-B2LsrINK.js.map +0 -1
  66. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -2,8 +2,8 @@ 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 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";
6
+ import { n as ANONYMOUS_USER_IDS, r as policy, t as ANONYMOUS_USER_ID } from "./policy-CPkCqVTz.js";
7
7
  //#region ../types/src/types/entities.ts
8
8
  /**
9
9
  * Class used to create a reference to a entity in a different path
@@ -1408,6 +1408,28 @@ function legacyForeignKeyName(name) {
1408
1408
  const snake = toSnakeCase(name);
1409
1409
  return `${snake.endsWith("s") ? snake.slice(0, -1) : snake}_id`;
1410
1410
  }
1411
+ /**
1412
+ * Truncate an identifier to what Postgres will actually store.
1413
+ *
1414
+ * Postgres silently truncates identifiers at NAMEDATALEN-1 = 63 **bytes**, so a
1415
+ * name generated longer than that is not the name the database ends up holding.
1416
+ * Anything that later looks the object up by the name it generated then misses.
1417
+ *
1418
+ * Byte length, not string length: NAMEDATALEN is a byte bound, and a multi-byte
1419
+ * character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.
1420
+ *
1421
+ * `TextEncoder` rather than `Buffer`, which is not a matter of taste: `Buffer`
1422
+ * is a Node global, and this package is imported by browser-facing ones. It
1423
+ * typechecked only where `@types/node` happened to be in scope, so
1424
+ * `packages/codegen` — whose tsconfig is `lib: ["ESNext", "dom"]` — could not
1425
+ * compile the file at all, and both of its suites failed to run. `TextEncoder`
1426
+ * and `TextDecoder` are standard in both runtimes and need no ambient types.
1427
+ */
1428
+ function toPostgresIdentifier(name) {
1429
+ const bytes = new TextEncoder().encode(name);
1430
+ if (bytes.byteLength <= 63) return name;
1431
+ return new TextDecoder("utf-8").decode(bytes.subarray(0, 63)).replace(/�+$/, "");
1432
+ }
1411
1433
  //#endregion
1412
1434
  //#region ../common/src/util/entities.ts
1413
1435
  /**
@@ -1431,10 +1453,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
1431
1453
  * have `id` and `path` fields — these are relation-shaped objects from
1432
1454
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
1433
1455
  *
1456
+ * When `targetPath` is given, also accepts a bare id. A relation column is a
1457
+ * foreign key, and the REST layer returns it as the scalar it is; only some
1458
+ * fetch paths hydrate it into an object. Which form a caller sees therefore
1459
+ * depends on how the row was loaded, and a caller that only accepted objects
1460
+ * reported half of its own data as a type error. The declared target is the
1461
+ * missing half: with it, an id is a relation that has not been fetched yet.
1462
+ *
1434
1463
  * Returns null if the value cannot be coerced.
1435
1464
  */
1436
- function normalizeToEntityRelation(value, propertyType) {
1465
+ function normalizeToEntityRelation(value, propertyType, targetPath) {
1437
1466
  if (value instanceof EntityRelation) return value;
1467
+ if (targetPath && (typeof value === "string" || typeof value === "number")) {
1468
+ if (value === "") return null;
1469
+ return new EntityRelation(value, targetPath);
1470
+ }
1438
1471
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1439
1472
  const obj = value;
1440
1473
  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;
@@ -1972,9 +2005,9 @@ function isKeywordAt(upper, i, keyword) {
1972
2005
  *
1973
2006
  * This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
1974
2007
  * `AND` inside
1975
- * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = auth.uid())`
2008
+ * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
1976
2009
  * split the expression, and re-emitting the halves produced
1977
- * `(EXISTS (...) AND m.user_id = auth.uid())`
2010
+ * `(EXISTS (...) AND m.user_id = rebase.uid())`
1978
2011
  * where `m` is no longer in scope — SQL that Postgres rejects outright with
1979
2012
  * "missing FROM-clause entry for table". Returning null instead keeps such a
1980
2013
  * clause as a `raw` expression, which round-trips verbatim.
@@ -2048,15 +2081,15 @@ function stripOuterParens(sql) {
2048
2081
  }
2049
2082
  }
2050
2083
  function sqlToPolicy(sql) {
2051
- const trimmed = stripOuterParens(sql.trim());
2084
+ const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
2052
2085
  if (trimmed.toLowerCase() === "true") return policy.true();
2053
2086
  if (trimmed.toLowerCase() === "false") return policy.false();
2054
- const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
2087
+ const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
2055
2088
  if (overlapMatch) {
2056
2089
  const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
2057
2090
  return policy.rolesOverlap(roles);
2058
2091
  }
2059
- const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
2092
+ const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
2060
2093
  if (containMatch) {
2061
2094
  const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
2062
2095
  return policy.rolesContain(roles);
@@ -2072,10 +2105,10 @@ function sqlToPolicy(sql) {
2072
2105
  const right = parseOperand(rightStr.trim());
2073
2106
  if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
2074
2107
  }
2075
- return policy.raw(sql);
2108
+ return policy.raw(trimmed);
2076
2109
  }
2077
2110
  /**
2078
- * Literals from other BaaS platforms that people compare `auth.uid()` against
2111
+ * Literals from other BaaS platforms that people compare `rebase.uid()` against
2079
2112
  * out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
2080
2113
  * `pgRoles`, one surface over: the same muscle memory inside a `using:` string
2081
2114
  * is the more dangerous spelling, because it inverts a rule instead of
@@ -2086,18 +2119,25 @@ var FOREIGN_CONVENTION_UIDS = {
2086
2119
  authenticated: "Supabase",
2087
2120
  service_role: "Supabase"
2088
2121
  };
2089
- /** `auth.uid() IS NOT NULL` in raw SQL, the clause that is always true. */
2090
- var UID_NOT_NULL = /auth\.uid\(\)\s+IS\s+NOT\s+NULL/i;
2122
+ /**
2123
+ * `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
2124
+ *
2125
+ * Both schema spellings, because this runs over policy bodies read back from a
2126
+ * database, and one migrated by a pre-1.0 release still holds `auth.uid()`.
2127
+ * A security check that stops recognising a dangerous clause because the
2128
+ * framework renamed a function is a check that silently turns off.
2129
+ */
2130
+ var UID_NOT_NULL = /\b(?:rebase|auth)\.uid\(\)\s+IS\s+NOT\s+NULL/i;
2091
2131
  /**
2092
2132
  * Find clauses that read as "signed-in users only" but admit anonymous callers.
2093
2133
  *
2094
- * Both spellings come from the same place — Supabase, where `auth.uid()` really
2095
- * is NULL for an anonymous request. Rebase substitutes
2134
+ * Both spellings come from the same place — Supabase, where its own `auth.uid()`
2135
+ * really is NULL for an anonymous request. Rebase substitutes
2096
2136
  * {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
2097
2137
  * is how the trusted *server* context is recognised), so:
2098
2138
  *
2099
- * - `auth.uid() IS NOT NULL` is a tautology on the user path, and
2100
- * - `auth.uid() != 'anon'` excludes one spelling of anonymous and admits the
2139
+ * - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
2140
+ * - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
2101
2141
  * other. This one is not hypothetical and was not only a foreign habit:
2102
2142
  * rebase's own request path reported `'anon'` while everything that compiled
2103
2143
  * or checked a policy used `'anonymous'`, so whichever literal an author
@@ -2129,7 +2169,7 @@ function findAnonymousGrants(expr) {
2129
2169
  if (UID_NOT_NULL.test(e.sql)) found.push({
2130
2170
  pattern: "uid-not-null",
2131
2171
  detail: e.sql,
2132
- 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".`
2172
+ 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".`
2133
2173
  });
2134
2174
  return;
2135
2175
  case "compare": {
@@ -2151,7 +2191,7 @@ function findAnonymousGrants(expr) {
2151
2191
  return found;
2152
2192
  }
2153
2193
  function parseOperand(str) {
2154
- if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return policy.authUid();
2194
+ if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /rebase\.uid\(\)/i.test(str)) return policy.authUid();
2155
2195
  const stringMatch = str.match(/^'(.+)'$/);
2156
2196
  if (stringMatch) return policy.literal(stringMatch[1]);
2157
2197
  if (/^\w+$/.test(str)) return policy.field(str);
@@ -2233,12 +2273,12 @@ function compile(expr, scope) {
2233
2273
  const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
2234
2274
  return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
2235
2275
  }
2236
- case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
2237
- case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
2238
- case "authenticated": return `auth.uid() IS NOT NULL AND auth.uid() NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
2239
- case "serverContext": return "auth.uid() IS NULL";
2276
+ case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
2277
+ case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
2278
+ case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
2279
+ case "serverContext": return `${RLS_UID_SQL} IS NULL`;
2240
2280
  case "existsIn": return compileExistsIn(expr, scope);
2241
- case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
2281
+ case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
2242
2282
  }
2243
2283
  }
2244
2284
  /**
@@ -2275,8 +2315,8 @@ function operandToSql(operand, scope) {
2275
2315
  case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
2276
2316
  case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
2277
2317
  case "literal": return quoteLiteral(operand.value);
2278
- case "authUid": return "auth.uid()";
2279
- case "authRoles": return "string_to_array(auth.roles(), ',')";
2318
+ case "authUid": return RLS_UID_SQL;
2319
+ case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
2280
2320
  }
2281
2321
  }
2282
2322
  /**
@@ -2356,7 +2396,7 @@ var DEFAULT_GUARDED_OPS = [
2356
2396
  "delete"
2357
2397
  ];
2358
2398
  /** Whether a collection is flagged as an authentication collection. */
2359
- function isAuthCollection(collection) {
2399
+ function isAuthCollection$1(collection) {
2360
2400
  const auth = collection.auth;
2361
2401
  return auth === true || typeof auth === "object" && auth?.enabled === true;
2362
2402
  }
@@ -2389,7 +2429,7 @@ function getEffectiveSecurityRules(collection) {
2389
2429
  condition: SERVER_OR_ADMIN_EXPR$1,
2390
2430
  check: SERVER_OR_ADMIN_EXPR$1
2391
2431
  });
2392
- if (isAuthCollection(collection)) {
2432
+ if (isAuthCollection$1(collection)) {
2393
2433
  injected.push({
2394
2434
  name: `${tableName}_default_self_read`,
2395
2435
  operations: ["select"],
@@ -3356,6 +3396,46 @@ function resolveDataSource(collection, registry) {
3356
3396
  capabilities: getDataSourceCapabilities(engine)
3357
3397
  };
3358
3398
  }
3399
+ /**
3400
+ * Does a SQL toolchain own this collection's storage?
3401
+ *
3402
+ * "Owns the storage" means: something generates a table for it, pushes that
3403
+ * table to a database, plans its RLS policies, and reports it as drifted when
3404
+ * the two disagree. That is true of a Postgres collection and false of a
3405
+ * Firestore or MongoDB one, whose documents live in a store Rebase never
3406
+ * migrates — and the two were never told apart. Every stage of the SQL
3407
+ * toolchain took "the collections" to mean *all* of them, so a Firestore
3408
+ * collection declared next to the Postgres ones got a `pgTable` in the
3409
+ * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
3410
+ * `db push` include list — where its name shielding a same-named real table
3411
+ * from Atlas's exclude list is the one that can lose data.
3412
+ *
3413
+ * The answer is the resolved engine's {@link DataSourceCapabilities}, not a
3414
+ * name check: an engine registered through `registerDataSourceCapabilities`
3415
+ * gets the same treatment as the built-in ones.
3416
+ *
3417
+ * Deliberately answers **true** for an engine nobody has heard of. Build-time
3418
+ * tooling (the CLI, the schema generator) has no data-source registry to
3419
+ * resolve a `dataSource` key against, so an unknown key resolves to an unknown
3420
+ * engine — and the cost of the two mistakes is not symmetric. Wrongly
3421
+ * including a collection generates a table nothing writes to; wrongly excluding
3422
+ * one silently stops generating a table the app is serving from. Declare
3423
+ * `engine` on a collection that is not SQL-backed and this is exact.
3424
+ */
3425
+ function isRelationalCollection(collection, registry) {
3426
+ return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
3427
+ }
3428
+ /**
3429
+ * The subset of `collections` a SQL toolchain owns — see
3430
+ * {@link isRelationalCollection}.
3431
+ *
3432
+ * Every stage that generates SQL from collections starts by calling this, so
3433
+ * the rule lives in one place rather than being re-decided per generator. It
3434
+ * keeps the input order.
3435
+ */
3436
+ function relationalCollections(collections, registry) {
3437
+ return collections.filter((collection) => isRelationalCollection(collection, registry));
3438
+ }
3359
3439
  //#endregion
3360
3440
  //#region ../common/src/collections/CollectionRegistry.ts
3361
3441
  var CollectionRegistry = class {
@@ -3650,8 +3730,24 @@ var QueryBuilder = class {
3650
3730
  /**
3651
3731
  * Set a free-text search string if supported by the backend.
3652
3732
  */
3653
- search(searchString) {
3733
+ search(searchString, options) {
3654
3734
  this.params.searchString = searchString;
3735
+ if (options?.explain !== void 0) this.params.searchExplain = options.explain;
3736
+ return this;
3737
+ }
3738
+ /**
3739
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
3740
+ *
3741
+ * Postgres only, over a property declared as `type: "vector"`. Rows come
3742
+ * back with a `_distance`; `where` filters before the ordering.
3743
+ */
3744
+ vectorSearch(property, vector, options) {
3745
+ this.params.vectorSearch = {
3746
+ property,
3747
+ vector,
3748
+ ...options?.distance !== void 0 && { distance: options.distance },
3749
+ ...options?.threshold !== void 0 && { threshold: options.threshold }
3750
+ };
3655
3751
  return this;
3656
3752
  }
3657
3753
  /**
@@ -3981,10 +4077,12 @@ function createPrimaryKeyResolver(options) {
3981
4077
  * than postgres still serve rows with one, and this keeps them working.
3982
4078
  */
3983
4079
  function rowToEntity(row, slug, primaryKeys = []) {
4080
+ const { _matches, ...values } = row;
3984
4081
  return {
3985
4082
  id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
3986
4083
  path: slug,
3987
- values: row
4084
+ values,
4085
+ ..._matches ? { searchMatches: _matches } : {}
3988
4086
  };
3989
4087
  }
3990
4088
  /**
@@ -4106,6 +4204,21 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4106
4204
  values: {}
4107
4205
  } });
4108
4206
  },
4207
+ updateMany: driver.updateMany ? async (updates) => {
4208
+ return (await driver.updateMany({
4209
+ path: slug,
4210
+ updates: updates.map((u) => ({
4211
+ id: u.id,
4212
+ values: u.data
4213
+ }))
4214
+ })).map((row) => rowToEntity(row, slug, getPks()));
4215
+ } : void 0,
4216
+ deleteMany: driver.deleteMany ? async (ids) => {
4217
+ await driver.deleteMany({
4218
+ path: slug,
4219
+ ids
4220
+ });
4221
+ } : void 0,
4109
4222
  count: driver.count ? async (params) => {
4110
4223
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
4111
4224
  return driver.count({
@@ -4127,6 +4240,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4127
4240
  orderBy: params?.orderBy?.[0],
4128
4241
  order: params?.orderBy?.[1],
4129
4242
  searchString: params?.searchString,
4243
+ searchExplain: params?.searchExplain,
4130
4244
  onUpdate: (entities) => {
4131
4245
  onUpdate({
4132
4246
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
@@ -4164,8 +4278,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4164
4278
  offset(count) {
4165
4279
  return new QueryBuilder(accessor).offset(count);
4166
4280
  },
4167
- search(searchString) {
4168
- return new QueryBuilder(accessor).search(searchString);
4281
+ search(searchString, options) {
4282
+ return new QueryBuilder(accessor).search(searchString, options);
4283
+ },
4284
+ vectorSearch(property, vector, options) {
4285
+ return new QueryBuilder(accessor).vectorSearch(property, vector, options);
4169
4286
  },
4170
4287
  include(...relations) {
4171
4288
  return new QueryBuilder(accessor).include(...relations);
@@ -4253,8 +4370,18 @@ var SdkQueryBuilder = class {
4253
4370
  this.params.offset = count;
4254
4371
  return this;
4255
4372
  }
4256
- search(searchString) {
4373
+ search(searchString, options) {
4257
4374
  this.params.searchString = searchString;
4375
+ if (options?.explain !== void 0) this.params.searchExplain = options.explain;
4376
+ return this;
4377
+ }
4378
+ vectorSearch(property, vector, options) {
4379
+ this.params.vectorSearch = {
4380
+ property,
4381
+ vector,
4382
+ ...options?.distance !== void 0 && { distance: options.distance },
4383
+ ...options?.threshold !== void 0 && { threshold: options.threshold }
4384
+ };
4258
4385
  return this;
4259
4386
  }
4260
4387
  include(...relations) {
@@ -4308,9 +4435,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
4308
4435
  async update(id, data) {
4309
4436
  return entityToRow(await snap.update(id, data));
4310
4437
  },
4438
+ async updateMany(updates) {
4439
+ if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
4440
+ if (updates.length === 0) return [];
4441
+ if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
4442
+ return (await snap.updateMany(updates.map((u) => ({
4443
+ id: u.id,
4444
+ data: u.data
4445
+ })))).map(entityToRow);
4446
+ },
4311
4447
  delete(id) {
4312
4448
  return snap.delete(id);
4313
4449
  },
4450
+ async deleteMany(ids) {
4451
+ if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
4452
+ if (ids.length === 0) return;
4453
+ if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
4454
+ await snap.deleteMany(ids);
4455
+ },
4314
4456
  count: snap.count ? (params) => snap.count(params) : void 0,
4315
4457
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
4316
4458
  data: res.data.map(entityToRow),
@@ -4326,6 +4468,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
4326
4468
  limit: (count) => new SdkQueryBuilder(client).limit(count),
4327
4469
  offset: (count) => new SdkQueryBuilder(client).offset(count),
4328
4470
  search: (searchString) => new SdkQueryBuilder(client).search(searchString),
4471
+ vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
4329
4472
  include: (...relations) => new SdkQueryBuilder(client).include(...relations)
4330
4473
  };
4331
4474
  return client;
@@ -4371,6 +4514,460 @@ function buildSdkData(driver) {
4371
4514
  return wrapAsSdkData(buildRebaseData(driver));
4372
4515
  }
4373
4516
  //#endregion
4374
- export { camelCase as A, normalizeToEntityRelation as C, getPolicyNamesForRule as D, legacyForeignKeyName as E, hasForeignKeyOnTarget as F, isManyToMany as I, Vector as L, DEFAULT_ONE_OF_TYPE as M, DEFAULT_ONE_OF_VALUE as N, isPrototypePollutingKey as O, resolveClientListLimit 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, toSnakeCase as j, mergeDeep 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 };
4517
+ //#region src/schema/search-column.ts
4518
+ /**
4519
+ * The one place a collection's `search` block becomes SQL.
4520
+ *
4521
+ * Four things describe a Postgres table in this codebase — the DDL generator,
4522
+ * the Drizzle schema generator, the runtime table builder for BaaS mode, and
4523
+ * the boot-time schema ensure — and each of them has, at some point, described
4524
+ * a column differently from the others. The `varchar(255)` note in
4525
+ * `generate-postgres-ddl-logic` is one such scar: the same property produced a
4526
+ * capped column down one path and an uncapped one down the other, and nothing
4527
+ * failed until a user hit the cap.
4528
+ *
4529
+ * So the search column is not implemented four times. It is computed once,
4530
+ * here, and every generator renders the same {@link SearchColumnSpec}. There is
4531
+ * a test asserting exactly that (`search-column-contract.test.ts`); the point of
4532
+ * this module is that the test has something to assert *about*.
4533
+ *
4534
+ * ## Why the expressions look the way they do
4535
+ *
4536
+ * A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
4537
+ * Postgres is stricter here than intuition. Verified against PostgreSQL 18:
4538
+ *
4539
+ * | expression | immutable |
4540
+ * |-----------------------------------------|-----------|
4541
+ * | `to_tsvector('spanish', col)` | yes |
4542
+ * | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
4543
+ * | `array_to_string(col, ' ')` | **no** |
4544
+ * | `col::text` on `text[]` | **no** |
4545
+ * | `to_jsonb(col)` | **no** |
4546
+ * | `unaccent(col)` | **no** — dictionary lookup is STABLE |
4547
+ * | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
4548
+ * | `setweight(...) || setweight(...)` | yes |
4549
+ *
4550
+ * Three of the four things a real search column needs are therefore unavailable
4551
+ * directly, which is why {@link searchHelperFunctions} exists: each wraps a
4552
+ * stable built-in in an SQL function declared IMMUTABLE. That declaration is a
4553
+ * promise, and it is a true one for these three — array joining, JSON string
4554
+ * extraction and accent folding are all deterministic for a given input; the
4555
+ * built-ins are marked stable only because they must account for element types
4556
+ * and dictionaries in general.
4557
+ *
4558
+ * The alternative was to skip `unaccent` and text arrays entirely. That is not
4559
+ * a real option in an accented language: Postgres stems `auditoría` to
4560
+ * `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
4561
+ * typed without accents misses every row that carries them.
4562
+ */
4563
+ /** Schema-qualified so a collection outside `public` still resolves them. */
4564
+ var HELPER_SCHEMA = "public";
4565
+ /**
4566
+ * Names of the helper functions. Frozen: they are recorded in the stored
4567
+ * generation expression of every search column ever created, so renaming one
4568
+ * orphans every table that already has a search column.
4569
+ */
4570
+ var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
4571
+ var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
4572
+ /** Raised when a `search` block names something that cannot be searched. */
4573
+ var SearchConfigError = class extends Error {
4574
+ constructor(message) {
4575
+ super(message);
4576
+ this.name = "SearchConfigError";
4577
+ }
4578
+ };
4579
+ /** The `search` block of a collection, or undefined when it has none. */
4580
+ var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
4581
+ /**
4582
+ * Refuse a `search` block on a collection this engine does not store.
4583
+ *
4584
+ * The type only permits one on a `PostgresCollectionConfig`, so TypeScript
4585
+ * already stops the ordinary case. This catches the rest — a JS config, a cast,
4586
+ * a collection whose `engine` was changed after the block was written — because
4587
+ * the alternative is the exact failure the block exists to prevent: a developer
4588
+ * who declared what to index, saw no error, and got the substring fallback.
4589
+ *
4590
+ * Called with *every* collection, before the Postgres ones are filtered out.
4591
+ */
4592
+ var assertSearchIsPostgresOnly = (collections) => {
4593
+ for (const collection of collections) {
4594
+ if (isPostgresCollectionConfig(collection)) continue;
4595
+ if (!collection.search) continue;
4596
+ const engine = collection.engine ?? "non-postgres";
4597
+ 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.`);
4598
+ }
4599
+ };
4600
+ var columnNameOf = (propName, prop) => prop && "columnName" in prop && typeof prop.columnName === "string" ? prop.columnName : toSnakeCase(propName);
4601
+ /**
4602
+ * Classify a property for search purposes.
4603
+ *
4604
+ * Deliberately narrower than `getSqlColumnType`: search only cares whether a
4605
+ * value reaches text, and the mapping from property to *physical* type is
4606
+ * asserted against `getSqlColumnType` in the contract test rather than
4607
+ * duplicated here.
4608
+ *
4609
+ * Returns null for anything that is not text-bearing, which the caller turns
4610
+ * into a boot error naming the property.
4611
+ */
4612
+ var classify = (prop) => {
4613
+ switch (prop.type) {
4614
+ case "string": {
4615
+ const sp = prop;
4616
+ if (sp.enum) return {
4617
+ kind: "text",
4618
+ reason: "enum"
4619
+ };
4620
+ if (sp.isId === "uuid" || sp.columnType === "uuid") return {
4621
+ kind: "text",
4622
+ reason: "uuid"
4623
+ };
4624
+ return { kind: "text" };
4625
+ }
4626
+ case "map":
4627
+ if (prop.columnType === "json") return {
4628
+ kind: "jsonb",
4629
+ reason: "json"
4630
+ };
4631
+ return { kind: "jsonb" };
4632
+ case "array": {
4633
+ const ap = prop;
4634
+ let colType = ap.columnType;
4635
+ if (!colType && ap.of && !Array.isArray(ap.of)) {
4636
+ const of = ap.of;
4637
+ if (of.type === "string") colType = "text[]";
4638
+ else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
4639
+ else if (of.type === "boolean") colType = "boolean[]";
4640
+ }
4641
+ if (colType === "text[]") return { kind: "text_array" };
4642
+ if (colType === "json") return {
4643
+ kind: "jsonb",
4644
+ reason: "json"
4645
+ };
4646
+ if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
4647
+ kind: "text_array",
4648
+ reason: "non_text_array"
4649
+ };
4650
+ return { kind: "jsonb" };
4651
+ }
4652
+ default: return null;
4653
+ }
4654
+ };
4655
+ var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
4656
+ /** SQL reading one field as plain text, before normalization. */
4657
+ var rawTextSql = (field) => {
4658
+ const col = `"${field.column}"`;
4659
+ if (field.kind === "text") return `coalesce(${col}, '')`;
4660
+ if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
4661
+ 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))`;
4662
+ };
4663
+ var quote = (v) => `'${v.replace(/'/g, "''")}'`;
4664
+ /**
4665
+ * Resolve and validate one declared field path.
4666
+ *
4667
+ * A path that does not resolve throws. The whole point of an explicit block is
4668
+ * that the author knows what is indexed; a silently dropped field would make it
4669
+ * a guess again, and the failure — a search that returns nothing for content
4670
+ * that is plainly in the row — is invisible from the outside.
4671
+ */
4672
+ var resolveField = (entry, collection, cfg) => {
4673
+ const path = typeof entry === "string" ? entry : entry.path;
4674
+ const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? "B";
4675
+ const where = `${collection.slug}.search`;
4676
+ if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
4677
+ const [head, ...rest] = path.split(".");
4678
+ const prop = collection.properties?.[head];
4679
+ if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
4680
+ const classified = classify(prop);
4681
+ 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).`);
4682
+ 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.`);
4683
+ if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
4684
+ 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.`);
4685
+ if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
4686
+ 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.`);
4687
+ const column = columnNameOf(head, prop);
4688
+ const textSql = normalize(rawTextSql({
4689
+ column,
4690
+ jsonPath: rest,
4691
+ kind: classified.kind
4692
+ }), cfg.unaccent === true);
4693
+ const language = cfg.language ?? "simple";
4694
+ return {
4695
+ path,
4696
+ column,
4697
+ jsonPath: rest,
4698
+ kind: classified.kind,
4699
+ weight,
4700
+ sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
4701
+ textSql
4702
+ };
4703
+ };
4704
+ /**
4705
+ * Build the full spec for a collection, or undefined when it has not opted in.
4706
+ *
4707
+ * Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
4708
+ * at boot surface that as a startup failure — a search block that half-works is
4709
+ * worse than one that refuses.
4710
+ */
4711
+ var buildSearchColumnSpec = (collection) => {
4712
+ const cfg = getSearchConfig(collection);
4713
+ if (!cfg) return void 0;
4714
+ 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.`);
4715
+ const table = getTableName(collection);
4716
+ const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
4717
+ const column = cfg.column ?? "search_vector";
4718
+ 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.`);
4719
+ const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
4720
+ const seen = /* @__PURE__ */ new Set();
4721
+ for (const f of fields) {
4722
+ if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
4723
+ seen.add(f.path);
4724
+ }
4725
+ const extensions = [];
4726
+ if (cfg.unaccent) extensions.push("unaccent");
4727
+ if (cfg.fuzzy) extensions.push("pg_trgm");
4728
+ const spec = {
4729
+ schema,
4730
+ table,
4731
+ column,
4732
+ language: cfg.language ?? "simple",
4733
+ unaccent: cfg.unaccent === true,
4734
+ fields,
4735
+ expression: fields.map((f) => f.sql).join(" || "),
4736
+ indexName: toPostgresIdentifier(`${table}_${column}_gin`),
4737
+ extensions
4738
+ };
4739
+ if (cfg.fuzzy) {
4740
+ const fuzzyColumn = `${column}_text`;
4741
+ 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.`);
4742
+ spec.fuzzy = {
4743
+ column: fuzzyColumn,
4744
+ expression: fields.map((f) => f.textSql).join(" || ' ' || "),
4745
+ indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
4746
+ threshold: cfg.fuzzyThreshold ?? .3
4747
+ };
4748
+ }
4749
+ return spec;
4750
+ };
4751
+ /**
4752
+ * The IMMUTABLE wrappers the generated expressions call.
4753
+ *
4754
+ * `CREATE OR REPLACE` so a boot against an existing database is a no-op rather
4755
+ * than an error, and idempotent for the same reason every other boot-time DDL
4756
+ * statement here is.
4757
+ *
4758
+ * The bodies are stable built-ins wrapped in an immutable promise — see the
4759
+ * module comment for why that promise is sound. `STRICT` matters: it makes NULL
4760
+ * in mean NULL out without executing the body, which is what the `coalesce` at
4761
+ * each call site then absorbs.
4762
+ */
4763
+ var searchHelperFunctions = (spec) => {
4764
+ 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 $$;`];
4765
+ 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) $$;`);
4766
+ return statements;
4767
+ };
4768
+ /**
4769
+ * `CREATE EXTENSION` statements the spec's expressions depend on.
4770
+ *
4771
+ * `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified
4772
+ * `CREATE EXTENSION` installs into the first schema on `search_path`, which
4773
+ * defaults to `"$user", public` — and the scaffold's database role is named
4774
+ * `rebase`, the same as the schema the generator creates one statement earlier.
4775
+ * So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
4776
+ * dictionary in `rebase`, and every reference to `public.unaccent` below fails
4777
+ * with "text search dictionary does not exist". Observed, not theorised.
4778
+ */
4779
+ var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
4780
+ /**
4781
+ * Index statements for the spec.
4782
+ *
4783
+ * `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
4784
+ * SQL file replayed as one unit — a migration, or `search.sql` — where a
4785
+ * concurrent build is not allowed. The boot-time ensure path runs statement by
4786
+ * statement against tables that are live and populated, and uses the
4787
+ * concurrent form instead; see `ensureSearchColumns`.
4788
+ */
4789
+ var searchIndexStatements = (spec) => {
4790
+ const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
4791
+ 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);`);
4792
+ return statements;
4793
+ };
4794
+ /**
4795
+ * The generated column names a collection's search block adds, if any.
4796
+ *
4797
+ * These are physical columns on the table, so `SELECT *` returns them. They are
4798
+ * an index in column form — a list of lexeme positions, or a concatenation of
4799
+ * every searchable field on the row — and nothing outside the query planner has
4800
+ * any use for them. Left in, every list response carries a second, larger copy
4801
+ * of the row's text.
4802
+ */
4803
+ var searchColumnNames = (collection) => {
4804
+ let spec;
4805
+ try {
4806
+ spec = buildSearchColumnSpec(collection);
4807
+ } catch {
4808
+ return [];
4809
+ }
4810
+ if (!spec) return [];
4811
+ return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
4812
+ };
4813
+ /**
4814
+ * True for a column whose type only ever holds a search index.
4815
+ *
4816
+ * Independent of any collection config on purpose: an introspected database
4817
+ * (BaaS mode) can carry a `tsvector` column this framework never created —
4818
+ * Pagila's `film.fulltext` is the canonical one — and it should not be returned
4819
+ * to callers either. `isDerivedIndexColumn` already keeps such a column out of
4820
+ * the *properties*; this keeps it out of the *rows*.
4821
+ */
4822
+ var isSearchIndexColumn = (column) => {
4823
+ const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
4824
+ return sqlType === "tsvector" || sqlType === "tsquery";
4825
+ };
4826
+ /**
4827
+ * A drizzle select projection over `table` with the search columns dropped.
4828
+ *
4829
+ * Returns undefined when nothing needs dropping, so the common case keeps using
4830
+ * a plain `select()` and this stays invisible in the generated SQL.
4831
+ */
4832
+ var visibleColumnProjection = (tableColumns, collection) => {
4833
+ const excluded = excludedColumnNames(tableColumns, collection);
4834
+ if (!tableColumns || excluded.length === 0) return void 0;
4835
+ const projection = {};
4836
+ for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
4837
+ return projection;
4838
+ };
4839
+ /** The same exclusion as a drizzle `db.query` `columns` denylist. */
4840
+ var hiddenColumnsOption = (tableColumns, collection) => {
4841
+ const excluded = excludedColumnNames(tableColumns, collection);
4842
+ if (excluded.length === 0) return void 0;
4843
+ return Object.fromEntries(excluded.map((name) => [name, false]));
4844
+ };
4845
+ /**
4846
+ * The columns to keep out of a response, by name.
4847
+ *
4848
+ * `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
4849
+ * for anything that is not a real drizzle table — a stub in a test, a derived
4850
+ * or nested path with no table behind it. Nothing to exclude is the right
4851
+ * answer there, and it has to be an answer rather than a throw: this runs on
4852
+ * the read path of every collection, opted in or not.
4853
+ */
4854
+ var excludedColumnNames = (tableColumns, collection) => {
4855
+ if (!tableColumns || typeof tableColumns !== "object") return [];
4856
+ const byName = new Set(collection ? searchColumnNames(collection) : []);
4857
+ return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
4858
+ };
4859
+ //#endregion
4860
+ //#region src/schema/auth-users-columns.ts
4861
+ /**
4862
+ * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
4863
+ * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
4864
+ * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
4865
+ * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
4866
+ * table that already holds a longer row.
4867
+ */
4868
+ var AUTH_USERS_COLUMNS = [
4869
+ {
4870
+ column: "email",
4871
+ type: "TEXT",
4872
+ notNull: true
4873
+ },
4874
+ {
4875
+ column: "display_name",
4876
+ type: "TEXT"
4877
+ },
4878
+ {
4879
+ column: "photo_url",
4880
+ type: "TEXT"
4881
+ },
4882
+ {
4883
+ column: "roles",
4884
+ type: "TEXT[]",
4885
+ default: "'{}'",
4886
+ notNull: true
4887
+ },
4888
+ {
4889
+ column: "password_hash",
4890
+ type: "TEXT"
4891
+ },
4892
+ {
4893
+ column: "email_verified",
4894
+ type: "BOOLEAN",
4895
+ default: "FALSE",
4896
+ notNull: true
4897
+ },
4898
+ {
4899
+ column: "email_verification_token",
4900
+ type: "TEXT"
4901
+ },
4902
+ {
4903
+ column: "email_verification_sent_at",
4904
+ type: "TIMESTAMP WITH TIME ZONE"
4905
+ },
4906
+ {
4907
+ column: "is_anonymous",
4908
+ type: "BOOLEAN",
4909
+ default: "FALSE",
4910
+ notNull: true
4911
+ },
4912
+ {
4913
+ column: "metadata",
4914
+ type: "JSONB",
4915
+ default: "'{}'",
4916
+ notNull: true
4917
+ },
4918
+ {
4919
+ column: "tokens_valid_after",
4920
+ type: "TIMESTAMP WITH TIME ZONE"
4921
+ },
4922
+ {
4923
+ column: "created_at",
4924
+ type: "TIMESTAMP WITH TIME ZONE",
4925
+ default: "NOW()",
4926
+ notNull: true
4927
+ },
4928
+ {
4929
+ column: "updated_at",
4930
+ type: "TIMESTAMP WITH TIME ZONE",
4931
+ default: "NOW()",
4932
+ notNull: true
4933
+ }
4934
+ ];
4935
+ var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
4936
+ /** Type + inline constraints, as they appear after the column name. */
4937
+ function authUsersColumnSql(spec) {
4938
+ return [
4939
+ spec.type,
4940
+ spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
4941
+ spec.notNull ? "NOT NULL" : ""
4942
+ ].filter(Boolean).join(" ");
4943
+ }
4944
+ /**
4945
+ * The auth-owned definition for a physical column name, or `undefined` when
4946
+ * auth does not own it.
4947
+ *
4948
+ * Callers pass the RESOLVED column name (after `columnName` mapping), because
4949
+ * that is the only name the three creators agree on: the scaffold's users
4950
+ * collection spells the property `displayName` and the column `display_name`.
4951
+ */
4952
+ function authUsersColumnDefinition(column) {
4953
+ const spec = BY_COLUMN.get(column);
4954
+ return spec ? authUsersColumnSql(spec) : void 0;
4955
+ }
4956
+ /**
4957
+ * Whether a collection is an auth collection, i.e. whether the definitions in
4958
+ * this module apply to its table at all.
4959
+ *
4960
+ * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
4961
+ * that one takes a `CollectionConfig`, this one is called from DDL code paths
4962
+ * that hold looser objects, and both spellings must accept `auth: true` as well
4963
+ * as `auth: { enabled: true }`.
4964
+ */
4965
+ function isAuthCollection(collection) {
4966
+ const auth = collection?.auth;
4967
+ if (auth === true) return true;
4968
+ return typeof auth === "object" && auth !== null && auth.enabled === true;
4969
+ }
4970
+ //#endregion
4971
+ export { resolveCollectionRelations as A, legacyForeignKeyName as B, securityRuleToConditions as C, getEnumVarName as D, getColumnName as E, createRelationRef as F, camelCase as G, getPolicyNamesForRule as H, createRelationRefWithData as I, DEFAULT_ONE_OF_VALUE as J, toSnakeCase as K, normalizeToEntityRelation as L, getDeclaredPrimaryKeys as M, isAddressableId as N, getTableName as O, parseIdValues as P, Vector as Q, updateDateAutoValues as R, policyToPostgres as S, findRelation as T, isPrototypePollutingKey as U, toPostgresIdentifier as V, mergeDeep as W, hasForeignKeyOnTarget as X, resolveClientListLimit as Y, isManyToMany as Z, resolveStringColumnLength as _, SEARCH_TEXT_FN as a, resolveJunctionSpecs as b, buildSearchColumnSpec as c, searchHelperFunctions as d, searchIndexStatements as f, relationalCollections as g, CollectionRegistry as h, isAuthCollection as i, buildCompositeId as j, getTableVarName as k, hiddenColumnsOption as l, buildSdkData as m, authUsersColumnDefinition as n, SEARCH_UNACCENT_FN as o, visibleColumnProjection as p, DEFAULT_ONE_OF_TYPE as q, authUsersColumnSql as r, assertSearchIsPostgresOnly as s, AUTH_USERS_COLUMNS as t, searchExtensionStatements as u, getJunctionCollectionConfig as v, findAnonymousGrants as w, getEffectiveSecurityRules as x, getJunctionSecurityRules as y, generateForeignKeyName as z };
4375
4972
 
4376
- //# sourceMappingURL=src-Bs2ZzSi4.js.map
4973
+ //# sourceMappingURL=auth-users-columns-Dt9g712t.js.map