@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.
- package/dist/PostgresBackendDriver.d.ts +49 -2
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-BbFOPJ1S.js → auth-users-columns-Do9mw5Y5.js} +602 -200
- package/dist/auth-users-columns-Do9mw5Y5.js.map +1 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-Bww-Lg0s.js +8999 -0
- package/dist/backup-service-Bww-Lg0s.js.map +1 -0
- package/dist/cli-helpers.d.ts +40 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-DMUdRQdM.js +57 -0
- package/dist/ensure-collection-policies-DMUdRQdM.js.map +1 -0
- package/dist/ensure-collection-tables-DSIxvLLD.js +694 -0
- package/dist/ensure-collection-tables-DSIxvLLD.js.map +1 -0
- package/dist/index.es.js +1701 -10167
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CPkCqVTz.js +105 -0
- package/dist/policy-CPkCqVTz.js.map +1 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +45 -3
- package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +137 -3
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/security/rls-enforcement.d.ts +53 -2
- package/dist/services/FetchService.d.ts +14 -8
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +36 -2
- package/dist/services/dataService.d.ts +5 -1
- package/dist/services/realtimeService.d.ts +25 -21
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +5 -4
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-D0TBU3ia.js +530 -0
- package/dist/websocket-D0TBU3ia.js.map +1 -0
- package/package.json +17 -16
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +169 -3
- package/src/PostgresBootstrapper.ts +233 -35
- package/src/auth/ensure-tables.ts +335 -81
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +92 -9
- package/src/cli.ts +219 -56
- package/src/collections/validate-relations.ts +136 -14
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +22 -6
- package/src/databasePoolManager.ts +5 -2
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor.ts +7 -5
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +301 -44
- package/src/schema/generate-drizzle-schema-logic.ts +49 -7
- package/src/schema/generate-postgres-ddl-logic.ts +474 -27
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/policy-drift.test.ts +11 -3
- package/src/security/rls-enforcement.ts +141 -3
- package/src/services/BranchService.ts +71 -28
- package/src/services/FetchService.ts +100 -107
- package/src/services/PersistService.ts +34 -7
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/channel-history.ts +8 -0
- package/src/services/channel-presence.ts +6 -0
- package/src/services/collection-helpers.ts +69 -3
- package/src/services/dataService.ts +5 -1
- package/src/services/realtimeService.ts +38 -35
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +65 -0
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
- 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 {
|
|
5
|
-
import { a as
|
|
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").
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
|
1169
|
-
*
|
|
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 "
|
|
1180
|
-
* generateForeignKeyName("
|
|
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
|
-
|
|
1189
|
-
|
|
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 =
|
|
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 =
|
|
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*
|
|
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*
|
|
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(
|
|
2086
|
+
return policy.raw(trimmed);
|
|
1790
2087
|
}
|
|
1791
2088
|
/**
|
|
1792
|
-
* Literals from other BaaS platforms that people compare `
|
|
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
|
-
/**
|
|
1804
|
-
|
|
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()`
|
|
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
|
-
* - `
|
|
1814
|
-
* - `
|
|
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: `\`
|
|
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) || /
|
|
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(
|
|
1946
|
-
case "rolesContain": return `string_to_array(
|
|
1947
|
-
case "authenticated": return
|
|
1948
|
-
case "serverContext": return
|
|
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
|
|
1988
|
-
case "authRoles": return
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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] =
|
|
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
|
|
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 =
|
|
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] =
|
|
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.
|
|
2877
|
-
if (b.
|
|
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 `
|
|
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
|
|
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
|
-
|
|
3772
|
-
|
|
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
|
|
3779
|
-
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
|
|
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
|
|
3856
|
-
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
|
|
4106
|
-
/**
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
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
|
-
*
|
|
4547
|
+
* The auth-owned definition for a physical column name, or `undefined` when
|
|
4548
|
+
* auth does not own it.
|
|
4116
4549
|
*
|
|
4117
|
-
*
|
|
4118
|
-
*
|
|
4119
|
-
*
|
|
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
|
|
4128
|
-
|
|
4129
|
-
return
|
|
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
|
-
*
|
|
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
|
-
*
|
|
4158
|
-
*
|
|
4159
|
-
*
|
|
4160
|
-
*
|
|
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
|
-
|
|
4165
|
-
const
|
|
4166
|
-
|
|
4167
|
-
|
|
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 {
|
|
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=
|
|
4575
|
+
//# sourceMappingURL=auth-users-columns-Do9mw5Y5.js.map
|