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

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 (63) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/{src-DlPBctw_.js → auth-users-columns-Do9mw5Y5.js} +318 -42
  3. package/dist/auth-users-columns-Do9mw5Y5.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 +1 -1
  7. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-DMUdRQdM.js} +2 -2
  8. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-DMUdRQdM.js.map} +1 -1
  9. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DSIxvLLD.js} +59 -15
  10. package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
  11. package/dist/index.es.js +534 -200
  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 +1 -1
  19. package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
  20. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  21. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  22. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  23. package/dist/security/rls-enforcement.d.ts +53 -2
  24. package/dist/services/FetchService.d.ts +10 -7
  25. package/dist/services/dataService.d.ts +2 -0
  26. package/dist/services/realtimeService.d.ts +25 -21
  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/{websocket-B2LsrINK.js → websocket-D0TBU3ia.js} +3 -3
  30. package/dist/{websocket-B2LsrINK.js.map → websocket-D0TBU3ia.js.map} +1 -1
  31. package/package.json +9 -8
  32. package/src/PostgresBackendDriver.ts +165 -3
  33. package/src/PostgresBootstrapper.ts +41 -2
  34. package/src/auth/ensure-tables.ts +185 -86
  35. package/src/cli-helpers.ts +22 -9
  36. package/src/cli.ts +175 -30
  37. package/src/collections/validate-relations.ts +124 -17
  38. package/src/data-transformer.ts +13 -3
  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 +88 -19
  43. package/src/schema/generate-drizzle-schema-logic.ts +8 -2
  44. package/src/schema/generate-postgres-ddl-logic.ts +97 -13
  45. package/src/schema/generated-schema-staleness.ts +169 -0
  46. package/src/schema/introspect-db-logic.ts +1 -1
  47. package/src/schema/non-sql-collections.test.ts +131 -0
  48. package/src/schema/rls-bootstrap-sql.ts +288 -0
  49. package/src/security/anonymous-grants.test.ts +4 -2
  50. package/src/security/rls-enforcement.ts +141 -3
  51. package/src/services/BranchService.ts +5 -0
  52. package/src/services/FetchService.ts +10 -93
  53. package/src/services/PersistService.ts +14 -1
  54. package/src/services/channel-history.ts +8 -0
  55. package/src/services/channel-presence.ts +6 -0
  56. package/src/services/dataService.ts +2 -0
  57. package/src/services/realtimeService.ts +36 -33
  58. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  59. package/dist/policy-CeA1JcxP.js.map +0 -1
  60. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  61. package/dist/src-DlPBctw_.js.map +0 -1
  62. package/dist/src-DoU9yPqq.js.map +0 -1
  63. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -3,7 +3,7 @@ import { BranchService } from "./services/BranchService";
3
3
  import { RealtimeService } from "./services/realtimeService";
4
4
  import { DatabasePoolManager } from "./databasePoolManager";
5
5
  import { DrizzleClient } from "./interfaces";
