@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g18cfeb7

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 (112) hide show
  1. package/dist/PostgresBackendDriver.d.ts +49 -2
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/{src-BbFOPJ1S.js → auth-users-columns-Do9mw5Y5.js} +602 -200
  5. package/dist/auth-users-columns-Do9mw5Y5.js.map +1 -0
  6. package/dist/backup/backup-service.d.ts +10 -1
  7. package/dist/backup/pg-tools.d.ts +47 -0
  8. package/dist/backup-service-Bww-Lg0s.js +8999 -0
  9. package/dist/backup-service-Bww-Lg0s.js.map +1 -0
  10. package/dist/cli-helpers.d.ts +40 -1
  11. package/dist/connection-BuZ97wsr.js +250 -0
  12. package/dist/connection-BuZ97wsr.js.map +1 -0
  13. package/dist/connection.d.ts +42 -0
  14. package/dist/ensure-collection-policies-DMUdRQdM.js +57 -0
  15. package/dist/ensure-collection-policies-DMUdRQdM.js.map +1 -0
  16. package/dist/ensure-collection-tables-DSIxvLLD.js +694 -0
  17. package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
  18. package/dist/index.es.js +1701 -10167
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/policy-CPkCqVTz.js +105 -0
  21. package/dist/policy-CPkCqVTz.js.map +1 -0
  22. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  23. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  24. package/dist/schema/auth-schema.d.ts +83 -144
  25. package/dist/schema/auth-users-columns.d.ts +97 -0
  26. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  27. package/dist/schema/ensure-collection-tables.d.ts +45 -3
  28. package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
  29. package/dist/schema/generate-postgres-ddl-logic.d.ts +137 -3
  30. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  31. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  32. package/dist/schema/introspect-db-logic.d.ts +94 -5
  33. package/dist/schema/introspect-db-queries.d.ts +119 -0
  34. package/dist/schema/introspect-db-structure.d.ts +263 -0
  35. package/dist/schema/introspect-db-types.d.ts +11 -0
  36. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  37. package/dist/security/rls-enforcement.d.ts +53 -2
  38. package/dist/services/FetchService.d.ts +14 -8
  39. package/dist/services/RelationService.d.ts +24 -1
  40. package/dist/services/channel-bus/index.d.ts +1 -7
  41. package/dist/services/collection-helpers.d.ts +36 -2
  42. package/dist/services/dataService.d.ts +5 -1
  43. package/dist/services/realtimeService.d.ts +25 -21
  44. package/dist/services/row-pipeline.d.ts +1 -1
  45. package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
  46. package/dist/src-C_wvdMnl.js.map +1 -0
  47. package/dist/utils/connection-string.d.ts +29 -0
  48. package/dist/utils/drizzle-conditions.d.ts +5 -4
  49. package/dist/utils/pg-error-utils.d.ts +35 -0
  50. package/dist/websocket-D0TBU3ia.js +530 -0
  51. package/dist/websocket-D0TBU3ia.js.map +1 -0
  52. package/package.json +17 -16
  53. package/src/PostgresAdapter.ts +21 -2
  54. package/src/PostgresBackendDriver.ts +169 -3
  55. package/src/PostgresBootstrapper.ts +233 -35
  56. package/src/auth/ensure-tables.ts +335 -81
  57. package/src/auth/services.ts +24 -2
  58. package/src/backup/backup-cli.ts +41 -2
  59. package/src/backup/backup-service.ts +38 -5
  60. package/src/backup/pg-tools.ts +96 -3
  61. package/src/cli-helpers.ts +92 -9
  62. package/src/cli.ts +219 -56
  63. package/src/collections/validate-relations.ts +136 -14
  64. package/src/connection.ts +73 -0
  65. package/src/data-transformer.ts +22 -6
  66. package/src/databasePoolManager.ts +5 -2
  67. package/src/history/ensure-history-table.ts +7 -0
  68. package/src/schema/auth-schema.ts +30 -19
  69. package/src/schema/auth-users-columns.ts +131 -0
  70. package/src/schema/doctor.ts +7 -5
  71. package/src/schema/ensure-collection-policies.ts +105 -0
  72. package/src/schema/ensure-collection-tables.test.ts +105 -9
  73. package/src/schema/ensure-collection-tables.ts +301 -44
  74. package/src/schema/generate-drizzle-schema-logic.ts +49 -7
  75. package/src/schema/generate-postgres-ddl-logic.ts +474 -27
  76. package/src/schema/generated-schema-staleness.ts +169 -0
  77. package/src/schema/introspect-db-constraints.ts +385 -0
  78. package/src/schema/introspect-db-inference.ts +18 -8
  79. package/src/schema/introspect-db-logic.ts +385 -71
  80. package/src/schema/introspect-db-queries.ts +326 -0
  81. package/src/schema/introspect-db-structure.ts +670 -0
  82. package/src/schema/introspect-db-types.ts +56 -0
  83. package/src/schema/introspect-db.ts +37 -80
  84. package/src/schema/introspect-runtime.test.ts +56 -8
  85. package/src/schema/introspect-runtime.ts +31 -9
  86. package/src/schema/non-sql-collections.test.ts +131 -0
  87. package/src/schema/rls-bootstrap-sql.ts +288 -0
  88. package/src/security/anonymous-grants.test.ts +4 -2
  89. package/src/security/policy-drift.test.ts +11 -3
  90. package/src/security/rls-enforcement.ts +141 -3
  91. package/src/services/BranchService.ts +71 -28
  92. package/src/services/FetchService.ts +100 -107
  93. package/src/services/PersistService.ts +34 -7
  94. package/src/services/RelationService.ts +249 -48
  95. package/src/services/channel-bus/index.ts +0 -9
  96. package/src/services/channel-history.ts +8 -0
  97. package/src/services/channel-presence.ts +6 -0
  98. package/src/services/collection-helpers.ts +69 -3
  99. package/src/services/dataService.ts +5 -1
  100. package/src/services/realtimeService.ts +38 -35
  101. package/src/services/row-pipeline.ts +1 -1
  102. package/src/utils/connection-string.ts +58 -0
  103. package/src/utils/drizzle-conditions.ts +31 -6
  104. package/src/utils/pg-error-utils.ts +65 -0
  105. package/src/websocket.ts +18 -9
  106. package/dist/chunk-DSJWtz9O.js +0 -40
  107. package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
  108. package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
  109. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  110. package/dist/src-BbFOPJ1S.js.map +0 -1
  111. package/dist/src-Zqwaw3P5.js.map +0 -1
  112. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -1,8 +1,80 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  import "process";