6
- import { DatabaseAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseClient, RebaseSdkData, RestFetchService, SaveManyProps, SaveProps, TableMetadata, User } from "@rebasepro/types";
6
+ import { DatabaseAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseClient, RebaseSdkData, RestFetchService, SaveManyProps, SaveProps, UpdateManyProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
7
7
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
8
8
  import { HistoryService } from "./history/HistoryService";
9
9
  export declare class PostgresBackendDriver implements DataDriver {
@@ -62,6 +62,25 @@ export declare class PostgresBackendDriver implements DataDriver {
62
62
  * REST/SDK read leaks unmasked data (see {@link applyAfterReadForRest}).
63
63
  */
64
64
  get restFetchService(): RestFetchService;
65
+ /**
66
+ * Build the context handed to every collection callback.
67
+ *
68
+ * Note `data: this.data` — `this` is whichever driver is running the
69
+ * operation, so the callback's data plane inherits that driver's privilege.
70
+ * On a user request `AuthenticatedPostgresBackendDriver.withTransaction`
71
+ * constructs a fresh base driver bound to the RLS-scoped transaction and
72
+ * runs the operation on it, so `this.data` speaks through that connection
73
+ * and policies apply. On server-context work `this` is the base driver on
74
+ * the owner connection, and they do not. Pinned by the
75
+ * `"scopes context.data to the caller"` case in the `rls-enforcement` e2e
76
+ * suite, because it is the kind of property that is easy to break from a
77
+ * distance and impossible to notice.
78
+ *
79
+ * Previously returned through `as unknown as RebaseCallContext`, which
80
+ * disabled checking for the whole object and let `driver` — documented in
81
+ * the callbacks guide — sit on the runtime context while absent from the
82
+ * contract. Both are declared now, so this is a plain typed return.
83
+ */
65
84
  private buildCallContext;
66
85
  private resolveCollectionCallbacks;
67
86
  /**
@@ -102,6 +121,34 @@ export declare class PostgresBackendDriver implements DataDriver {
102
121
  * with the last write winning, exactly as separate calls would.
103
122
  */
104
123
  saveMany<M extends Record<string, unknown>>({ path, rows, collection, upsert }: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
124
+ /**
125
+ * Update many rows through the same pipeline as {@link save}, in one
126
+ * transaction.
127
+ *
128
+ * Structurally the mirror of {@link saveMany} — same tx-bound sub-driver,
129
+ * same deferred notifications, same per-row error labelling — but it calls
130
+ * `save` with an explicit `id` and `status: "existing"`, which is precisely
131
+ * what `saveMany` cannot do: that one passes `status: "new"` and keeps the
132
+ * key inside `values`, so it inserts or upserts and can never target a
133
+ * particular row.
134
+ *
135
+ * All-or-nothing, so an id matching no row aborts the batch. A partial
136
+ * update is the outcome with no good recovery: the caller cannot tell which
137
+ * half landed without re-reading everything.
138
+ */
139
+ updateMany<M extends Record<string, unknown>>({ path, updates, collection }: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
140
+ /**
141
+ * Delete many rows in one transaction, running the full delete pipeline —
142
+ * `beforeDelete`, the delete, `afterDelete` — for each.
143
+ *
144
+ * Looping the single-row {@link delete} rather than emitting one
145
+ * `DELETE ... WHERE id = ANY($1)` is the deliberate choice: a single
146
+ * statement would be faster and would skip every callback, so a collection
147
+ * relying on `beforeDelete` to veto or on `afterDelete` to clean up
148
+ * dependents would behave differently depending on how many rows the caller
149
+ * happened to delete at once. Same pipeline, one transaction.
150
+ */
151
+ deleteMany<M extends Record<string, unknown>>({ path, ids, collection }: DeleteManyProps<M>): Promise<void>;
105
152
  delete<M extends Record<string, unknown>>({ row, collection }: DeleteProps<M>): Promise<void>;
106
153
  deleteAll(path: string): Promise<void>;
107
154
  checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
@@ -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
@@ -55,6 +55,25 @@ function hasForeignKeyOnTarget(relation) {
55
55
  function isManyToMany(relation) {
56
56
  return relation.kind === "manyToMany";
57
57
  }
58
+ /**
59
+ * Resolve a client-supplied list `limit` into a safe, always-defined value.
60
+ *
61
+ * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
62
+ * so `0`, negatives, and absurd values can never bypass the cap.
63
+ * - An absent / blank / non-numeric limit falls back to the mode default:
64
+ * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
65
+ *
66
+ * The return is never `undefined` — no ingress that routes its client limit
67
+ * through this can produce an unbounded read.
68
+ */
69
+ function resolveClientListLimit(rawLimit, opts = {}) {
70
+ const maxLimit = opts.maxLimit ?? 1e3;
71
+ if (rawLimit != null && String(rawLimit).trim() !== "") {
72
+ const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
73
+ if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
74
+ }
75
+ return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
76
+ }
58
77
  //#endregion
59
78
  //#region ../common/src/util/common.ts
60
79
  var DEFAULT_ONE_OF_TYPE = "type";
@@ -1007,6 +1026,35 @@ function camelCase(str) {
1007
1026
  });
1008
1027
  })))();
1009
1028
  /**
1029
+ * Segments that reach the prototype chain rather than a property of the object.
1030
+ *
1031
+ * The twin of this function in `@rebasepro/forms` could be made to write onto
1032
+ * `Object.prototype` through a path of `__proto__.x`. This copy survives the
1033
+ * write by accident — its `clone` always spreads into a fresh object, while the
1034
+ * form engine's has a "preserve class instances" branch that hands back
1035
+ * `Object.prototype` itself — but `getIn` still *reads* through the chain, and
1036
+ * handing back `Object.prototype` is how a polluted value is read out again.
1037
+ *
1038
+ * Closed on both sides here, so the two implementations agree.
1039
+ */
1040
+ var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
1041
+ "__proto__",
1042
+ "constructor",
1043
+ "prototype"
1044
+ ]);
1045
+ /**
1046
+ * Whether writing this single key with `obj[key] = …` would reach the prototype
1047
+ * chain instead of creating a property.
1048
+ *
1049
+ * The single-key counterpart of {@link pathTraversesPrototype}, for the many
1050
+ * places that copy an object one key at a time. `JSON.parse` creates
1051
+ * `__proto__` as an *own* property, so it survives `hasOwnProperty` — and then
1052
+ * `target[key] = value` invokes the setter and replaces the target's prototype.
1053
+ */
1054
+ function isPrototypePollutingKey(key) {
1055
+ return UNSAFE_PATH_SEGMENTS.has(key);
1056
+ }
1057
+ /**
1010
1058
  * Deep clone a value, preserving function references and class instances.
1011
1059
  * Unlike structuredClone, this handles objects that contain functions
1012
1060
  * (e.g. CollectionConfig with target(), childCollections(), callbacks).
@@ -1383,10 +1431,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
1383
1431
  * have `id` and `path` fields — these are relation-shaped objects from
1384
1432
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
1385
1433
  *
1434
+ * When `targetPath` is given, also accepts a bare id. A relation column is a
1435
+ * foreign key, and the REST layer returns it as the scalar it is; only some
1436
+ * fetch paths hydrate it into an object. Which form a caller sees therefore
1437
+ * depends on how the row was loaded, and a caller that only accepted objects
1438
+ * reported half of its own data as a type error. The declared target is the
1439
+ * missing half: with it, an id is a relation that has not been fetched yet.
1440
+ *
1386
1441
  * Returns null if the value cannot be coerced.
1387
1442
  */