3
3
  __createRequire(import.meta.url);
4
- import { r as __require, t as __commonJSMin } from "./chunk-DSJWtz9O.js";
5
- import { a as policy, c as getDeclaredSubcollections, f as getDataSourceCapabilities, g as EntityRelation, h as toCanonicalOp, i as ANONYMOUS_USER_ID, l as isPostgresCollectionConfig, m as REST_TO_CANONICAL, p as NULL_OPS, s as isManyToMany, u as isRelationalCollectionConfig } from "./src-Zqwaw3P5.js";
4
+ import { l as __require, s as __commonJSMin } from "./connection-BuZ97wsr.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
+ //#region ../types/src/types/entities.ts
8
+ /**
9
+ * Class used to create a reference to a entity in a different path
10
+ */
11
+ var EntityRelation = class {
12
+ __type = "relation";
13
+ /**
14
+ * ID of the entity
15
+ */
16
+ id;
17
+ /**
18
+ * A string representing the path of the referenced document (relative
19
+ * to the root of the database).
20
+ */
21
+ path;
22
+ /**
23
+ * Pre-fetched data payload to eliminate N+1 queries.
24
+ * When present, clients can use this directly instead of fetching.
25
+ */
26
+ data;
27
+ constructor(id, path, data) {
28
+ this.id = id;
29
+ this.path = path;
30
+ this.data = data;
31
+ }
32
+ get pathWithId() {
33
+ return `${this.path}/${this.id}`;
34
+ }
35
+ isEntityReference() {
36
+ return false;
37
+ }
38
+ isEntityRelation() {
39
+ return true;
40
+ }
41
+ };
42
+ var Vector = class {
43
+ value;
44
+ constructor(value) {
45
+ this.value = value;
46
+ }
47
+ };
48
+ //#endregion
49
+ //#region ../types/src/types/relations.ts
50
+ /** @group Models */
51
+ function hasForeignKeyOnTarget(relation) {
52
+ return relation.kind === "hasOne" || relation.kind === "hasMany";
53
+ }
54
+ /** @group Models */
55
+ function isManyToMany(relation) {
56
+ return relation.kind === "manyToMany";
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
+ }
77
+ //#endregion
6
78
  //#region ../common/src/util/common.ts
7
79
  var DEFAULT_ONE_OF_TYPE = "type";
8
80
  var DEFAULT_ONE_OF_VALUE = "value";
@@ -954,6 +1026,35 @@ function camelCase(str) {
954
1026
  });
955
1027
  })))();
956
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
+ /**
957
1058
  * Deep clone a value, preserving function references and class instances.
958
1059
  * Unlike structuredClone, this handles objects that contain functions
959
1060
  * (e.g. CollectionConfig with target(), childCollections(), callbacks).
@@ -1014,16 +1115,12 @@ function removeFunctions(o) {
1014
1115
  if (o === void 0) return void 0;
1015
1116
  if (o === null) return null;
1016
1117
  if (typeof o === "object") {
1017
- if (Array.isArray(o)) return o.map((v) => removeFunctions(v));
1118
+ if (Array.isArray(o)) return o.filter((v) => typeof v !== "function").map((v) => removeFunctions(v));
1018
1119
  if (!isPlainObject(o)) return o;
1019
- return Object.entries(o).filter(([_, value]) => typeof value !== "function").map(([key, value]) => {
1020
- if (Array.isArray(value)) return { [key]: value.map((v) => removeFunctions(v)) };
1021
- else if (typeof value === "object") return { [key]: removeFunctions(value) };
1022
- else return { [key]: value };
1023
- }).reduce((a, b) => ({
1024
- ...a,
1025
- ...b
1026
- }), {});
1120
+ return Object.entries(o).filter(([_, value]) => typeof value !== "function").reduce((acc, [key, value]) => {
1121
+ acc[key] = removeFunctions(value);
1122
+ return acc;
1123
+ }, {});
1027
1124
  }
1028
1125
  return o;
1029
1126
  }
@@ -1162,11 +1259,101 @@ function getPolicyNamesForRule(rule, tableName) {
1162
1259
  return ops.map((op, opIdx) => rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`);
1163
1260
  }
1164
1261
  //#endregion
1262
+ //#region ../utils/src/plurals.ts
1263
+ /**
1264
+ * Returns the singular of an English word.
1265
+ *
1266
+ * @param {string} word
1267
+ * @param {number} [amount]
1268
+ * @returns {string}
1269
+ */
1270
+ function singular(word, amount) {
1271
+ if (amount !== void 0 && amount !== 1) return word;
1272
+ const singulars = {
1273
+ "(quiz)zes$": "$1",
1274
+ "(matr)ices$": "$1ix",
1275
+ "(vert|ind)ices$": "$1ex",
1276
+ "^(ox)en$": "$1",
1277
+ "(alias)es$": "$1",
1278
+ "(octop|vir)i$": "$1us",
1279
+ "(cris|ax|test)es$": "$1is",
1280
+ "(shoe)s$": "$1",
1281
+ "(o)es$": "$1",
1282
+ "(bus)es$": "$1",
1283
+ "([m|l])ice$": "$1ouse",
1284
+ "(x|ch|ss|sh)es$": "$1",
1285
+ "(m)ovies$": "$1ovie",
1286
+ "(s)eries$": "$1eries",
1287
+ "([^aeiouy]|qu)ies$": "$1y",
1288
+ "([lr])ves$": "$1f",
1289
+ "(tive)s$": "$1",
1290
+ "(hive)s$": "$1",
1291
+ "(li|wi|kni)ves$": "$1fe",
1292
+ "(shea|loa|lea|thie)ves$": "$1f",
1293
+ "(^analy)ses$": "$1sis",
1294
+ "((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$": "$1$2sis",
1295
+ "([ti])a$": "$1um",
1296
+ "(n)ews$": "$1ews",
1297
+ "(h|bl)ouses$": "$1ouse",
1298
+ "(corpse)s$": "$1",
1299
+ "(us)es$": "$1",
1300
+ s$: ""
1301
+ };
1302
+ const irregular = {
1303
+ move: "moves",
1304
+ foot: "feet",
1305
+ goose: "geese",
1306
+ sex: "sexes",
1307
+ child: "children",
1308
+ man: "men",
1309
+ tooth: "teeth",
1310
+ person: "people"
1311
+ };
1312
+ if ([
1313
+ "sheep",
1314
+ "fish",
1315
+ "deer",
1316
+ "moose",
1317
+ "series",
1318
+ "species",
1319
+ "money",
1320
+ "rice",
1321
+ "information",
1322
+ "equipment",
1323
+ "bison",
1324
+ "cod",
1325
+ "offspring",
1326
+ "pike",
1327
+ "salmon",
1328
+ "shrimp",
1329
+ "swine",
1330
+ "trout",
1331
+ "aircraft",
1332
+ "hovercraft",
1333
+ "spacecraft",
1334
+ "sugar",
1335
+ "tuna",
1336
+ "you",
1337
+ "wood"
1338
+ ].indexOf(word.toLowerCase()) >= 0) return word;
1339
+ for (const w in irregular) {
1340
+ const pattern = new RegExp(`${irregular[w]}$`, "i");
1341
+ if (pattern.test(word)) return word.replace(pattern, w);
1342
+ }
1343
+ for (const reg in singulars) {
1344
+ const pattern = new RegExp(reg, "i");
1345
+ if (pattern.test(word)) return word.replace(pattern, singulars[reg]);
1346
+ }
1347
+ return word;
1348
+ }
1349
+ //#endregion
1165
1350
  //#region ../utils/src/names.ts
1166
1351
  /**
1167
1352
  * Generates a foreign key column name from a given string, typically a collection slug or name.
1168
- * It converts the name to snake_case, attempts to singularize it by removing a trailing 's'
1169
- * (a common convention for collection names), and appends '_id'.
1353
+ * It singularizes the name, converts it to snake_case and appends '_id'.
1354
+ *
1355
+ * Singularization runs *before* snake-casing so that acronyms survive: `toSnakeCase`
1356
+ * splits on every capital, which turned "URLs" into "ur_ls" and then "ur_l_id".
1170
1357
  *
1171
1358
  * @param name The base name to convert to a foreign key.
1172
1359
  * @returns A foreign key name in the format 'singular_name_id'.
@@ -1176,8 +1363,8 @@ function getPolicyNamesForRule(rule, tableName) {
1176
1363
  * generateForeignKeyName("users")
1177
1364
  *
1178
1365
  * @example
1179
- * // returns "post_id"
1180
- * generateForeignKeyName("posts")
1366
+ * // returns "category_id"
1367
+ * generateForeignKeyName("categories")
1181
1368
  *
1182
1369
  * @example
1183
1370
  * // returns "product_id"
@@ -1185,8 +1372,41 @@ function getPolicyNamesForRule(rule, tableName) {
1185
1372
  *
1186
1373
  */
1187
1374
  function generateForeignKeyName(name) {
1188
- const snakeCaseName = toSnakeCase(name);
1189
- return `${snakeCaseName.endsWith("s") ? snakeCaseName.slice(0, -1) : snakeCaseName}_id`;
1375
+ return `${toSnakeCase(singularizeForKey(name))}_id`;
1376
+ }
1377
+ /**
1378
+ * `singular()` handles real English plurals, but its final catch-all rule strips
1379
+ * any trailing "s", which mangles words that only look plural. Guard the two
1380
+ * cases that produce a column name nobody would recognise:
1381
+ *
1382
+ * - a double "s" ending is never a plural marker ("address", "class", "process"),
1383
+ * so stripping it yields "addres";
1384
+ * - a name that singularizes to nothing (the literal "s") would yield "_id".
1385
+ */
1386
+ function singularizeForKey(name) {
1387
+ if (/ss$/i.test(name)) return name;
1388
+ const result = singular(name);
1389
+ return result.length > 0 ? result : name;
1390
+ }
1391
+ /**
1392
+ * What `generateForeignKeyName` returned before it learned to singularize:
1393
+ * snake-case the name, then chop one trailing "s".
1394
+ *
1395
+ * This is here to be *detected*, never to be generated. A database provisioned
1396
+ * under the old rule carries `categorie_id`, `addresse_id`, `children_id` or
1397
+ * `ur_l_id` where the current rule expects `category_id`, `address_id`,
1398
+ * `child_id` and `url_id` — and the boot-time schema ensure is additive, so it
1399
+ * would create the new column empty beside the populated old one and leave the
1400
+ * relation reading nothing. No error, no missing table: the failure is silent,
1401
+ * which is the only reason this function still exists.
1402
+ *
1403
+ * `ensureCollectionSchema` calls it to recognise that shape and say so.
1404
+ * Returns the same string as `generateForeignKeyName` for every regular plural,
1405
+ * so a caller can compare the two and act only when they differ.
1406
+ */
1407
+ function legacyForeignKeyName(name) {
1408
+ const snake = toSnakeCase(name);
1409
+ return `${snake.endsWith("s") ? snake.slice(0, -1) : snake}_id`;
1190
1410
  }
1191
1411
  //#endregion
1192
1412
  //#region ../common/src/util/entities.ts
@@ -1211,10 +1431,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
1211
1431
  * have `id` and `path` fields — these are relation-shaped objects from
1212
1432
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
1213
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
+ *
1214
1441
  * Returns null if the value cannot be coerced.
1215
1442
  */
1216
- function normalizeToEntityRelation(value, propertyType) {
1443
+ function normalizeToEntityRelation(value, propertyType, targetPath) {
1217
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
+ }
1218
1449
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1219
1450
  const obj = value;
1220
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;
@@ -1286,6 +1517,34 @@ function createRelationRefWithData(id, path, data) {
1286
1517
  data
1287
1518
  };
1288
1519
  }