1388
- function normalizeToEntityRelation(value, propertyType) {
1443
+ function normalizeToEntityRelation(value, propertyType, targetPath) {
1389
1444
  if (value instanceof EntityRelation) return value;
1445
+ if (targetPath && (typeof value === "string" || typeof value === "number")) {
1446
+ if (value === "") return null;
1447
+ return new EntityRelation(value, targetPath);
1448
+ }
1390
1449
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1391
1450
  const obj = value;
1392
1451
  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;
@@ -1924,9 +1983,9 @@ function isKeywordAt(upper, i, keyword) {
1924
1983
  *
1925
1984
  * This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
1926
1985
  * `AND` inside
1927
- * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = auth.uid())`
1986
+ * `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
1928
1987
  * split the expression, and re-emitting the halves produced
1929
- * `(EXISTS (...) AND m.user_id = auth.uid())`
1988
+ * `(EXISTS (...) AND m.user_id = rebase.uid())`
1930
1989
  * where `m` is no longer in scope — SQL that Postgres rejects outright with
1931
1990
  * "missing FROM-clause entry for table". Returning null instead keeps such a
1932
1991
  * clause as a `raw` expression, which round-trips verbatim.
@@ -2000,15 +2059,15 @@ function stripOuterParens(sql) {
2000
2059
  }
2001
2060
  }
2002
2061
  function sqlToPolicy(sql) {
2003
- const trimmed = stripOuterParens(sql.trim());
2062
+ const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
2004
2063
  if (trimmed.toLowerCase() === "true") return policy.true();
2005
2064
  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);
2065
+ const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
2007
2066
  if (overlapMatch) {
2008
2067
  const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
2009
2068
  return policy.rolesOverlap(roles);
2010
2069
  }
2011
- const containMatch = trimmed.match(/^string_to_array\s*\(\s*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
2070
+ const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
2012
2071
  if (containMatch) {
2013
2072
  const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
2014
2073
  return policy.rolesContain(roles);
@@ -2024,10 +2083,10 @@ function sqlToPolicy(sql) {
2024
2083
  const right = parseOperand(rightStr.trim());
2025
2084
  if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
2026
2085
  }
2027
- return policy.raw(sql);
2086
+ return policy.raw(trimmed);
2028
2087
  }
2029
2088
  /**
2030
- * Literals from other BaaS platforms that people compare `auth.uid()` against
2089
+ * Literals from other BaaS platforms that people compare `rebase.uid()` against
2031
2090
  * out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
2032
2091
  * `pgRoles`, one surface over: the same muscle memory inside a `using:` string
2033
2092
  * is the more dangerous spelling, because it inverts a rule instead of
@@ -2038,18 +2097,25 @@ var FOREIGN_CONVENTION_UIDS = {
2038
2097
  authenticated: "Supabase",
2039
2098
  service_role: "Supabase"
2040
2099
  };
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;
2100
+ /**
2101
+ * `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
2102
+ *
2103
+ * Both schema spellings, because this runs over policy bodies read back from a
2104
+ * database, and one migrated by a pre-1.0 release still holds `auth.uid()`.
2105
+ * A security check that stops recognising a dangerous clause because the
2106
+ * framework renamed a function is a check that silently turns off.
2107
+ */
2108
+ var UID_NOT_NULL = /\b(?:rebase|auth)\.uid\(\)\s+IS\s+NOT\s+NULL/i;
2043
2109
  /**
2044
2110
  * Find clauses that read as "signed-in users only" but admit anonymous callers.
2045
2111
  *
2046
- * Both spellings come from the same place — Supabase, where `auth.uid()` really
2047
- * is NULL for an anonymous request. Rebase substitutes
2112
+ * Both spellings come from the same place — Supabase, where its own `auth.uid()`
2113
+ * really is NULL for an anonymous request. Rebase substitutes
2048
2114
  * {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
2049
2115
  * is how the trusted *server* context is recognised), so:
2050
2116
  *
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
2117
+ * - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
2118
+ * - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
2053
2119
  * other. This one is not hypothetical and was not only a foreign habit:
2054
2120
  * rebase's own request path reported `'anon'` while everything that compiled
2055
2121
  * or checked a policy used `'anonymous'`, so whichever literal an author
@@ -2081,7 +2147,7 @@ function findAnonymousGrants(expr) {
2081
2147
  if (UID_NOT_NULL.test(e.sql)) found.push({
2082
2148
  pattern: "uid-not-null",
2083
2149
  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".`
2150
+ 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
2151
  });
2086
2152
  return;
2087
2153
  case "compare": {
@@ -2103,7 +2169,7 @@ function findAnonymousGrants(expr) {
2103
2169
  return found;
2104
2170
  }
2105
2171
  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();
2172
+ if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /rebase\.uid\(\)/i.test(str)) return policy.authUid();
2107
2173
  const stringMatch = str.match(/^'(.+)'$/);
2108
2174
  if (stringMatch) return policy.literal(stringMatch[1]);
2109
2175
  if (/^\w+$/.test(str)) return policy.field(str);
@@ -2185,12 +2251,12 @@ function compile(expr, scope) {
2185
2251
  const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
2186
2252
  return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
2187
2253
  }
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";
2254
+ case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
2255
+ case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
2256
+ case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
2257
+ case "serverContext": return `${RLS_UID_SQL} IS NULL`;
2192
2258
  case "existsIn": return compileExistsIn(expr, scope);
2193
- case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
2259
+ case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
2194
2260
  }
2195
2261
  }
2196
2262
  /**
@@ -2227,8 +2293,8 @@ function operandToSql(operand, scope) {
2227
2293
  case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
2228
2294
  case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
2229
2295
  case "literal": return quoteLiteral(operand.value);
2230
- case "authUid": return "auth.uid()";
2231
- case "authRoles": return "string_to_array(auth.roles(), ',')";
2296
+ case "authUid": return RLS_UID_SQL;
2297
+ case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
2232
2298
  }
2233
2299
  }
2234
2300
  /**
@@ -2308,7 +2374,7 @@ var DEFAULT_GUARDED_OPS = [
2308
2374
  "delete"
2309
2375
  ];
2310
2376
  /** Whether a collection is flagged as an authentication collection. */
2311
- function isAuthCollection(collection) {
2377
+ function isAuthCollection$1(collection) {
2312
2378
  const auth = collection.auth;
2313
2379
  return auth === true || typeof auth === "object" && auth?.enabled === true;
2314
2380
  }
@@ -2341,7 +2407,7 @@ function getEffectiveSecurityRules(collection) {
2341
2407
  condition: SERVER_OR_ADMIN_EXPR$1,
2342
2408
  check: SERVER_OR_ADMIN_EXPR$1
2343
2409
  });
2344
- if (isAuthCollection(collection)) {
2410
+ if (isAuthCollection$1(collection)) {
2345
2411
  injected.push({
2346
2412
  name: `${tableName}_default_self_read`,
2347
2413
  operations: ["select"],
@@ -3308,6 +3374,46 @@ function resolveDataSource(collection, registry) {
3308
3374
  capabilities: getDataSourceCapabilities(engine)
3309
3375
  };
3310
3376
  }
3377
+ /**
3378
+ * Does a SQL toolchain own this collection's storage?
3379
+ *
3380
+ * "Owns the storage" means: something generates a table for it, pushes that
3381
+ * table to a database, plans its RLS policies, and reports it as drifted when
3382
+ * the two disagree. That is true of a Postgres collection and false of a
3383
+ * Firestore or MongoDB one, whose documents live in a store Rebase never
3384
+ * migrates — and the two were never told apart. Every stage of the SQL
3385
+ * toolchain took "the collections" to mean *all* of them, so a Firestore
3386
+ * collection declared next to the Postgres ones got a `pgTable` in the
3387
+ * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
3388
+ * `db push` include list — where its name shielding a same-named real table
3389
+ * from Atlas's exclude list is the one that can lose data.
3390
+ *
3391
+ * The answer is the resolved engine's {@link DataSourceCapabilities}, not a
3392
+ * name check: an engine registered through `registerDataSourceCapabilities`
3393
+ * gets the same treatment as the built-in ones.
3394
+ *
3395
+ * Deliberately answers **true** for an engine nobody has heard of. Build-time
3396
+ * tooling (the CLI, the schema generator) has no data-source registry to
3397
+ * resolve a `dataSource` key against, so an unknown key resolves to an unknown
3398
+ * engine — and the cost of the two mistakes is not symmetric. Wrongly
3399
+ * including a collection generates a table nothing writes to; wrongly excluding
3400
+ * one silently stops generating a table the app is serving from. Declare
3401
+ * `engine` on a collection that is not SQL-backed and this is exact.
3402
+ */
3403
+ function isRelationalCollection(collection, registry) {
3404
+ return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
3405
+ }
3406
+ /**
3407
+ * The subset of `collections` a SQL toolchain owns — see
3408
+ * {@link isRelationalCollection}.
3409
+ *
3410
+ * Every stage that generates SQL from collections starts by calling this, so
3411
+ * the rule lives in one place rather than being re-decided per generator. It
3412
+ * keeps the input order.
3413
+ */
3414
+ function relationalCollections(collections, registry) {
3415
+ return collections.filter((collection) => isRelationalCollection(collection, registry));
3416
+ }
3311
3417
  //#endregion
3312
3418
  //#region ../common/src/collections/CollectionRegistry.ts
3313
3419
  var CollectionRegistry = class {
@@ -3660,6 +3766,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
3660
3766
  Object.setPrototypeOf(this, RebasePaginationError.prototype);
3661
3767
  }
3662
3768
  };
3769
+ /**
3770
+ * Resolve `limit`/`offset`/`page` into the window a read will actually use.
3771
+ *
3772
+ * Lives here, next to the walk, for the reason at the top of this file: every
3773
+ * transport has to mean the same thing by "page two". Four of them did not —
3774
+ * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
3775
+ * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
3776
+ * the published type documented a fourth number. Pages that overlap or skip
3777
+ * rows are the mildest of those outcomes.
3778
+ *
3779
+ * `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
3780
+ * is the value to hand a driver: it stays `undefined` when the caller named no
3781
+ * offset, because keyset pagination seeks with a `where` clause and must not
3782
+ * look like it is paging by offset.
3783
+ */
3784
+ function resolveFindWindow(params) {
3785
+ const limit = params?.limit ?? 50;
3786
+ const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
3787
+ return {
3788
+ limit,
3789
+ offset,
3790
+ driverOffset: params?.page != null ? offset : params?.offset
3791
+ };
3792
+ }
3663
3793
  function normalizePageSize(raw) {
3664
3794
  if (raw === void 0 || !Number.isFinite(raw)) return 200;
3665
3795
  return Math.max(1, Math.floor(raw));
@@ -3955,21 +4085,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3955
4085
  const accessor = {
3956
4086
  async find(params) {
3957
4087
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3958
- const limit = params?.limit ?? 20;
3959
- const offset = params?.offset ?? 0;
4088
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3960
4089
  const fetchService = driver.restFetchService;
3961
4090
  const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
3962
4091
  filter,
3963
- limit: params?.limit,
3964
- offset: params?.offset,
4092
+ logical: params?.logical,
4093
+ limit,
4094
+ offset: driverOffset,
3965
4095
  orderBy: params?.orderBy?.[0],
3966
4096
  order: params?.orderBy?.[1],
3967
4097
  searchString: params?.searchString
3968
4098
  }, params?.include) : await driver.fetchCollection({
3969
4099
  path: slug,
3970
- limit: params?.limit,
3971
- offset: params?.offset,
4100
+ limit,
4101
+ offset: driverOffset,
3972
4102
  filter,
4103
+ logical: params?.logical,
3973
4104
  orderBy: params?.orderBy?.[0],
3974
4105
  order: params?.orderBy?.[1],
3975
4106
  searchString: params?.searchString
@@ -3979,7 +4110,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3979
4110
  if (driver.count) {
3980
4111
  total = await driver.count({
3981
4112
  path: slug,
3982
- filter
4113
+ filter,
4114
+ logical: params?.logical,
4115
+ searchString: params?.searchString
3983
4116
  });
3984
4117
  hasMore = offset + rows.length < total;
3985
4118
  }
@@ -4031,22 +4164,39 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4031
4164
  values: {}
4032
4165
  } });
4033
4166
  },
4167
+ updateMany: driver.updateMany ? async (updates) => {
4168
+ return (await driver.updateMany({
4169
+ path: slug,
4170
+ updates: updates.map((u) => ({
4171
+ id: u.id,
4172
+ values: u.data
4173
+ }))
4174
+ })).map((row) => rowToEntity(row, slug, getPks()));
4175
+ } : void 0,
4176
+ deleteMany: driver.deleteMany ? async (ids) => {
4177
+ await driver.deleteMany({
4178
+ path: slug,
4179
+ ids
4180
+ });
4181
+ } : void 0,
4034
4182
  count: driver.count ? async (params) => {
4035
4183
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
4036
4184
  return driver.count({
4037
4185
  path: slug,
4038
- filter
4186
+ filter,
4187
+ logical: params?.logical,
4188
+ searchString: params?.searchString
4039
4189
  });
4040
4190
  } : void 0,
4041
4191
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
4042
- const limit = params?.limit ?? 20;
4043
- const offset = params?.offset ?? 0;
4192
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
4044
4193
  const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
4045
4194
  return driver.listenCollection({
4046
4195
  path: slug,
4047
- limit: params?.limit,
4048
- offset: params?.offset,
4196
+ limit,
4197
+ offset: driverOffset,
4049
4198
  filter: params?.where,
4199
+ logical: params?.logical,
4050
4200
  orderBy: params?.orderBy?.[0],
4051
4201
  order: params?.orderBy?.[1],
4052
4202
  searchString: params?.searchString,
@@ -4054,7 +4204,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4054
4204
  onUpdate({
4055
4205
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
4056
4206
  meta: {
4057
- total: entities.length,
4207
+ total: offset + entities.length,
4058
4208
  limit,
4059
4209
  offset,
4060
4210
  hasMore: entities.length >= limit
@@ -4231,9 +4381,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
4231
4381
  async update(id, data) {
4232
4382
  return entityToRow(await snap.update(id, data));
4233
4383
  },
4384
+ async updateMany(updates) {
4385
+ if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
4386
+ if (updates.length === 0) return [];
4387
+ if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
4388
+ return (await snap.updateMany(updates.map((u) => ({
4389
+ id: u.id,
4390
+ data: u.data
4391
+ })))).map(entityToRow);
4392
+ },
4234
4393
  delete(id) {
4235
4394
  return snap.delete(id);
4236
4395
  },
4396
+ async deleteMany(ids) {
4397
+ if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
4398
+ if (ids.length === 0) return;
4399
+ if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
4400
+ await snap.deleteMany(ids);
4401
+ },
4237
4402
  count: snap.count ? (params) => snap.count(params) : void 0,
4238
4403
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
4239
4404
  data: res.data.map(entityToRow),
@@ -4294,6 +4459,117 @@ function buildSdkData(driver) {
4294
4459
  return wrapAsSdkData(buildRebaseData(driver));
4295
4460
  }
4296
4461
  //#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 };
4462
+ //#region src/schema/auth-users-columns.ts
4463
+ /**
4464
+ * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
4465
+ * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
4466
+ * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
4467
+ * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
4468
+ * table that already holds a longer row.
4469
+ */
4470
+ var AUTH_USERS_COLUMNS = [
4471
+ {
4472
+ column: "email",
4473
+ type: "TEXT",
4474
+ notNull: true
4475
+ },
4476
+ {
4477
+ column: "display_name",
4478
+ type: "TEXT"
4479
+ },
4480
+ {
4481
+ column: "photo_url",
4482
+ type: "TEXT"
4483
+ },
4484
+ {
4485
+ column: "roles",
4486
+ type: "TEXT[]",
4487
+ default: "'{}'",
4488
+ notNull: true
4489
+ },
4490
+ {
4491
+ column: "password_hash",
4492
+ type: "TEXT"
4493
+ },
4494
+ {
4495
+ column: "email_verified",
4496
+ type: "BOOLEAN",
4497
+ default: "FALSE",
4498
+ notNull: true
4499
+ },
4500
+ {
4501
+ column: "email_verification_token",
4502
+ type: "TEXT"
4503
+ },
4504
+ {
4505
+ column: "email_verification_sent_at",
4506
+ type: "TIMESTAMP WITH TIME ZONE"
4507
+ },
4508
+ {
4509
+ column: "is_anonymous",
4510
+ type: "BOOLEAN",
4511
+ default: "FALSE",
4512
+ notNull: true
4513
+ },
4514
+ {
4515
+ column: "metadata",
4516
+ type: "JSONB",
4517
+ default: "'{}'",
4518
+ notNull: true
4519
+ },
4520
+ {
4521
+ column: "tokens_valid_after",
4522
+ type: "TIMESTAMP WITH TIME ZONE"
4523
+ },
4524
+ {
4525
+ column: "created_at",
4526
+ type: "TIMESTAMP WITH TIME ZONE",
4527
+ default: "NOW()",
4528
+ notNull: true
4529
+ },
4530
+ {
4531
+ column: "updated_at",
4532
+ type: "TIMESTAMP WITH TIME ZONE",
4533
+ default: "NOW()",
4534
+ notNull: true
4535
+ }
4536
+ ];
4537
+ var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
4538
+ /** Type + inline constraints, as they appear after the column name. */
4539
+ function authUsersColumnSql(spec) {
4540
+ return [
4541
+ spec.type,
4542
+ spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
4543
+ spec.notNull ? "NOT NULL" : ""
4544
+ ].filter(Boolean).join(" ");
4545
+ }
4546
+ /**
4547
+ * The auth-owned definition for a physical column name, or `undefined` when
4548
+ * auth does not own it.
4549
+ *
4550
+ * Callers pass the RESOLVED column name (after `columnName` mapping), because
4551
+ * that is the only name the three creators agree on: the scaffold's users
4552
+ * collection spells the property `displayName` and the column `display_name`.
4553
+ */
4554
+ function authUsersColumnDefinition(column) {
4555
+ const spec = BY_COLUMN.get(column);
4556
+ return spec ? authUsersColumnSql(spec) : void 0;
4557
+ }
4558
+ /**
4559
+ * Whether a collection is an auth collection, i.e. whether the definitions in
4560
+ * this module apply to its table at all.
4561
+ *
4562
+ * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
4563
+ * that one takes a `CollectionConfig`, this one is called from DDL code paths
4564
+ * that hold looser objects, and both spellings must accept `auth: true` as well
4565
+ * as `auth: { enabled: true }`.
4566
+ */
4567
+ function isAuthCollection(collection) {
4568
+ const auth = collection?.auth;
4569
+ if (auth === true) return true;
4570
+ return typeof auth === "object" && auth !== null && auth.enabled === true;
4571
+ }
4572
+ //#endregion
4573
+ export { generateForeignKeyName as A, hasForeignKeyOnTarget as B, getDeclaredPrimaryKeys as C, createRelationRefWithData as D, createRelationRef as E, camelCase as F, Vector as H, toSnakeCase as I, DEFAULT_ONE_OF_TYPE as L, getPolicyNamesForRule as M, isPrototypePollutingKey as N, normalizeToEntityRelation as O, mergeDeep as P, DEFAULT_ONE_OF_VALUE as R, buildCompositeId as S, parseIdValues as T, isManyToMany as V, getColumnName as _, buildSdkData as a, getTableVarName as b, resolveStringColumnLength as c, resolveJunctionSpecs as d, getEffectiveSecurityRules as f, findRelation as g, findAnonymousGrants as h, isAuthCollection as i, legacyForeignKeyName as j, updateDateAutoValues as k, getJunctionCollectionConfig as l, securityRuleToConditions as m, authUsersColumnDefinition as n, CollectionRegistry as o, policyToPostgres as p, authUsersColumnSql as r, relationalCollections as s, AUTH_USERS_COLUMNS as t, getJunctionSecurityRules as u, getEnumVarName as v, isAddressableId as w, resolveCollectionRelations as x, getTableName as y, resolveClientListLimit as z };
4298
4574
 
4299
- //# sourceMappingURL=src-DlPBctw_.js.map
4575
+ //# sourceMappingURL=auth-users-columns-Do9mw5Y5.js.map