1520
+ /** The eight-four-four-four-twelve shape of a UUID, any version. */
1521
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1522
+ /** Whether one address part can be a value of the column it addresses. */
1523
+ function partIsAddressable(part, pk) {
1524
+ if (pk.isUUID) return UUID_PATTERN.test(String(part));
1525
+ if (pk.type === "number") return typeof part === "number" ? Number.isFinite(part) : !isNaN(parseInt(String(part), 10));
1526
+ return true;
1527
+ }
1528
+ /**
1529
+ * Whether an address could name a row at all, before asking the database.
1530
+ *
1531
+ * A `uuid` column cannot hold `"new"`, and an `integer` column cannot hold
1532
+ * `"abc"` — so the answer to "which row is this" is "none", and that is a 404,
1533
+ * not a failure. Postgres cannot say so politely: the comparison never runs, it
1534
+ * raises `22P02` and aborts the enclosing transaction, after which every
1535
+ * further statement returns the far less helpful `25P02`.
1536
+ *
1537
+ * `isUUID` must come from the column, not from `isId: "uuid"` in a config: the
1538
+ * config is a claim about a key, and a `text` column that holds ids of some
1539
+ * other shape is a working app this must not start rejecting.
1540
+ */
1541
+ function isAddressableId(idValue, primaryKeys) {
1542
+ if (primaryKeys.length === 0) return false;
1543
+ if (primaryKeys.length === 1) return partIsAddressable(idValue, primaryKeys[0]);
1544
+ const parts = String(idValue).split(":::");
1545
+ if (parts.length !== primaryKeys.length) return false;
1546
+ return parts.every((part, i) => partIsAddressable(part, primaryKeys[i]));
1547
+ }
1289
1548
  /**
1290
1549
  * Derive a row's address from its key columns.
1291
1550
  *
@@ -1426,8 +1685,7 @@ function enumToObjectEntries(enumValues) {
1426
1685
  function resolveRelation(relation, sourceCollection, propertyKey) {
1427
1686
  const target = relation.target;
1428
1687
  if (typeof target !== "function") throw new Error(`Relation${relation.relationName ? ` '${relation.relationName}'` : ""} on '${sourceCollection.slug}' has no \`target\`. Give it a thunk: \`target: () => otherCollection\`.`);
1429
- const targetCollection = target();
1430
- if (!targetCollection?.slug) throw new Error(`Relation${relation.relationName ? ` '${relation.relationName}'` : ""} on '${sourceCollection.slug}' has a \`target\` that did not resolve to a collection.`);
1688
+ const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);
1431
1689
  const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);
1432
1690
  const shared = {
1433
1691
  relationName,
@@ -1454,7 +1712,8 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
1454
1712
  cardinality: "one",
1455
1713
  writable: true,
1456
1714
  shared: false,
1457
- foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
1715
+ foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
1716
+ sourceKey: relation.sourceKey
1458
1717
  };
1459
1718
  case "hasMany": return {
1460
1719
  ...shared,
@@ -1462,7 +1721,8 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
1462
1721
  cardinality: "many",
1463
1722
  writable: true,
1464
1723
  shared: false,
1465
- foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
1724
+ foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
1725
+ sourceKey: relation.sourceKey
1466
1726
  };
1467
1727
  case "manyToMany": {
1468
1728
  const sourceTable = getTableName(sourceCollection);
@@ -1491,6 +1751,43 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
1491
1751
  default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
1492
1752
  }
1493
1753
  }
1754
+ /** How this relation is addressed in an error message, before it has a resolved name. */
1755
+ function describe(relation, sourceCollection, propertyKey) {
1756
+ const name = relation.relationName ?? propertyKey;
1757
+ return `Relation${name ? ` '${name}'` : ""} on '${sourceCollection.slug}'`;
1758
+ }
1759
+ /**
1760
+ * Call the `target` thunk, and translate the two ways an import cycle breaks it
1761
+ * into an error that names the cause.
1762
+ *
1763
+ * The thunk exists to defer the reference until every module has finished
1764
+ * evaluating, and for a cycle that closes at import time it does. What it cannot
1765
+ * defer is a cycle that leaves the binding permanently unusable, and there are
1766
+ * two shapes of that:
1767
+ *
1768
+ * - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are
1769
+ * in the temporal dead zone, so reading one throws `ReferenceError: x is not
1770
+ * defined`. The stack points at the thunk — a one-line arrow function that is
1771
+ * obviously fine — and says nothing about the cycle that made it throw.
1772
+ * - **CJS interop.** The half-initialised module object has no `default` yet,
1773
+ * the import resolves to `undefined`, and the thunk returns it without
1774
+ * complaint. That one used to surface here as "did not resolve to a
1775
+ * collection", which is true and unhelpful.
1776
+ *
1777
+ * Both mean the same thing, and the fix for both is the same: break the cycle,
1778
+ * or move the relation into the collection that does not close it.
1779
+ */
1780
+ function callTarget(relation, sourceCollection, propertyKey, target) {
1781
+ let targetCollection;
1782
+ try {
1783
+ targetCollection = target();
1784
+ } catch (error) {
1785
+ if (error instanceof ReferenceError) throw new Error(`${describe(relation, sourceCollection, propertyKey)} targets a collection that is not initialized yet — almost always an import cycle between the two collection files. Break the cycle (move the shared piece into a third module, or import the target lazily) so the target's module finishes evaluating before the registry is built.`, { cause: error });
1786
+ throw error;
1787
+ }
1788
+ if (!targetCollection?.slug) throw new Error(`${describe(relation, sourceCollection, propertyKey)} has a \`target\` that resolved to ${targetCollection === void 0 ? "`undefined`" : "something that is not a collection"}. ` + (targetCollection === void 0 ? "Under CommonJS interop an import cycle resolves the default import to `undefined`, so check whether this collection and its target import each other. Otherwise the thunk is returning the wrong value — it must return the collection itself, not a promise or a module." : "The thunk must return a collection config with a `slug`."));
1789
+ return targetCollection;
1790
+ }
1494
1791
  //#endregion
1495
1792
  //#region ../common/src/util/relations.ts
1496
1793
  /**
@@ -1686,9 +1983,9 @@ function isKeywordAt(upper, i, keyword) {
1686
1983
  *
1687
1984
  * This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
1688
1985
  * `AND` inside
1689
- * `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())`
1690
1987
  * split the expression, and re-emitting the halves produced
1691
- * `(EXISTS (...) AND m.user_id = auth.uid())`
1988
+ * `(EXISTS (...) AND m.user_id = rebase.uid())`
1692
1989
  * where `m` is no longer in scope — SQL that Postgres rejects outright with
1693
1990
  * "missing FROM-clause entry for table". Returning null instead keeps such a
1694
1991
  * clause as a `raw` expression, which round-trips verbatim.
@@ -1762,15 +2059,15 @@ function stripOuterParens(sql) {
1762
2059
  }
1763
2060
  }
1764
2061
  function sqlToPolicy(sql) {
1765
- const trimmed = stripOuterParens(sql.trim());
2062
+ const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
1766
2063
  if (trimmed.toLowerCase() === "true") return policy.true();
1767
2064
  if (trimmed.toLowerCase() === "false") return policy.false();
1768
- 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);
1769
2066
  if (overlapMatch) {
1770
2067
  const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
1771
2068
  return policy.rolesOverlap(roles);
1772
2069
  }
1773
- 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);
1774
2071
  if (containMatch) {
1775
2072
  const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
1776
2073
  return policy.rolesContain(roles);
@@ -1786,10 +2083,10 @@ function sqlToPolicy(sql) {
1786
2083
  const right = parseOperand(rightStr.trim());
1787
2084
  if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
1788
2085
  }
1789
- return policy.raw(sql);
2086
+ return policy.raw(trimmed);
1790
2087
  }
1791
2088
  /**
1792
- * Literals from other BaaS platforms that people compare `auth.uid()` against
2089
+ * Literals from other BaaS platforms that people compare `rebase.uid()` against
1793
2090
  * out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
1794
2091
  * `pgRoles`, one surface over: the same muscle memory inside a `using:` string
1795
2092
  * is the more dangerous spelling, because it inverts a rule instead of
@@ -1800,18 +2097,30 @@ var FOREIGN_CONVENTION_UIDS = {
1800
2097
  authenticated: "Supabase",
1801
2098
  service_role: "Supabase"
1802
2099
  };
1803
- /** `auth.uid() IS NOT NULL` in raw SQL, the clause that is always true. */
1804
- 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;
1805
2109
  /**
1806
2110
  * Find clauses that read as "signed-in users only" but admit anonymous callers.
1807
2111
  *
1808
- * Both spellings come from the same place — Supabase, where `auth.uid()` really
1809
- * 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
1810
2114
  * {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
1811
2115
  * is how the trusted *server* context is recognised), so:
1812
2116
  *
1813
- * - `auth.uid() IS NOT NULL` is a tautology on the user path, and
1814
- * - `auth.uid() != 'anon'` compares against a string no caller ever has.
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
2119
+ * other. This one is not hypothetical and was not only a foreign habit:
2120
+ * rebase's own request path reported `'anon'` while everything that compiled
2121
+ * or checked a policy used `'anonymous'`, so whichever literal an author
2122
+ * picked, half the anonymous callers walked through. See
2123
+ * {@link ANONYMOUS_USER_IDS}.
1815
2124
  *
1816
2125
  * Either one turns a lockdown into a full grant, and neither looks wrong. No
1817
2126
  * real user id is ever one of these literals, and a user-context request is
@@ -1838,7 +2147,7 @@ function findAnonymousGrants(expr) {
1838
2147
  if (UID_NOT_NULL.test(e.sql)) found.push({
1839
2148
  pattern: "uid-not-null",
1840
2149
  detail: e.sql,
1841
- 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".`
1842
2151
  });
1843
2152
  return;
1844
2153
  case "compare": {
@@ -1849,7 +2158,7 @@ function findAnonymousGrants(expr) {
1849
2158
  found.push({
1850
2159
  pattern: "foreign-uid-literal",
1851
2160
  detail: literal.value,
1852
- explanation: `'${literal.value}' is a ${platform} convention. Rebase reports an anonymous request as '${ANONYMOUS_USER_ID}', so comparing against '${literal.value}' passes for every caller. Use \`condition: policy.authenticated()\` to mean "signed in".`
2161
+ explanation: `'${literal.value}' is a ${platform} convention. Rebase reports an anonymous request as '${ANONYMOUS_USER_ID}', so comparing against '${literal.value}' passes for every caller. Use \`condition: policy.authenticated()\` to mean "signed in" — it compiles to NOT IN (${ANONYMOUS_USER_IDS.map((v) => `'${v}'`).join(", ")}), covering every spelling rebase has reported rather than whichever one you remember.`
1853
2162
  });
1854
2163
  return;
1855
2164
  }
@@ -1860,7 +2169,7 @@ function findAnonymousGrants(expr) {
1860
2169
  return found;
1861
2170
  }
1862
2171
  function parseOperand(str) {
1863
- 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();
1864
2173
  const stringMatch = str.match(/^'(.+)'$/);
1865
2174
  if (stringMatch) return policy.literal(stringMatch[1]);
1866
2175
  if (/^\w+$/.test(str)) return policy.field(str);
@@ -1942,12 +2251,12 @@ function compile(expr, scope) {
1942
2251
  const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
1943
2252
  return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
1944
2253
  }
1945
- case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
1946
- case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
1947
- case "authenticated": return `auth.uid() IS NOT NULL AND auth.uid() <> ${quoteLiteral(ANONYMOUS_USER_ID)}`;
1948
- 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`;
1949
2258
  case "existsIn": return compileExistsIn(expr, scope);
1950
- 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)}`);
1951
2260
  }
1952
2261
  }
1953
2262
  /**
@@ -1984,8 +2293,8 @@ function operandToSql(operand, scope) {
1984
2293
  case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
1985
2294
  case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
1986
2295
  case "literal": return quoteLiteral(operand.value);
1987
- case "authUid": return "auth.uid()";
1988
- 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}, ',')`;
1989
2298
  }
1990
2299
  }
1991
2300
  /**
@@ -2016,75 +2325,6 @@ function rolesArraySql(roles) {
2016
2325
  return `ARRAY[${[...roles].sort().map((r) => `'${r}'`).join(",")}]`;
2017
2326
  }
2018
2327
  //#endregion
2019
- //#region ../common/src/util/callbacks.ts
2020
- /**
2021
- * Helper function to recursively check if there are any callbacks in the properties.
2022
- */
2023
- function hasPropertyCallbacks(properties, callbackName) {
2024
- if (!properties) return false;
2025
- for (const property of Object.values(properties)) {
2026
- if (property.callbacks?.[callbackName]) return true;
2027
- if (property.type === "map" && property.properties) {
2028
- if (hasPropertyCallbacks(property.properties, callbackName)) return true;
2029
- } else if (property.type === "array" && property.of) {
2030
- const ofs = Array.isArray(property.of) ? property.of : [property.of];
2031
- for (const of of ofs) {
2032
- if (of.callbacks?.[callbackName]) return true;
2033
- if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
2034
- }
2035
- }
2036
- }
2037
- return false;
2038
- }
2039
- /**
2040
- * Recursively process properties to apply field-level hooks.
2041
- */
2042
- async function processProperties(properties, values, previousValues, propsContext, callbackName) {
2043
- if (!values || typeof values !== "object") return values;
2044
- const result = { ...values };
2045
- for (const [key, property] of Object.entries(properties)) {
2046
- if (result[key] === void 0) continue;
2047
- let currentValue = result[key];
2048
- const previousValue = previousValues?.[key];
2049
- if (property.type === "array" && Array.isArray(currentValue)) {
2050
- if (property.of && !Array.isArray(property.of)) currentValue = await Promise.all(currentValue.map(async (item, index) => {
2051
- const prevItem = Array.isArray(previousValue) ? previousValue[index] : void 0;
2052
- return (await processProperties({ "_tmp": property.of }, { "_tmp": item }, { "_tmp": prevItem }, propsContext, callbackName))["_tmp"];
2053
- }));
2054
- } else if (property.type === "map" && property.properties && typeof currentValue === "object") currentValue = await processProperties(property.properties, currentValue, previousValue ?? {}, propsContext, callbackName);
2055
- if (property.callbacks?.[callbackName]) {
2056
- const cbRes = await Promise.resolve(property.callbacks[callbackName]({
2057
- ...propsContext,
2058
- value: currentValue,
2059
- previousValue
2060
- }));
2061
- if (cbRes !== void 0) currentValue = cbRes;
2062
- }
2063
- result[key] = currentValue;
2064
- }
2065
- return result;
2066
- }
2067
- /**
2068
- * Helper function to extract field-level PropertyCallbacks from a properties schema
2069
- * and wrap them into an CollectionCallbacks object recursively.
2070
- */
2071
- var buildPropertyCallbacks = (properties) => {
2072
- if (!properties) return void 0;
2073
- const propertyCallbacks = {};
2074
- if (hasPropertyCallbacks(properties, "afterRead")) propertyCallbacks.afterRead = async (props) => {
2075
- const row = props.row;
2076
- const processedValues = await processProperties(properties, row, row, props, "afterRead");
2077
- return {
2078
- ...props.row,
2079
- ...processedValues
2080
- };
2081
- };
2082
- if (hasPropertyCallbacks(properties, "beforeSave")) propertyCallbacks.beforeSave = async (props) => {
2083
- return await processProperties(properties, props.values, props.previousValues ?? {}, props, "beforeSave");
2084
- };
2085
- return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
2086
- };
2087
- //#endregion
2088
2328
  //#region ../common/src/util/auth-default-policies.ts
2089
2329
  /**
2090
2330
  * Default RLS policies injected by the schema generator.
@@ -2134,7 +2374,7 @@ var DEFAULT_GUARDED_OPS = [
2134
2374
  "delete"
2135
2375
  ];
2136
2376
  /** Whether a collection is flagged as an authentication collection. */
2137
- function isAuthCollection(collection) {
2377
+ function isAuthCollection$1(collection) {
2138
2378
  const auth = collection.auth;
2139
2379
  return auth === true || typeof auth === "object" && auth?.enabled === true;
2140
2380
  }
@@ -2167,7 +2407,7 @@ function getEffectiveSecurityRules(collection) {
2167
2407
  condition: SERVER_OR_ADMIN_EXPR$1,
2168
2408
  check: SERVER_OR_ADMIN_EXPR$1
2169
2409
  });
2170
- if (isAuthCollection(collection)) {
2410
+ if (isAuthCollection$1(collection)) {
2171
2411
  injected.push({
2172
2412
  name: `${tableName}_default_self_read`,
2173
2413
  operations: ["select"],
@@ -2659,10 +2899,27 @@ function getJunctionSecurityRules(spec) {
2659
2899
  });
2660
2900
  })))();
2661
2901
  /**
2662
- * Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
2902
+ * How wide a `varchar`/`char` column should be for a given property.
2903
+ *
2904
+ * One definition, three call sites, because they used to disagree. For the same
2905
+ * `columnType: "varchar"` property the DDL generator emitted `VARCHAR(255)`
2906
+ * while the Drizzle generator emitted a bare `varchar("col")` — which Postgres
2907
+ * reads as *unbounded* — so which of the two you ran decided whether the column
2908
+ * had a limit at all. Introspection then dropped the length entirely, so reading
2909
+ * an existing `character varying(500)` column back and regenerating it produced
2910
+ * a `VARCHAR(255)`: a silent narrowing of a column with data already in it.
2911
+ *
2912
+ * `validation.max` is the property's own statement about how long the value may
2913
+ * be, so it is the only sensible source for the column's width — and it keeps
2914
+ * the constraint the database enforces in step with the one the app enforces,
2915
+ * rather than inventing a second, different limit underneath it.
2663
2916
  */
2917
+ function resolveStringColumnLength(prop) {
2918
+ const max = prop.validation?.max;
2919
+ return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : 255;
2920
+ }
2664
2921
  //#endregion
2665
- //#region ../../node_modules/.pnpm/fast-equals@6.0.0/node_modules/fast-equals/dist/es/index.mjs
2922
+ //#region ../../node_modules/.pnpm/fast-equals@6.0.2/node_modules/fast-equals/dist/es/index.mjs
2666
2923
  var { getOwnPropertyNames, getOwnPropertySymbols } = Object;
2667
2924
  var { hasOwnProperty } = Object.prototype;
2668
2925
  /**
@@ -2698,7 +2955,8 @@ function createIsCircular(areItemsEqual) {
2698
2955
  * not enumerable and symbol properties.
2699
2956
  */
2700
2957
  function getStrictProperties(object) {
2701
- return getOwnPropertyNames(object).concat(getOwnPropertySymbols(object));
2958
+ const symbols = getOwnPropertySymbols(object);
2959
+ return symbols.length ? getOwnPropertyNames(object).concat(symbols) : getOwnPropertyNames(object);
2702
2960
  }
2703
2961
  /**
2704
2962
  * Whether the object contains the property passed as an own property.
@@ -2771,7 +3029,7 @@ function areMapsEqual(a, b, state) {
2771
3029
  const size = a.size;
2772
3030
  if (size !== b.size) return false;
2773
3031
  if (!size) return true;
2774
- const matchedIndices = new Array(size);
3032
+ const matchedIndices = new Uint8Array(size);
2775
3033
  const aIterable = a.entries();
2776
3034
  let aResult;
2777
3035
  let bResult;
@@ -2779,7 +3037,7 @@ function areMapsEqual(a, b, state) {
2779
3037
  while (aResult = aIterable.next()) {
2780
3038
  if (aResult.done) break;
2781
3039
  const bIterable = b.entries();
2782
- let hasMatch = false;
3040
+ let hasMatch = 0;
2783
3041
  let matchIndex = 0;
2784
3042
  while (bResult = bIterable.next()) {
2785
3043
  if (bResult.done) break;
@@ -2790,7 +3048,7 @@ function areMapsEqual(a, b, state) {
2790
3048
  const aEntry = aResult.value;
2791
3049
  const bEntry = bResult.value;
2792
3050
  if (state.equals(aEntry[0], bEntry[0], index, matchIndex, a, b, state) && state.equals(aEntry[1], bEntry[1], aEntry[0], bEntry[0], a, b, state)) {
2793
- hasMatch = matchedIndices[matchIndex] = true;
3051
+ hasMatch = matchedIndices[matchIndex] = 1;
2794
3052
  break;
2795
3053
  }
2796
3054
  matchIndex++;
@@ -2848,19 +3106,19 @@ function areSetsEqual(a, b, state) {
2848
3106
  const size = a.size;
2849
3107
  if (size !== b.size) return false;
2850
3108
  if (!size) return true;
2851
- const matchedIndices = new Array(size);
3109
+ const matchedIndices = new Uint8Array(size);
2852
3110
  const aIterable = a.values();
2853
3111
  let aResult;
2854
3112
  let bResult;
2855
3113
  while (aResult = aIterable.next()) {
2856
3114
  if (aResult.done) break;
2857
3115
  const bIterable = b.values();
2858
- let hasMatch = false;
3116
+ let hasMatch = 0;
2859
3117
  let matchIndex = 0;
2860
3118
  while (bResult = bIterable.next()) {
2861
3119
  if (bResult.done) break;
2862
3120
  if (!matchedIndices[matchIndex] && state.equals(aResult.value, bResult.value, aResult.value, bResult.value, a, b, state)) {
2863
- hasMatch = matchedIndices[matchIndex] = true;
3121
+ hasMatch = matchedIndices[matchIndex] = 1;
2864
3122
  break;
2865
3123
  }
2866
3124
  matchIndex++;
@@ -2873,8 +3131,8 @@ function areSetsEqual(a, b, state) {
2873
3131
  * Whether the TypedArray instances are equal in value.
2874
3132
  */
2875
3133
  function areTypedArraysEqual(a, b) {
2876
- let index = a.byteLength;
2877
- if (b.byteLength !== index || a.byteOffset !== b.byteOffset) return false;
3134
+ let index = a.length;
3135
+ if (b.length !== index || a.byteOffset !== b.byteOffset) return false;
2878
3136
  while (index-- > 0) if (a[index] !== b[index]) return false;
2879
3137
  return true;
2880
3138
  }
@@ -3116,6 +3374,46 @@ function resolveDataSource(collection, registry) {
3116
3374
  capabilities: getDataSourceCapabilities(engine)
3117
3375
  };
3118
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
+ }
3119
3417
  //#endregion
3120
3418
  //#region ../common/src/collections/CollectionRegistry.ts
3121
3419
  var CollectionRegistry = class {
@@ -3468,6 +3766,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
3468
3766
  Object.setPrototypeOf(this, RebasePaginationError.prototype);
3469
3767
  }
3470
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
+ }
3471
3793
  function normalizePageSize(raw) {
3472
3794
  if (raw === void 0 || !Number.isFinite(raw)) return 200;
3473
3795
  return Math.max(1, Math.floor(raw));
@@ -3724,7 +4046,7 @@ function rowToEntity(row, slug, primaryKeys = []) {
3724
4046
  };
3725
4047
  }
3726
4048
  /**
3727
- * The relation envelope `toCmsRow` writes where a relation was:
4049
+ * The relation envelope `toFlatRow` writes where a relation was:
3728
4050
  * `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
3729
4051
  * admin's view-model, and the only pipeline that produces one is postgres'.
3730
4052
  */
@@ -3763,21 +4085,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3763
4085
  const accessor = {
3764
4086
  async find(params) {
3765
4087
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3766
- const limit = params?.limit ?? 20;
3767
- const offset = params?.offset ?? 0;
4088
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3768
4089
  const fetchService = driver.restFetchService;
3769
4090
  const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
3770
4091
  filter,
3771
- limit: params?.limit,
3772
- offset: params?.offset,
4092
+ logical: params?.logical,
4093
+ limit,
4094
+ offset: driverOffset,
3773
4095
  orderBy: params?.orderBy?.[0],
3774
4096
  order: params?.orderBy?.[1],
3775
4097
  searchString: params?.searchString
3776
4098
  }, params?.include) : await driver.fetchCollection({
3777
4099
  path: slug,
3778
- limit: params?.limit,
3779
- offset: params?.offset,
4100
+ limit,
4101
+ offset: driverOffset,
3780
4102
  filter,
4103
+ logical: params?.logical,
3781
4104
  orderBy: params?.orderBy?.[0],
3782
4105
  order: params?.orderBy?.[1],
3783
4106
  searchString: params?.searchString
@@ -3787,7 +4110,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3787
4110
  if (driver.count) {
3788
4111
  total = await driver.count({
3789
4112
  path: slug,
3790
- filter
4113
+ filter,
4114
+ logical: params?.logical,
4115
+ searchString: params?.searchString
3791
4116
  });
3792
4117
  hasMore = offset + rows.length < total;
3793
4118
  }
@@ -3839,22 +4164,39 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3839
4164
  values: {}
3840
4165
  } });
3841
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,
3842
4182
  count: driver.count ? async (params) => {
3843
4183
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3844
4184
  return driver.count({
3845
4185
  path: slug,
3846
- filter
4186
+ filter,
4187
+ logical: params?.logical,
4188
+ searchString: params?.searchString
3847
4189
  });
3848
4190
  } : void 0,
3849
4191
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
3850
- const limit = params?.limit ?? 20;
3851
- const offset = params?.offset ?? 0;
4192
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3852
4193
  const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
3853
4194
  return driver.listenCollection({
3854
4195
  path: slug,
3855
- limit: params?.limit,
3856
- offset: params?.offset,
4196
+ limit,
4197
+ offset: driverOffset,
3857
4198
  filter: params?.where,
4199
+ logical: params?.logical,
3858
4200
  orderBy: params?.orderBy?.[0],
3859
4201
  order: params?.orderBy?.[1],
3860
4202
  searchString: params?.searchString,
@@ -3862,7 +4204,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3862
4204
  onUpdate({
3863
4205
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
3864
4206
  meta: {
3865
- total: entities.length,
4207
+ total: offset + entities.length,
3866
4208
  limit,
3867
4209
  offset,
3868
4210
  hasMore: entities.length >= limit
@@ -4039,9 +4381,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
4039
4381
  async update(id, data) {
4040
4382
  return entityToRow(await snap.update(id, data));
4041
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
+ },
4042
4393
  delete(id) {
4043
4394
  return snap.delete(id);
4044
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
+ },
4045
4402
  count: snap.count ? (params) => snap.count(params) : void 0,
4046
4403
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
4047
4404
  data: res.data.map(entityToRow),
@@ -4102,72 +4459,117 @@ function buildSdkData(driver) {
4102
4459
  return wrapAsSdkData(buildRebaseData(driver));
4103
4460
  }
4104
4461
  //#endregion
4105
- //#region ../common/src/table-classification.ts
4106
- /** Schemas that are always considered Rebase-internal. */
4107
- var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
4108
- /** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
4109
- var REBASE_INTERNAL_PREFIXES = [
4110
- "_rebase_",
4111
- "_auth_",
4112
- "drizzle_"
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
+ }
4113
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
+ }
4114
4546
  /**
4115
- * Synchronously classify a table based on naming conventions.
4547
+ * The auth-owned definition for a physical column name, or `undefined` when
4548
+ * auth does not own it.
4116
4549
  *
4117
- * @param tableName - The unqualified name of the table.
4118
- * @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
4119
- * @returns `"rebase-internal"` when the table belongs to a reserved schema or
4120
- * carries a reserved prefix; `"user"` otherwise.
4121
- *
4122
- * @remarks
4123
- * Junction-table detection requires an async database query and is therefore
4124
- * **not** handled by this function. Use {@link detectJunctionTables} to obtain
4125
- * the set of junction tables, then reclassify as needed.
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`.
4126
4553
  */
4127
- function classifyTable(tableName, schemaName) {
4128
- if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "rebase-internal";
4129
- return "user";
4130
- }
4131
- /** SQL query that detects junction tables in the `public` schema. */
4132
- var JUNCTION_TABLES_SQL = `
4133
- SELECT t.table_name
4134
- FROM information_schema.tables t
4135
- WHERE t.table_schema = 'public'
4136
- AND t.table_type = 'BASE TABLE'
4137
- AND NOT EXISTS (
4138
- SELECT 1
4139
- FROM information_schema.columns c
4140
- WHERE c.table_schema = t.table_schema
4141
- AND c.table_name = t.table_name
4142
- AND c.column_name NOT IN (
4143
- SELECT kcu.column_name
4144
- FROM information_schema.key_column_usage kcu
4145
- JOIN information_schema.table_constraints tc
4146
- ON tc.constraint_name = kcu.constraint_name
4147
- AND tc.table_schema = kcu.table_schema
4148
- WHERE tc.constraint_type = 'FOREIGN KEY'
4149
- AND kcu.table_schema = t.table_schema
4150
- AND kcu.table_name = t.table_name
4151
- )
4152
- )
4153
- `;
4554
+ function authUsersColumnDefinition(column) {
4555
+ const spec = BY_COLUMN.get(column);
4556
+ return spec ? authUsersColumnSql(spec) : void 0;
4557
+ }
4154
4558
  /**
4155
- * Asynchronously detect junction (link) tables in the `public` schema.
4559
+ * Whether a collection is an auth collection, i.e. whether the definitions in
4560
+ * this module apply to its table at all.
4156
4561
  *
4157
- * A junction table is defined as a table where **every** column participates in
4158
- * at least one foreign-key constraint.
4159
- *
4160
- * @param executeSql - A callback that executes a raw SQL string and returns the
4161
- * resulting rows.
4162
- * @returns A `Set` containing the names of all detected junction tables.
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 }`.
4163
4566
  */
4164
- async function detectJunctionTables(executeSql) {
4165
- const rows = await executeSql(JUNCTION_TABLES_SQL);
4166
- const junctionTables = /* @__PURE__ */ new Set();
4167
- for (const row of rows) if (typeof row.table_name === "string") junctionTables.add(row.table_name);
4168
- return junctionTables;
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;
4169
4571
  }
4170
4572
  //#endregion
4171
- export { toSnakeCase as A, createRelationRefWithData as C, getPolicyNamesForRule as D, generateForeignKeyName as E, DEFAULT_ONE_OF_VALUE as M, mergeDeep as O, createRelationRef as S, updateDateAutoValues as T, getTableVarName as _, getJunctionCollectionConfig as a, getDeclaredPrimaryKeys as b, getEffectiveSecurityRules as c, securityRuleToConditions as d, findAnonymousGrants as f, getTableName as g, getEnumVarName as h, CollectionRegistry as i, DEFAULT_ONE_OF_TYPE as j, camelCase as k, buildPropertyCallbacks as l, getColumnName as m, detectJunctionTables as n, getJunctionSecurityRules as o, findRelation as p, buildSdkData as r, resolveJunctionSpecs as s, classifyTable as t, policyToPostgres as u, resolveCollectionRelations as v, normalizeToEntityRelation as w, parseIdValues as x, buildCompositeId as y };
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 };
4172
4574
 
4173
- //# sourceMappingURL=src-BbFOPJ1S.js.map
4575
+ //# sourceMappingURL=auth-users-columns-Do9mw5Y5.js.map