@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g06dbe5b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBackendDriver.d.ts +49 -2
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +40 -0
- package/dist/{src-BbFOPJ1S.js → auth-users-columns-CBEOeYqa.js} +1118 -220
- package/dist/auth-users-columns-CBEOeYqa.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 +96 -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/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-B_JMGa5K.js +57 -0
- package/dist/ensure-collection-policies-B_JMGa5K.js.map +1 -0
- package/dist/ensure-collection-tables-DzeTEvMv.js +800 -0
- package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
- package/dist/index.es.js +2460 -10233
- package/dist/index.es.js.map +1 -1
- 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 +185 -144
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +23 -25
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +99 -3
- package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +185 -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/schema/search-column.d.ts +248 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +38 -8
- package/dist/services/RelationService.d.ts +54 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +62 -2
- package/dist/services/dataService.d.ts +8 -1
- package/dist/services/realtimeService.d.ts +131 -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 +129 -6
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-D1qbmLZ2.js +587 -0
- package/dist/websocket-D1qbmLZ2.js.map +1 -0
- package/package.json +17 -16
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +176 -6
- package/src/PostgresBootstrapper.ts +245 -37
- package/src/auth/ensure-tables.ts +357 -81
- package/src/auth/services.ts +93 -7
- 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 +199 -10
- package/src/cli.ts +276 -56
- package/src/collections/validate-relations.ts +136 -14
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +129 -20
- package/src/databasePoolManager.ts +5 -2
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-schema.ts +46 -19
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +12 -63
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +135 -76
- 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 +572 -42
- package/src/schema/generate-drizzle-schema-logic.ts +169 -43
- package/src/schema/generate-postgres-ddl-logic.ts +670 -29
- package/src/schema/generate-postgres-ddl.ts +25 -2
- 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 +446 -100
- 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/schema/search-column.ts +643 -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 +149 -6
- package/src/services/BranchService.ts +71 -28
- package/src/services/FetchService.ts +265 -122
- package/src/services/PersistService.ts +57 -7
- package/src/services/RelationService.ts +358 -114
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +123 -4
- package/src/services/dataService.ts +8 -1
- package/src/services/realtimeService.ts +346 -81
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +396 -29
- package/src/utils/pg-error-utils.ts +73 -3
- package/src/websocket.ts +130 -24
- 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,62 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import {
|
|
5
|
-
import { a as policy,
|
|
4
|
+
import { l as __require, s as __commonJSMin } from "./connection-BuZ97wsr.js";
|
|
5
|
+
import { a as policy, i as ANONYMOUS_USER_IDS, r as ANONYMOUS_USER_ID } from "./data_driver-ULAyJEi9.js";
|
|
6
|
+
import { a as rewriteLegacyRlsFunctions, c as isPostgresCollectionConfig, d as getDataSourceCapabilities, f as NULL_OPS, i as RLS_UID_SQL, l as isRelationalCollectionConfig, m as toCanonicalOp, p as REST_TO_CANONICAL, r as RLS_ROLES_SQL, s as getDeclaredSubcollections } from "./src-C_wvdMnl.js";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
//#region ../types/src/types/entities.ts
|
|
9
|
+
/**
|
|
10
|
+
* Class used to create a reference to a entity in a different path
|
|
11
|
+
*/
|
|
12
|
+
var EntityRelation = class {
|
|
13
|
+
__type = "relation";
|
|
14
|
+
/**
|
|
15
|
+
* ID of the entity
|
|
16
|
+
*/
|
|
17
|
+
id;
|
|
18
|
+
/**
|
|
19
|
+
* A string representing the path of the referenced document (relative
|
|
20
|
+
* to the root of the database).
|
|
21
|
+
*/
|
|
22
|
+
path;
|
|
23
|
+
/**
|
|
24
|
+
* Pre-fetched data payload to eliminate N+1 queries.
|
|
25
|
+
* When present, clients can use this directly instead of fetching.
|
|
26
|
+
*/
|
|
27
|
+
data;
|
|
28
|
+
constructor(id, path, data) {
|
|
29
|
+
this.id = id;
|
|
30
|
+
this.path = path;
|
|
31
|
+
this.data = data;
|
|
32
|
+
}
|
|
33
|
+
get pathWithId() {
|
|
34
|
+
return `${this.path}/${this.id}`;
|
|
35
|
+
}
|
|
36
|
+
isEntityReference() {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
isEntityRelation() {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var Vector = class {
|
|
44
|
+
value;
|
|
45
|
+
constructor(value) {
|
|
46
|
+
this.value = value;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region ../types/src/types/relations.ts
|
|
51
|
+
/** @group Models */
|
|
52
|
+
function hasForeignKeyOnTarget(relation) {
|
|
53
|
+
return relation.kind === "hasOne" || relation.kind === "hasMany";
|
|
54
|
+
}
|
|
55
|
+
/** @group Models */
|
|
56
|
+
function isManyToMany(relation) {
|
|
57
|
+
return relation.kind === "manyToMany";
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
6
60
|
//#region ../common/src/util/common.ts
|
|
7
61
|
var DEFAULT_ONE_OF_TYPE = "type";
|
|
8
62
|
var DEFAULT_ONE_OF_VALUE = "value";
|
|
@@ -954,6 +1008,35 @@ function camelCase(str) {
|
|
|
954
1008
|
});
|
|
955
1009
|
})))();
|
|
956
1010
|
/**
|
|
1011
|
+
* Segments that reach the prototype chain rather than a property of the object.
|
|
1012
|
+
*
|
|
1013
|
+
* The twin of this function in `@rebasepro/forms` could be made to write onto
|
|
1014
|
+
* `Object.prototype` through a path of `__proto__.x`. This copy survives the
|
|
1015
|
+
* write by accident — its `clone` always spreads into a fresh object, while the
|
|
1016
|
+
* form engine's has a "preserve class instances" branch that hands back
|
|
1017
|
+
* `Object.prototype` itself — but `getIn` still *reads* through the chain, and
|
|
1018
|
+
* handing back `Object.prototype` is how a polluted value is read out again.
|
|
1019
|
+
*
|
|
1020
|
+
* Closed on both sides here, so the two implementations agree.
|
|
1021
|
+
*/
|
|
1022
|
+
var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1023
|
+
"__proto__",
|
|
1024
|
+
"constructor",
|
|
1025
|
+
"prototype"
|
|
1026
|
+
]);
|
|
1027
|
+
/**
|
|
1028
|
+
* Whether writing this single key with `obj[key] = …` would reach the prototype
|
|
1029
|
+
* chain instead of creating a property.
|
|
1030
|
+
*
|
|
1031
|
+
* The single-key counterpart of {@link pathTraversesPrototype}, for the many
|
|
1032
|
+
* places that copy an object one key at a time. `JSON.parse` creates
|
|
1033
|
+
* `__proto__` as an *own* property, so it survives `hasOwnProperty` — and then
|
|
1034
|
+
* `target[key] = value` invokes the setter and replaces the target's prototype.
|
|
1035
|
+
*/
|
|
1036
|
+
function isPrototypePollutingKey(key) {
|
|
1037
|
+
return UNSAFE_PATH_SEGMENTS.has(key);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
957
1040
|
* Deep clone a value, preserving function references and class instances.
|
|
958
1041
|
* Unlike structuredClone, this handles objects that contain functions
|
|
959
1042
|
* (e.g. CollectionConfig with target(), childCollections(), callbacks).
|
|
@@ -1014,16 +1097,12 @@ function removeFunctions(o) {
|
|
|
1014
1097
|
if (o === void 0) return void 0;
|
|
1015
1098
|
if (o === null) return null;
|
|
1016
1099
|
if (typeof o === "object") {
|
|
1017
|
-
if (Array.isArray(o)) return o.map((v) => removeFunctions(v));
|
|
1100
|
+
if (Array.isArray(o)) return o.filter((v) => typeof v !== "function").map((v) => removeFunctions(v));
|
|
1018
1101
|
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
|
-
}), {});
|
|
1102
|
+
return Object.entries(o).filter(([_, value]) => typeof value !== "function").reduce((acc, [key, value]) => {
|
|
1103
|
+
acc[key] = removeFunctions(value);
|
|
1104
|
+
return acc;
|
|
1105
|
+
}, {});
|
|
1027
1106
|
}
|
|
1028
1107
|
return o;
|
|
1029
1108
|
}
|
|
@@ -1162,11 +1241,101 @@ function getPolicyNamesForRule(rule, tableName) {
|
|
|
1162
1241
|
return ops.map((op, opIdx) => rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`);
|
|
1163
1242
|
}
|
|
1164
1243
|
//#endregion
|
|
1244
|
+
//#region ../utils/src/plurals.ts
|
|
1245
|
+
/**
|
|
1246
|
+
* Returns the singular of an English word.
|
|
1247
|
+
*
|
|
1248
|
+
* @param {string} word
|
|
1249
|
+
* @param {number} [amount]
|
|
1250
|
+
* @returns {string}
|
|
1251
|
+
*/
|
|
1252
|
+
function singular(word, amount) {
|
|
1253
|
+
if (amount !== void 0 && amount !== 1) return word;
|
|
1254
|
+
const singulars = {
|
|
1255
|
+
"(quiz)zes$": "$1",
|
|
1256
|
+
"(matr)ices$": "$1ix",
|
|
1257
|
+
"(vert|ind)ices$": "$1ex",
|
|
1258
|
+
"^(ox)en$": "$1",
|
|
1259
|
+
"(alias)es$": "$1",
|
|
1260
|
+
"(octop|vir)i$": "$1us",
|
|
1261
|
+
"(cris|ax|test)es$": "$1is",
|
|
1262
|
+
"(shoe)s$": "$1",
|
|
1263
|
+
"(o)es$": "$1",
|
|
1264
|
+
"(bus)es$": "$1",
|
|
1265
|
+
"([m|l])ice$": "$1ouse",
|
|
1266
|
+
"(x|ch|ss|sh)es$": "$1",
|
|
1267
|
+
"(m)ovies$": "$1ovie",
|
|
1268
|
+
"(s)eries$": "$1eries",
|
|
1269
|
+
"([^aeiouy]|qu)ies$": "$1y",
|
|
1270
|
+
"([lr])ves$": "$1f",
|
|
1271
|
+
"(tive)s$": "$1",
|
|
1272
|
+
"(hive)s$": "$1",
|
|
1273
|
+
"(li|wi|kni)ves$": "$1fe",
|
|
1274
|
+
"(shea|loa|lea|thie)ves$": "$1f",
|
|
1275
|
+
"(^analy)ses$": "$1sis",
|
|
1276
|
+
"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$": "$1$2sis",
|
|
1277
|
+
"([ti])a$": "$1um",
|
|
1278
|
+
"(n)ews$": "$1ews",
|
|
1279
|
+
"(h|bl)ouses$": "$1ouse",
|
|
1280
|
+
"(corpse)s$": "$1",
|
|
1281
|
+
"(us)es$": "$1",
|
|
1282
|
+
s$: ""
|
|
1283
|
+
};
|
|
1284
|
+
const irregular = {
|
|
1285
|
+
move: "moves",
|
|
1286
|
+
foot: "feet",
|
|
1287
|
+
goose: "geese",
|
|
1288
|
+
sex: "sexes",
|
|
1289
|
+
child: "children",
|
|
1290
|
+
man: "men",
|
|
1291
|
+
tooth: "teeth",
|
|
1292
|
+
person: "people"
|
|
1293
|
+
};
|
|
1294
|
+
if ([
|
|
1295
|
+
"sheep",
|
|
1296
|
+
"fish",
|
|
1297
|
+
"deer",
|
|
1298
|
+
"moose",
|
|
1299
|
+
"series",
|
|
1300
|
+
"species",
|
|
1301
|
+
"money",
|
|
1302
|
+
"rice",
|
|
1303
|
+
"information",
|
|
1304
|
+
"equipment",
|
|
1305
|
+
"bison",
|
|
1306
|
+
"cod",
|
|
1307
|
+
"offspring",
|
|
1308
|
+
"pike",
|
|
1309
|
+
"salmon",
|
|
1310
|
+
"shrimp",
|
|
1311
|
+
"swine",
|
|
1312
|
+
"trout",
|
|
1313
|
+
"aircraft",
|
|
1314
|
+
"hovercraft",
|
|
1315
|
+
"spacecraft",
|
|
1316
|
+
"sugar",
|
|
1317
|
+
"tuna",
|
|
1318
|
+
"you",
|
|
1319
|
+
"wood"
|
|
1320
|
+
].indexOf(word.toLowerCase()) >= 0) return word;
|
|
1321
|
+
for (const w in irregular) {
|
|
1322
|
+
const pattern = new RegExp(`${irregular[w]}$`, "i");
|
|
1323
|
+
if (pattern.test(word)) return word.replace(pattern, w);
|
|
1324
|
+
}
|
|
1325
|
+
for (const reg in singulars) {
|
|
1326
|
+
const pattern = new RegExp(reg, "i");
|
|
1327
|
+
if (pattern.test(word)) return word.replace(pattern, singulars[reg]);
|
|
1328
|
+
}
|
|
1329
|
+
return word;
|
|
1330
|
+
}
|
|
1331
|
+
//#endregion
|
|
1165
1332
|
//#region ../utils/src/names.ts
|
|
1166
1333
|
/**
|
|
1167
1334
|
* Generates a foreign key column name from a given string, typically a collection slug or name.
|
|
1168
|
-
* It
|
|
1169
|
-
*
|
|
1335
|
+
* It singularizes the name, converts it to snake_case and appends '_id'.
|
|
1336
|
+
*
|
|
1337
|
+
* Singularization runs *before* snake-casing so that acronyms survive: `toSnakeCase`
|
|
1338
|
+
* splits on every capital, which turned "URLs" into "ur_ls" and then "ur_l_id".
|
|
1170
1339
|
*
|
|
1171
1340
|
* @param name The base name to convert to a foreign key.
|
|
1172
1341
|
* @returns A foreign key name in the format 'singular_name_id'.
|
|
@@ -1176,8 +1345,8 @@ function getPolicyNamesForRule(rule, tableName) {
|
|
|
1176
1345
|
* generateForeignKeyName("users")
|
|
1177
1346
|
*
|
|
1178
1347
|
* @example
|
|
1179
|
-
* // returns "
|
|
1180
|
-
* generateForeignKeyName("
|
|
1348
|
+
* // returns "category_id"
|
|
1349
|
+
* generateForeignKeyName("categories")
|
|
1181
1350
|
*
|
|
1182
1351
|
* @example
|
|
1183
1352
|
* // returns "product_id"
|
|
@@ -1185,8 +1354,63 @@ function getPolicyNamesForRule(rule, tableName) {
|
|
|
1185
1354
|
*
|
|
1186
1355
|
*/
|
|
1187
1356
|
function generateForeignKeyName(name) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1357
|
+
return `${toSnakeCase(singularizeForKey(name))}_id`;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* `singular()` handles real English plurals, but its final catch-all rule strips
|
|
1361
|
+
* any trailing "s", which mangles words that only look plural. Guard the two
|
|
1362
|
+
* cases that produce a column name nobody would recognise:
|
|
1363
|
+
*
|
|
1364
|
+
* - a double "s" ending is never a plural marker ("address", "class", "process"),
|
|
1365
|
+
* so stripping it yields "addres";
|
|
1366
|
+
* - a name that singularizes to nothing (the literal "s") would yield "_id".
|
|
1367
|
+
*/
|
|
1368
|
+
function singularizeForKey(name) {
|
|
1369
|
+
if (/ss$/i.test(name)) return name;
|
|
1370
|
+
const result = singular(name);
|
|
1371
|
+
return result.length > 0 ? result : name;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* What `generateForeignKeyName` returned before it learned to singularize:
|
|
1375
|
+
* snake-case the name, then chop one trailing "s".
|
|
1376
|
+
*
|
|
1377
|
+
* This is here to be *detected*, never to be generated. A database provisioned
|
|
1378
|
+
* under the old rule carries `categorie_id`, `addresse_id`, `children_id` or
|
|
1379
|
+
* `ur_l_id` where the current rule expects `category_id`, `address_id`,
|
|
1380
|
+
* `child_id` and `url_id` — and the boot-time schema ensure is additive, so it
|
|
1381
|
+
* would create the new column empty beside the populated old one and leave the
|
|
1382
|
+
* relation reading nothing. No error, no missing table: the failure is silent,
|
|
1383
|
+
* which is the only reason this function still exists.
|
|
1384
|
+
*
|
|
1385
|
+
* `ensureCollectionSchema` calls it to recognise that shape and say so.
|
|
1386
|
+
* Returns the same string as `generateForeignKeyName` for every regular plural,
|
|
1387
|
+
* so a caller can compare the two and act only when they differ.
|
|
1388
|
+
*/
|
|
1389
|
+
function legacyForeignKeyName(name) {
|
|
1390
|
+
const snake = toSnakeCase(name);
|
|
1391
|
+
return `${snake.endsWith("s") ? snake.slice(0, -1) : snake}_id`;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Truncate an identifier to what Postgres will actually store.
|
|
1395
|
+
*
|
|
1396
|
+
* Postgres silently truncates identifiers at NAMEDATALEN-1 = 63 **bytes**, so a
|
|
1397
|
+
* name generated longer than that is not the name the database ends up holding.
|
|
1398
|
+
* Anything that later looks the object up by the name it generated then misses.
|
|
1399
|
+
*
|
|
1400
|
+
* Byte length, not string length: NAMEDATALEN is a byte bound, and a multi-byte
|
|
1401
|
+
* character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.
|
|
1402
|
+
*
|
|
1403
|
+
* `TextEncoder` rather than `Buffer`, which is not a matter of taste: `Buffer`
|
|
1404
|
+
* is a Node global, and this package is imported by browser-facing ones. It
|
|
1405
|
+
* typechecked only where `@types/node` happened to be in scope, so
|
|
1406
|
+
* `packages/codegen` — whose tsconfig is `lib: ["ESNext", "dom"]` — could not
|
|
1407
|
+
* compile the file at all, and both of its suites failed to run. `TextEncoder`
|
|
1408
|
+
* and `TextDecoder` are standard in both runtimes and need no ambient types.
|
|
1409
|
+
*/
|
|
1410
|
+
function toPostgresIdentifier(name) {
|
|
1411
|
+
const bytes = new TextEncoder().encode(name);
|
|
1412
|
+
if (bytes.byteLength <= 63) return name;
|
|
1413
|
+
return new TextDecoder("utf-8").decode(bytes.subarray(0, 63)).replace(/�+$/, "");
|
|
1190
1414
|
}
|
|
1191
1415
|
//#endregion
|
|
1192
1416
|
//#region ../common/src/util/entities.ts
|
|
@@ -1211,10 +1435,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
|
|
|
1211
1435
|
* have `id` and `path` fields — these are relation-shaped objects from
|
|
1212
1436
|
* edge cases in the data pipeline (REST fallback, stale cache, custom data source).
|
|
1213
1437
|
*
|
|
1438
|
+
* When `targetPath` is given, also accepts a bare id. A relation column is a
|
|
1439
|
+
* foreign key, and the REST layer returns it as the scalar it is; only some
|
|
1440
|
+
* fetch paths hydrate it into an object. Which form a caller sees therefore
|
|
1441
|
+
* depends on how the row was loaded, and a caller that only accepted objects
|
|
1442
|
+
* reported half of its own data as a type error. The declared target is the
|
|
1443
|
+
* missing half: with it, an id is a relation that has not been fetched yet.
|
|
1444
|
+
*
|
|
1214
1445
|
* Returns null if the value cannot be coerced.
|
|
1215
1446
|
*/
|
|
1216
|
-
function normalizeToEntityRelation(value, propertyType) {
|
|
1447
|
+
function normalizeToEntityRelation(value, propertyType, targetPath) {
|
|
1217
1448
|
if (value instanceof EntityRelation) return value;
|
|
1449
|
+
if (targetPath && (typeof value === "string" || typeof value === "number")) {
|
|
1450
|
+
if (value === "") return null;
|
|
1451
|
+
return new EntityRelation(value, targetPath);
|
|
1452
|
+
}
|
|
1218
1453
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1219
1454
|
const obj = value;
|
|
1220
1455
|
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
@@ -1286,6 +1521,53 @@ function createRelationRefWithData(id, path, data) {
|
|
|
1286
1521
|
data
|
|
1287
1522
|
};
|
|
1288
1523
|
}
|
|
1524
|
+
//#endregion
|
|
1525
|
+
//#region ../common/src/util/collections.ts
|
|
1526
|
+
/**
|
|
1527
|
+
* A copy of `collections` ordered by slug.
|
|
1528
|
+
*
|
|
1529
|
+
* Every generator that turns collections into a file is order-dependent, and
|
|
1530
|
+
* every one of them is compared against its own output — `rebase doctor`
|
|
1531
|
+
* regenerates in memory and diffs, `generate-sdk && git diff --exit-code` gates
|
|
1532
|
+
* CI. While only the *writers* sorted, a project whose `readdirSync` order
|
|
1533
|
+
* differed from its slug order was reported permanently out of date, and the
|
|
1534
|
+
* fix the message printed rewrote the file in the order it was already in. The
|
|
1535
|
+
* generators sort themselves now, so no caller can get this wrong.
|
|
1536
|
+
*
|
|
1537
|
+
* A slug-less collection is left to the generator's own validation, which names
|
|
1538
|
+
* the offending collection; sorting must not throw first.
|
|
1539
|
+
*/
|
|
1540
|
+
function sortCollectionsBySlug(collections) {
|
|
1541
|
+
return [...collections].sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""));
|
|
1542
|
+
}
|
|
1543
|
+
/** The eight-four-four-four-twelve shape of a UUID, any version. */
|
|
1544
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1545
|
+
/** Whether one address part can be a value of the column it addresses. */
|
|
1546
|
+
function partIsAddressable(part, pk) {
|
|
1547
|
+
if (pk.isUUID) return UUID_PATTERN.test(String(part));
|
|
1548
|
+
if (pk.type === "number") return typeof part === "number" ? Number.isFinite(part) : !isNaN(parseInt(String(part), 10));
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Whether an address could name a row at all, before asking the database.
|
|
1553
|
+
*
|
|
1554
|
+
* A `uuid` column cannot hold `"new"`, and an `integer` column cannot hold
|
|
1555
|
+
* `"abc"` — so the answer to "which row is this" is "none", and that is a 404,
|
|
1556
|
+
* not a failure. Postgres cannot say so politely: the comparison never runs, it
|
|
1557
|
+
* raises `22P02` and aborts the enclosing transaction, after which every
|
|
1558
|
+
* further statement returns the far less helpful `25P02`.
|
|
1559
|
+
*
|
|
1560
|
+
* `isUUID` must come from the column, not from `isId: "uuid"` in a config: the
|
|
1561
|
+
* config is a claim about a key, and a `text` column that holds ids of some
|
|
1562
|
+
* other shape is a working app this must not start rejecting.
|
|
1563
|
+
*/
|
|
1564
|
+
function isAddressableId(idValue, primaryKeys) {
|
|
1565
|
+
if (primaryKeys.length === 0) return false;
|
|
1566
|
+
if (primaryKeys.length === 1) return partIsAddressable(idValue, primaryKeys[0]);
|
|
1567
|
+
const parts = String(idValue).split(":::");
|
|
1568
|
+
if (parts.length !== primaryKeys.length) return false;
|
|
1569
|
+
return parts.every((part, i) => partIsAddressable(part, primaryKeys[i]));
|
|
1570
|
+
}
|
|
1289
1571
|
/**
|
|
1290
1572
|
* Derive a row's address from its key columns.
|
|
1291
1573
|
*
|
|
@@ -1426,8 +1708,7 @@ function enumToObjectEntries(enumValues) {
|
|
|
1426
1708
|
function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
1427
1709
|
const target = relation.target;
|
|
1428
1710
|
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.`);
|
|
1711
|
+
const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);
|
|
1431
1712
|
const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);
|
|
1432
1713
|
const shared = {
|
|
1433
1714
|
relationName,
|
|
@@ -1454,7 +1735,8 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
|
1454
1735
|
cardinality: "one",
|
|
1455
1736
|
writable: true,
|
|
1456
1737
|
shared: false,
|
|
1457
|
-
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
|
|
1738
|
+
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
|
|
1739
|
+
sourceKey: relation.sourceKey
|
|
1458
1740
|
};
|
|
1459
1741
|
case "hasMany": return {
|
|
1460
1742
|
...shared,
|
|
@@ -1462,7 +1744,8 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
|
1462
1744
|
cardinality: "many",
|
|
1463
1745
|
writable: true,
|
|
1464
1746
|
shared: false,
|
|
1465
|
-
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
|
|
1747
|
+
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
|
|
1748
|
+
sourceKey: relation.sourceKey
|
|
1466
1749
|
};
|
|
1467
1750
|
case "manyToMany": {
|
|
1468
1751
|
const sourceTable = getTableName(sourceCollection);
|
|
@@ -1491,6 +1774,43 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
|
1491
1774
|
default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
|
|
1492
1775
|
}
|
|
1493
1776
|
}
|
|
1777
|
+
/** How this relation is addressed in an error message, before it has a resolved name. */
|
|
1778
|
+
function describe(relation, sourceCollection, propertyKey) {
|
|
1779
|
+
const name = relation.relationName ?? propertyKey;
|
|
1780
|
+
return `Relation${name ? ` '${name}'` : ""} on '${sourceCollection.slug}'`;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Call the `target` thunk, and translate the two ways an import cycle breaks it
|
|
1784
|
+
* into an error that names the cause.
|
|
1785
|
+
*
|
|
1786
|
+
* The thunk exists to defer the reference until every module has finished
|
|
1787
|
+
* evaluating, and for a cycle that closes at import time it does. What it cannot
|
|
1788
|
+
* defer is a cycle that leaves the binding permanently unusable, and there are
|
|
1789
|
+
* two shapes of that:
|
|
1790
|
+
*
|
|
1791
|
+
* - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are
|
|
1792
|
+
* in the temporal dead zone, so reading one throws `ReferenceError: x is not
|
|
1793
|
+
* defined`. The stack points at the thunk — a one-line arrow function that is
|
|
1794
|
+
* obviously fine — and says nothing about the cycle that made it throw.
|
|
1795
|
+
* - **CJS interop.** The half-initialised module object has no `default` yet,
|
|
1796
|
+
* the import resolves to `undefined`, and the thunk returns it without
|
|
1797
|
+
* complaint. That one used to surface here as "did not resolve to a
|
|
1798
|
+
* collection", which is true and unhelpful.
|
|
1799
|
+
*
|
|
1800
|
+
* Both mean the same thing, and the fix for both is the same: break the cycle,
|
|
1801
|
+
* or move the relation into the collection that does not close it.
|
|
1802
|
+
*/
|
|
1803
|
+
function callTarget(relation, sourceCollection, propertyKey, target) {
|
|
1804
|
+
let targetCollection;
|
|
1805
|
+
try {
|
|
1806
|
+
targetCollection = target();
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
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 });
|
|
1809
|
+
throw error;
|
|
1810
|
+
}
|
|
1811
|
+
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`."));
|
|
1812
|
+
return targetCollection;
|
|
1813
|
+
}
|
|
1494
1814
|
//#endregion
|
|
1495
1815
|
//#region ../common/src/util/relations.ts
|
|
1496
1816
|
/**
|
|
@@ -1686,9 +2006,9 @@ function isKeywordAt(upper, i, keyword) {
|
|
|
1686
2006
|
*
|
|
1687
2007
|
* This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
|
|
1688
2008
|
* `AND` inside
|
|
1689
|
-
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id =
|
|
2009
|
+
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
|
|
1690
2010
|
* split the expression, and re-emitting the halves produced
|
|
1691
|
-
* `(EXISTS (...) AND m.user_id =
|
|
2011
|
+
* `(EXISTS (...) AND m.user_id = rebase.uid())`
|
|
1692
2012
|
* where `m` is no longer in scope — SQL that Postgres rejects outright with
|
|
1693
2013
|
* "missing FROM-clause entry for table". Returning null instead keeps such a
|
|
1694
2014
|
* clause as a `raw` expression, which round-trips verbatim.
|
|
@@ -1762,15 +2082,15 @@ function stripOuterParens(sql) {
|
|
|
1762
2082
|
}
|
|
1763
2083
|
}
|
|
1764
2084
|
function sqlToPolicy(sql) {
|
|
1765
|
-
const trimmed = stripOuterParens(sql.trim());
|
|
2085
|
+
const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
|
|
1766
2086
|
if (trimmed.toLowerCase() === "true") return policy.true();
|
|
1767
2087
|
if (trimmed.toLowerCase() === "false") return policy.false();
|
|
1768
|
-
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
2088
|
+
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
|
|
1769
2089
|
if (overlapMatch) {
|
|
1770
2090
|
const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
1771
2091
|
return policy.rolesOverlap(roles);
|
|
1772
2092
|
}
|
|
1773
|
-
const containMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
2093
|
+
const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
|
|
1774
2094
|
if (containMatch) {
|
|
1775
2095
|
const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
1776
2096
|
return policy.rolesContain(roles);
|
|
@@ -1786,10 +2106,10 @@ function sqlToPolicy(sql) {
|
|
|
1786
2106
|
const right = parseOperand(rightStr.trim());
|
|
1787
2107
|
if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
|
|
1788
2108
|
}
|
|
1789
|
-
return policy.raw(
|
|
2109
|
+
return policy.raw(trimmed);
|
|
1790
2110
|
}
|
|
1791
2111
|
/**
|
|
1792
|
-
* Literals from other BaaS platforms that people compare `
|
|
2112
|
+
* Literals from other BaaS platforms that people compare `rebase.uid()` against
|
|
1793
2113
|
* out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
|
|
1794
2114
|
* `pgRoles`, one surface over: the same muscle memory inside a `using:` string
|
|
1795
2115
|
* is the more dangerous spelling, because it inverts a rule instead of
|
|
@@ -1800,18 +2120,30 @@ var FOREIGN_CONVENTION_UIDS = {
|
|
|
1800
2120
|
authenticated: "Supabase",
|
|
1801
2121
|
service_role: "Supabase"
|
|
1802
2122
|
};
|
|
1803
|
-
/**
|
|
1804
|
-
|
|
2123
|
+
/**
|
|
2124
|
+
* `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
|
|
2125
|
+
*
|
|
2126
|
+
* Both schema spellings, because this runs over policy bodies read back from a
|
|
2127
|
+
* database, and one migrated by a pre-1.0 release still holds `auth.uid()`.
|
|
2128
|
+
* A security check that stops recognising a dangerous clause because the
|
|
2129
|
+
* framework renamed a function is a check that silently turns off.
|
|
2130
|
+
*/
|
|
2131
|
+
var UID_NOT_NULL = /\b(?:rebase|auth)\.uid\(\)\s+IS\s+NOT\s+NULL/i;
|
|
1805
2132
|
/**
|
|
1806
2133
|
* Find clauses that read as "signed-in users only" but admit anonymous callers.
|
|
1807
2134
|
*
|
|
1808
|
-
* Both spellings come from the same place — Supabase, where `auth.uid()`
|
|
1809
|
-
* is NULL for an anonymous request. Rebase substitutes
|
|
2135
|
+
* Both spellings come from the same place — Supabase, where its own `auth.uid()`
|
|
2136
|
+
* really is NULL for an anonymous request. Rebase substitutes
|
|
1810
2137
|
* {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
|
|
1811
2138
|
* is how the trusted *server* context is recognised), so:
|
|
1812
2139
|
*
|
|
1813
|
-
* - `
|
|
1814
|
-
* - `
|
|
2140
|
+
* - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
|
|
2141
|
+
* - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
|
|
2142
|
+
* other. This one is not hypothetical and was not only a foreign habit:
|
|
2143
|
+
* rebase's own request path reported `'anon'` while everything that compiled
|
|
2144
|
+
* or checked a policy used `'anonymous'`, so whichever literal an author
|
|
2145
|
+
* picked, half the anonymous callers walked through. See
|
|
2146
|
+
* {@link ANONYMOUS_USER_IDS}.
|
|
1815
2147
|
*
|
|
1816
2148
|
* Either one turns a lockdown into a full grant, and neither looks wrong. No
|
|
1817
2149
|
* real user id is ever one of these literals, and a user-context request is
|
|
@@ -1838,7 +2170,7 @@ function findAnonymousGrants(expr) {
|
|
|
1838
2170
|
if (UID_NOT_NULL.test(e.sql)) found.push({
|
|
1839
2171
|
pattern: "uid-not-null",
|
|
1840
2172
|
detail: e.sql,
|
|
1841
|
-
explanation: `\`
|
|
2173
|
+
explanation: `\`rebase.uid() IS NOT NULL\` is true for every request that came from a client, including anonymous ones — they carry '${ANONYMOUS_USER_ID}', not NULL. Use \`condition: policy.authenticated()\` to mean "signed in".`
|
|
1842
2174
|
});
|
|
1843
2175
|
return;
|
|
1844
2176
|
case "compare": {
|
|
@@ -1849,7 +2181,7 @@ function findAnonymousGrants(expr) {
|
|
|
1849
2181
|
found.push({
|
|
1850
2182
|
pattern: "foreign-uid-literal",
|
|
1851
2183
|
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".`
|
|
2184
|
+
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
2185
|
});
|
|
1854
2186
|
return;
|
|
1855
2187
|
}
|
|
@@ -1860,12 +2192,43 @@ function findAnonymousGrants(expr) {
|
|
|
1860
2192
|
return found;
|
|
1861
2193
|
}
|
|
1862
2194
|
function parseOperand(str) {
|
|
1863
|
-
if (
|
|
1864
|
-
const
|
|
1865
|
-
if (
|
|
1866
|
-
if (
|
|
2195
|
+
if (/^current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)$/i.test(str) || /^rebase\.uid\(\)$/i.test(str)) return policy.authUid();
|
|
2196
|
+
const literal = parseSingleQuoted(str);
|
|
2197
|
+
if (literal !== null) return policy.literal(literal);
|
|
2198
|
+
if (/^-?\d+$/.test(str)) return policy.literal(Number(str));
|
|
2199
|
+
if (/^-?\d*\.\d+$/.test(str)) return policy.literal(Number(str));
|
|
2200
|
+
if (/^true$/i.test(str)) return policy.literal(true);
|
|
2201
|
+
if (/^false$/i.test(str)) return policy.literal(false);
|
|
2202
|
+
if (/^null$/i.test(str)) return policy.literal(null);
|
|
2203
|
+
if (/^\w+$/.test(str) && toSnakeCase(str) !== "") return policy.field(str);
|
|
1867
2204
|
return null;
|
|
1868
2205
|
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Decode a single-quoted SQL literal, or null when `str` is not exactly one.
|
|
2208
|
+
*
|
|
2209
|
+
* Rejecting is as important as decoding: `'a' = 'b'` is two literals and an
|
|
2210
|
+
* operator, not one literal whose body contains a quote, and a regex anchored
|
|
2211
|
+
* on the outer quotes would happily read it as the latter. Every interior quote
|
|
2212
|
+
* must therefore be part of a `''` pair.
|
|
2213
|
+
*/
|
|
2214
|
+
function parseSingleQuoted(str) {
|
|
2215
|
+
if (str.length < 2 || !str.startsWith("'") || !str.endsWith("'")) return null;
|
|
2216
|
+
const body = str.slice(1, -1);
|
|
2217
|
+
let out = "";
|
|
2218
|
+
for (let i = 0; i < body.length; i++) {
|
|
2219
|
+
if (body[i] !== "'") {
|
|
2220
|
+
out += body[i];
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
if (body[i + 1] === "'") {
|
|
2224
|
+
out += "'";
|
|
2225
|
+
i++;
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
return out;
|
|
2231
|
+
}
|
|
1869
2232
|
//#endregion
|
|
1870
2233
|
//#region ../common/src/util/policy/securityRuleToConditions.ts
|
|
1871
2234
|
/**
|
|
@@ -1942,12 +2305,12 @@ function compile(expr, scope) {
|
|
|
1942
2305
|
const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
|
|
1943
2306
|
return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
|
|
1944
2307
|
}
|
|
1945
|
-
case "rolesOverlap": return `string_to_array(
|
|
1946
|
-
case "rolesContain": return `string_to_array(
|
|
1947
|
-
case "authenticated": return
|
|
1948
|
-
case "serverContext": return
|
|
2308
|
+
case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
|
|
2309
|
+
case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
|
|
2310
|
+
case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
|
|
2311
|
+
case "serverContext": return `${RLS_UID_SQL} IS NULL`;
|
|
1949
2312
|
case "existsIn": return compileExistsIn(expr, scope);
|
|
1950
|
-
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
2313
|
+
case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
1951
2314
|
}
|
|
1952
2315
|
}
|
|
1953
2316
|
/**
|
|
@@ -1984,8 +2347,8 @@ function operandToSql(operand, scope) {
|
|
|
1984
2347
|
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
1985
2348
|
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
1986
2349
|
case "literal": return quoteLiteral(operand.value);
|
|
1987
|
-
case "authUid": return
|
|
1988
|
-
case "authRoles": return
|
|
2350
|
+
case "authUid": return RLS_UID_SQL;
|
|
2351
|
+
case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
|
|
1989
2352
|
}
|
|
1990
2353
|
}
|
|
1991
2354
|
/**
|
|
@@ -2016,75 +2379,6 @@ function rolesArraySql(roles) {
|
|
|
2016
2379
|
return `ARRAY[${[...roles].sort().map((r) => `'${r}'`).join(",")}]`;
|
|
2017
2380
|
}
|
|
2018
2381
|
//#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
2382
|
//#region ../common/src/util/auth-default-policies.ts
|
|
2089
2383
|
/**
|
|
2090
2384
|
* Default RLS policies injected by the schema generator.
|
|
@@ -2092,8 +2386,14 @@ var buildPropertyCallbacks = (properties) => {
|
|
|
2092
2386
|
* Rebase's enforcement model is unified: authenticated (user-context) requests
|
|
2093
2387
|
* run under the restricted `rebase_user` role, so Postgres RLS binds *every*
|
|
2094
2388
|
* statement — reads and writes. A collection's `securityRules` are the whole
|
|
2095
|
-
* authorization model. The server context (auth flows, migrations,
|
|
2096
|
-
* `
|
|
2389
|
+
* authorization model. The server context (auth flows, migrations, raw
|
|
2390
|
+
* `rebase.sql`) runs as the owner and bypasses RLS.
|
|
2391
|
+
*
|
|
2392
|
+
* `rebase.dataAsAdmin` is **not** in that set, despite the name: it is scoped as
|
|
2393
|
+
* `{ uid: "service", roles: ["admin"] }`, so it runs as `rebase_user` like any
|
|
2394
|
+
* other caller and clears the baseline below through the *admin* arm, not the
|
|
2395
|
+
* server arm. Which is why `disableDefaultPolicies` plus a lone
|
|
2396
|
+
* `policy.serverContext()` rule locks it out too.
|
|
2097
2397
|
*
|
|
2098
2398
|
* Because RLS default-denies, every collection is **locked by default**: with
|
|
2099
2399
|
* no rules, only the server context and admins can touch it. The generator
|
|
@@ -2134,7 +2434,7 @@ var DEFAULT_GUARDED_OPS = [
|
|
|
2134
2434
|
"delete"
|
|
2135
2435
|
];
|
|
2136
2436
|
/** Whether a collection is flagged as an authentication collection. */
|
|
2137
|
-
function isAuthCollection(collection) {
|
|
2437
|
+
function isAuthCollection$1(collection) {
|
|
2138
2438
|
const auth = collection.auth;
|
|
2139
2439
|
return auth === true || typeof auth === "object" && auth?.enabled === true;
|
|
2140
2440
|
}
|
|
@@ -2167,7 +2467,7 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2167
2467
|
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2168
2468
|
check: SERVER_OR_ADMIN_EXPR$1
|
|
2169
2469
|
});
|
|
2170
|
-
if (isAuthCollection(collection)) {
|
|
2470
|
+
if (isAuthCollection$1(collection)) {
|
|
2171
2471
|
injected.push({
|
|
2172
2472
|
name: `${tableName}_default_self_read`,
|
|
2173
2473
|
operations: ["select"],
|
|
@@ -2659,10 +2959,27 @@ function getJunctionSecurityRules(spec) {
|
|
|
2659
2959
|
});
|
|
2660
2960
|
})))();
|
|
2661
2961
|
/**
|
|
2662
|
-
*
|
|
2962
|
+
* How wide a `varchar`/`char` column should be for a given property.
|
|
2963
|
+
*
|
|
2964
|
+
* One definition, three call sites, because they used to disagree. For the same
|
|
2965
|
+
* `columnType: "varchar"` property the DDL generator emitted `VARCHAR(255)`
|
|
2966
|
+
* while the Drizzle generator emitted a bare `varchar("col")` — which Postgres
|
|
2967
|
+
* reads as *unbounded* — so which of the two you ran decided whether the column
|
|
2968
|
+
* had a limit at all. Introspection then dropped the length entirely, so reading
|
|
2969
|
+
* an existing `character varying(500)` column back and regenerating it produced
|
|
2970
|
+
* a `VARCHAR(255)`: a silent narrowing of a column with data already in it.
|
|
2971
|
+
*
|
|
2972
|
+
* `validation.max` is the property's own statement about how long the value may
|
|
2973
|
+
* be, so it is the only sensible source for the column's width — and it keeps
|
|
2974
|
+
* the constraint the database enforces in step with the one the app enforces,
|
|
2975
|
+
* rather than inventing a second, different limit underneath it.
|
|
2663
2976
|
*/
|
|
2977
|
+
function resolveStringColumnLength(prop) {
|
|
2978
|
+
const max = prop.validation?.max;
|
|
2979
|
+
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : 255;
|
|
2980
|
+
}
|
|
2664
2981
|
//#endregion
|
|
2665
|
-
//#region ../../node_modules/.pnpm/fast-equals@6.0.
|
|
2982
|
+
//#region ../../node_modules/.pnpm/fast-equals@6.0.2/node_modules/fast-equals/dist/es/index.mjs
|
|
2666
2983
|
var { getOwnPropertyNames, getOwnPropertySymbols } = Object;
|
|
2667
2984
|
var { hasOwnProperty } = Object.prototype;
|
|
2668
2985
|
/**
|
|
@@ -2698,7 +3015,8 @@ function createIsCircular(areItemsEqual) {
|
|
|
2698
3015
|
* not enumerable and symbol properties.
|
|
2699
3016
|
*/
|
|
2700
3017
|
function getStrictProperties(object) {
|
|
2701
|
-
|
|
3018
|
+
const symbols = getOwnPropertySymbols(object);
|
|
3019
|
+
return symbols.length ? getOwnPropertyNames(object).concat(symbols) : getOwnPropertyNames(object);
|
|
2702
3020
|
}
|
|
2703
3021
|
/**
|
|
2704
3022
|
* Whether the object contains the property passed as an own property.
|
|
@@ -2771,7 +3089,7 @@ function areMapsEqual(a, b, state) {
|
|
|
2771
3089
|
const size = a.size;
|
|
2772
3090
|
if (size !== b.size) return false;
|
|
2773
3091
|
if (!size) return true;
|
|
2774
|
-
const matchedIndices = new
|
|
3092
|
+
const matchedIndices = new Uint8Array(size);
|
|
2775
3093
|
const aIterable = a.entries();
|
|
2776
3094
|
let aResult;
|
|
2777
3095
|
let bResult;
|
|
@@ -2779,7 +3097,7 @@ function areMapsEqual(a, b, state) {
|
|
|
2779
3097
|
while (aResult = aIterable.next()) {
|
|
2780
3098
|
if (aResult.done) break;
|
|
2781
3099
|
const bIterable = b.entries();
|
|
2782
|
-
let hasMatch =
|
|
3100
|
+
let hasMatch = 0;
|
|
2783
3101
|
let matchIndex = 0;
|
|
2784
3102
|
while (bResult = bIterable.next()) {
|
|
2785
3103
|
if (bResult.done) break;
|
|
@@ -2790,7 +3108,7 @@ function areMapsEqual(a, b, state) {
|
|
|
2790
3108
|
const aEntry = aResult.value;
|
|
2791
3109
|
const bEntry = bResult.value;
|
|
2792
3110
|
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] =
|
|
3111
|
+
hasMatch = matchedIndices[matchIndex] = 1;
|
|
2794
3112
|
break;
|
|
2795
3113
|
}
|
|
2796
3114
|
matchIndex++;
|
|
@@ -2848,19 +3166,19 @@ function areSetsEqual(a, b, state) {
|
|
|
2848
3166
|
const size = a.size;
|
|
2849
3167
|
if (size !== b.size) return false;
|
|
2850
3168
|
if (!size) return true;
|
|
2851
|
-
const matchedIndices = new
|
|
3169
|
+
const matchedIndices = new Uint8Array(size);
|
|
2852
3170
|
const aIterable = a.values();
|
|
2853
3171
|
let aResult;
|
|
2854
3172
|
let bResult;
|
|
2855
3173
|
while (aResult = aIterable.next()) {
|
|
2856
3174
|
if (aResult.done) break;
|
|
2857
3175
|
const bIterable = b.values();
|
|
2858
|
-
let hasMatch =
|
|
3176
|
+
let hasMatch = 0;
|
|
2859
3177
|
let matchIndex = 0;
|
|
2860
3178
|
while (bResult = bIterable.next()) {
|
|
2861
3179
|
if (bResult.done) break;
|
|
2862
3180
|
if (!matchedIndices[matchIndex] && state.equals(aResult.value, bResult.value, aResult.value, bResult.value, a, b, state)) {
|
|
2863
|
-
hasMatch = matchedIndices[matchIndex] =
|
|
3181
|
+
hasMatch = matchedIndices[matchIndex] = 1;
|
|
2864
3182
|
break;
|
|
2865
3183
|
}
|
|
2866
3184
|
matchIndex++;
|
|
@@ -2873,8 +3191,8 @@ function areSetsEqual(a, b, state) {
|
|
|
2873
3191
|
* Whether the TypedArray instances are equal in value.
|
|
2874
3192
|
*/
|
|
2875
3193
|
function areTypedArraysEqual(a, b) {
|
|
2876
|
-
let index = a.
|
|
2877
|
-
if (b.
|
|
3194
|
+
let index = a.length;
|
|
3195
|
+
if (b.length !== index || a.byteOffset !== b.byteOffset) return false;
|
|
2878
3196
|
while (index-- > 0) if (a[index] !== b[index]) return false;
|
|
2879
3197
|
return true;
|
|
2880
3198
|
}
|
|
@@ -3116,6 +3434,46 @@ function resolveDataSource(collection, registry) {
|
|
|
3116
3434
|
capabilities: getDataSourceCapabilities(engine)
|
|
3117
3435
|
};
|
|
3118
3436
|
}
|
|
3437
|
+
/**
|
|
3438
|
+
* Does a SQL toolchain own this collection's storage?
|
|
3439
|
+
*
|
|
3440
|
+
* "Owns the storage" means: something generates a table for it, pushes that
|
|
3441
|
+
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
3442
|
+
* the two disagree. That is true of a Postgres collection and false of a
|
|
3443
|
+
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
3444
|
+
* migrates — and the two were never told apart. Every stage of the SQL
|
|
3445
|
+
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
3446
|
+
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
3447
|
+
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
3448
|
+
* `db push` include list — where its name shielding a same-named real table
|
|
3449
|
+
* from Atlas's exclude list is the one that can lose data.
|
|
3450
|
+
*
|
|
3451
|
+
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
3452
|
+
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
3453
|
+
* gets the same treatment as the built-in ones.
|
|
3454
|
+
*
|
|
3455
|
+
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
3456
|
+
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
3457
|
+
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
3458
|
+
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
3459
|
+
* including a collection generates a table nothing writes to; wrongly excluding
|
|
3460
|
+
* one silently stops generating a table the app is serving from. Declare
|
|
3461
|
+
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
3462
|
+
*/
|
|
3463
|
+
function isRelationalCollection(collection, registry) {
|
|
3464
|
+
return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* The subset of `collections` a SQL toolchain owns — see
|
|
3468
|
+
* {@link isRelationalCollection}.
|
|
3469
|
+
*
|
|
3470
|
+
* Every stage that generates SQL from collections starts by calling this, so
|
|
3471
|
+
* the rule lives in one place rather than being re-decided per generator. It
|
|
3472
|
+
* keeps the input order.
|
|
3473
|
+
*/
|
|
3474
|
+
function relationalCollections(collections, registry) {
|
|
3475
|
+
return collections.filter((collection) => isRelationalCollection(collection, registry));
|
|
3476
|
+
}
|
|
3119
3477
|
//#endregion
|
|
3120
3478
|
//#region ../common/src/collections/CollectionRegistry.ts
|
|
3121
3479
|
var CollectionRegistry = class {
|
|
@@ -3410,8 +3768,24 @@ var QueryBuilder = class {
|
|
|
3410
3768
|
/**
|
|
3411
3769
|
* Set a free-text search string if supported by the backend.
|
|
3412
3770
|
*/
|
|
3413
|
-
search(searchString) {
|
|
3771
|
+
search(searchString, options) {
|
|
3414
3772
|
this.params.searchString = searchString;
|
|
3773
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
3774
|
+
return this;
|
|
3775
|
+
}
|
|
3776
|
+
/**
|
|
3777
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
3778
|
+
*
|
|
3779
|
+
* Postgres only, over a property declared as `type: "vector"`. Rows come
|
|
3780
|
+
* back with a `_distance`; `where` filters before the ordering.
|
|
3781
|
+
*/
|
|
3782
|
+
vectorSearch(property, vector, options) {
|
|
3783
|
+
this.params.vectorSearch = {
|
|
3784
|
+
property,
|
|
3785
|
+
vector,
|
|
3786
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
3787
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
3788
|
+
};
|
|
3415
3789
|
return this;
|
|
3416
3790
|
}
|
|
3417
3791
|
/**
|
|
@@ -3468,6 +3842,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
|
|
|
3468
3842
|
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
3469
3843
|
}
|
|
3470
3844
|
};
|
|
3845
|
+
/**
|
|
3846
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
3847
|
+
*
|
|
3848
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
3849
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
3850
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
3851
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
3852
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
3853
|
+
* rows are the mildest of those outcomes.
|
|
3854
|
+
*
|
|
3855
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
3856
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
3857
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
3858
|
+
* look like it is paging by offset.
|
|
3859
|
+
*/
|
|
3860
|
+
function resolveFindWindow(params) {
|
|
3861
|
+
const limit = params?.limit ?? 50;
|
|
3862
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
3863
|
+
return {
|
|
3864
|
+
limit,
|
|
3865
|
+
offset,
|
|
3866
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3471
3869
|
function normalizePageSize(raw) {
|
|
3472
3870
|
if (raw === void 0 || !Number.isFinite(raw)) return 200;
|
|
3473
3871
|
return Math.max(1, Math.floor(raw));
|
|
@@ -3587,21 +3985,39 @@ async function collectAllPages(find, params, label = "collection") {
|
|
|
3587
3985
|
* metadata, so type coercion is the responsibility of the server-side data
|
|
3588
3986
|
* driver which has access to the collection schema.
|
|
3589
3987
|
*
|
|
3590
|
-
*
|
|
3591
|
-
*
|
|
3988
|
+
* Structural characters inside a value are backslash-escaped: `,` → `\,`,
|
|
3989
|
+
* `(` → `\(`, `)` → `\)`, and a literal backslash as `\\`. Decoding is
|
|
3990
|
+
* deliberately conservative — only those four sequences are decoded, so a
|
|
3991
|
+
* backslash that arrives unescaped from an older client survives intact.
|
|
3592
3992
|
*
|
|
3593
3993
|
* @module
|
|
3594
3994
|
*/
|
|
3595
3995
|
/**
|
|
3596
|
-
* Unescape a
|
|
3597
|
-
*
|
|
3996
|
+
* Unescape a wire-format value.
|
|
3997
|
+
*
|
|
3998
|
+
* **Conservative**, and deliberately so: only the four sequences
|
|
3999
|
+
* {@link escapeWireValue} actually produces are decoded. A backslash followed
|
|
4000
|
+
* by anything else is left exactly as it is.
|
|
4001
|
+
*
|
|
4002
|
+
* This used to consume the backslash before *any* character, which is
|
|
4003
|
+
* indistinguishable for anything this codec emitted — it only ever emits those
|
|
4004
|
+
* four — but not for input arriving from elsewhere. A client on an older
|
|
4005
|
+
* release sends a Windows path or a LIKE pattern with a literal `C:\x`
|
|
4006
|
+
* unescaped, and greedy unescaping silently turned it into `C:x`, changing
|
|
4007
|
+
* which rows matched. Decoding only what the encoder can produce makes the two
|
|
4008
|
+
* directions agree across versions.
|
|
3598
4009
|
*/
|
|
3599
|
-
function
|
|
4010
|
+
function unescapeWireValue(value) {
|
|
3600
4011
|
let result = "";
|
|
3601
|
-
for (let i = 0; i < value.length; i++)
|
|
3602
|
-
|
|
3603
|
-
i
|
|
3604
|
-
|
|
4012
|
+
for (let i = 0; i < value.length; i++) {
|
|
4013
|
+
const next = value[i + 1];
|
|
4014
|
+
if (value[i] === "\\" && (next === "\\" || next === "," || next === "(" || next === ")")) {
|
|
4015
|
+
result += next;
|
|
4016
|
+
i++;
|
|
4017
|
+
continue;
|
|
4018
|
+
}
|
|
4019
|
+
result += value[i];
|
|
4020
|
+
}
|
|
3605
4021
|
return result;
|
|
3606
4022
|
}
|
|
3607
4023
|
/**
|
|
@@ -3619,10 +4035,10 @@ function splitListItems(inner) {
|
|
|
3619
4035
|
current += inner[i] + inner[i + 1];
|
|
3620
4036
|
i++;
|
|
3621
4037
|
} else if (inner[i] === ",") {
|
|
3622
|
-
items.push(
|
|
4038
|
+
items.push(unescapeWireValue(current));
|
|
3623
4039
|
current = "";
|
|
3624
4040
|
} else current += inner[i];
|
|
3625
|
-
items.push(
|
|
4041
|
+
items.push(unescapeWireValue(current));
|
|
3626
4042
|
return items;
|
|
3627
4043
|
}
|
|
3628
4044
|
var REST_OP_LOOKUP = REST_TO_CANONICAL;
|
|
@@ -3717,14 +4133,16 @@ function createPrimaryKeyResolver(options) {
|
|
|
3717
4133
|
* than postgres still serve rows with one, and this keeps them working.
|
|
3718
4134
|
*/
|
|
3719
4135
|
function rowToEntity(row, slug, primaryKeys = []) {
|
|
4136
|
+
const { _matches, ...values } = row;
|
|
3720
4137
|
return {
|
|
3721
4138
|
id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
|
|
3722
4139
|
path: slug,
|
|
3723
|
-
values
|
|
4140
|
+
values,
|
|
4141
|
+
..._matches ? { searchMatches: _matches } : {}
|
|
3724
4142
|
};
|
|
3725
4143
|
}
|
|
3726
4144
|
/**
|
|
3727
|
-
* The relation envelope `
|
|
4145
|
+
* The relation envelope `toFlatRow` writes where a relation was:
|
|
3728
4146
|
* `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
|
|
3729
4147
|
* admin's view-model, and the only pipeline that produces one is postgres'.
|
|
3730
4148
|
*/
|
|
@@ -3763,21 +4181,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3763
4181
|
const accessor = {
|
|
3764
4182
|
async find(params) {
|
|
3765
4183
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3766
|
-
const limit = params
|
|
3767
|
-
const offset = params?.offset ?? 0;
|
|
4184
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3768
4185
|
const fetchService = driver.restFetchService;
|
|
3769
4186
|
const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
|
|
3770
4187
|
filter,
|
|
3771
|
-
|
|
3772
|
-
|
|
4188
|
+
logical: params?.logical,
|
|
4189
|
+
limit,
|
|
4190
|
+
offset: driverOffset,
|
|
3773
4191
|
orderBy: params?.orderBy?.[0],
|
|
3774
4192
|
order: params?.orderBy?.[1],
|
|
3775
4193
|
searchString: params?.searchString
|
|
3776
4194
|
}, params?.include) : await driver.fetchCollection({
|
|
3777
4195
|
path: slug,
|
|
3778
|
-
limit
|
|
3779
|
-
offset:
|
|
4196
|
+
limit,
|
|
4197
|
+
offset: driverOffset,
|
|
3780
4198
|
filter,
|
|
4199
|
+
logical: params?.logical,
|
|
3781
4200
|
orderBy: params?.orderBy?.[0],
|
|
3782
4201
|
order: params?.orderBy?.[1],
|
|
3783
4202
|
searchString: params?.searchString
|
|
@@ -3787,7 +4206,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3787
4206
|
if (driver.count) {
|
|
3788
4207
|
total = await driver.count({
|
|
3789
4208
|
path: slug,
|
|
3790
|
-
filter
|
|
4209
|
+
filter,
|
|
4210
|
+
logical: params?.logical,
|
|
4211
|
+
searchString: params?.searchString
|
|
3791
4212
|
});
|
|
3792
4213
|
hasMore = offset + rows.length < total;
|
|
3793
4214
|
}
|
|
@@ -3839,30 +4260,48 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3839
4260
|
values: {}
|
|
3840
4261
|
} });
|
|
3841
4262
|
},
|
|
4263
|
+
updateMany: driver.updateMany ? async (updates) => {
|
|
4264
|
+
return (await driver.updateMany({
|
|
4265
|
+
path: slug,
|
|
4266
|
+
updates: updates.map((u) => ({
|
|
4267
|
+
id: u.id,
|
|
4268
|
+
values: u.data
|
|
4269
|
+
}))
|
|
4270
|
+
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
4271
|
+
} : void 0,
|
|
4272
|
+
deleteMany: driver.deleteMany ? async (ids) => {
|
|
4273
|
+
await driver.deleteMany({
|
|
4274
|
+
path: slug,
|
|
4275
|
+
ids
|
|
4276
|
+
});
|
|
4277
|
+
} : void 0,
|
|
3842
4278
|
count: driver.count ? async (params) => {
|
|
3843
4279
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3844
4280
|
return driver.count({
|
|
3845
4281
|
path: slug,
|
|
3846
|
-
filter
|
|
4282
|
+
filter,
|
|
4283
|
+
logical: params?.logical,
|
|
4284
|
+
searchString: params?.searchString
|
|
3847
4285
|
});
|
|
3848
4286
|
} : void 0,
|
|
3849
4287
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
3850
|
-
const limit = params
|
|
3851
|
-
const offset = params?.offset ?? 0;
|
|
4288
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3852
4289
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
3853
4290
|
return driver.listenCollection({
|
|
3854
4291
|
path: slug,
|
|
3855
|
-
limit
|
|
3856
|
-
offset:
|
|
4292
|
+
limit,
|
|
4293
|
+
offset: driverOffset,
|
|
3857
4294
|
filter: params?.where,
|
|
4295
|
+
logical: params?.logical,
|
|
3858
4296
|
orderBy: params?.orderBy?.[0],
|
|
3859
4297
|
order: params?.orderBy?.[1],
|
|
3860
4298
|
searchString: params?.searchString,
|
|
4299
|
+
searchExplain: params?.searchExplain,
|
|
3861
4300
|
onUpdate: (entities) => {
|
|
3862
4301
|
onUpdate({
|
|
3863
4302
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
3864
4303
|
meta: {
|
|
3865
|
-
total: entities.length,
|
|
4304
|
+
total: offset + entities.length,
|
|
3866
4305
|
limit,
|
|
3867
4306
|
offset,
|
|
3868
4307
|
hasMore: entities.length >= limit
|
|
@@ -3895,8 +4334,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3895
4334
|
offset(count) {
|
|
3896
4335
|
return new QueryBuilder(accessor).offset(count);
|
|
3897
4336
|
},
|
|
3898
|
-
search(searchString) {
|
|
3899
|
-
return new QueryBuilder(accessor).search(searchString);
|
|
4337
|
+
search(searchString, options) {
|
|
4338
|
+
return new QueryBuilder(accessor).search(searchString, options);
|
|
4339
|
+
},
|
|
4340
|
+
vectorSearch(property, vector, options) {
|
|
4341
|
+
return new QueryBuilder(accessor).vectorSearch(property, vector, options);
|
|
3900
4342
|
},
|
|
3901
4343
|
include(...relations) {
|
|
3902
4344
|
return new QueryBuilder(accessor).include(...relations);
|
|
@@ -3984,8 +4426,18 @@ var SdkQueryBuilder = class {
|
|
|
3984
4426
|
this.params.offset = count;
|
|
3985
4427
|
return this;
|
|
3986
4428
|
}
|
|
3987
|
-
search(searchString) {
|
|
4429
|
+
search(searchString, options) {
|
|
3988
4430
|
this.params.searchString = searchString;
|
|
4431
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
4432
|
+
return this;
|
|
4433
|
+
}
|
|
4434
|
+
vectorSearch(property, vector, options) {
|
|
4435
|
+
this.params.vectorSearch = {
|
|
4436
|
+
property,
|
|
4437
|
+
vector,
|
|
4438
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
4439
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
4440
|
+
};
|
|
3989
4441
|
return this;
|
|
3990
4442
|
}
|
|
3991
4443
|
include(...relations) {
|
|
@@ -4039,9 +4491,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
4039
4491
|
async update(id, data) {
|
|
4040
4492
|
return entityToRow(await snap.update(id, data));
|
|
4041
4493
|
},
|
|
4494
|
+
async updateMany(updates) {
|
|
4495
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
4496
|
+
if (updates.length === 0) return [];
|
|
4497
|
+
if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
|
|
4498
|
+
return (await snap.updateMany(updates.map((u) => ({
|
|
4499
|
+
id: u.id,
|
|
4500
|
+
data: u.data
|
|
4501
|
+
})))).map(entityToRow);
|
|
4502
|
+
},
|
|
4042
4503
|
delete(id) {
|
|
4043
4504
|
return snap.delete(id);
|
|
4044
4505
|
},
|
|
4506
|
+
async deleteMany(ids) {
|
|
4507
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
4508
|
+
if (ids.length === 0) return;
|
|
4509
|
+
if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
|
|
4510
|
+
await snap.deleteMany(ids);
|
|
4511
|
+
},
|
|
4045
4512
|
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
4046
4513
|
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
4047
4514
|
data: res.data.map(entityToRow),
|
|
@@ -4057,6 +4524,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
4057
4524
|
limit: (count) => new SdkQueryBuilder(client).limit(count),
|
|
4058
4525
|
offset: (count) => new SdkQueryBuilder(client).offset(count),
|
|
4059
4526
|
search: (searchString) => new SdkQueryBuilder(client).search(searchString),
|
|
4527
|
+
vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
|
|
4060
4528
|
include: (...relations) => new SdkQueryBuilder(client).include(...relations)
|
|
4061
4529
|
};
|
|
4062
4530
|
return client;
|
|
@@ -4102,72 +4570,502 @@ function buildSdkData(driver) {
|
|
|
4102
4570
|
return wrapAsSdkData(buildRebaseData(driver));
|
|
4103
4571
|
}
|
|
4104
4572
|
//#endregion
|
|
4105
|
-
//#region
|
|
4106
|
-
/**
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4573
|
+
//#region src/schema/search-column.ts
|
|
4574
|
+
/**
|
|
4575
|
+
* The one place a collection's `search` block becomes SQL.
|
|
4576
|
+
*
|
|
4577
|
+
* Four things describe a Postgres table in this codebase — the DDL generator,
|
|
4578
|
+
* the Drizzle schema generator, the runtime table builder for BaaS mode, and
|
|
4579
|
+
* the boot-time schema ensure — and each of them has, at some point, described
|
|
4580
|
+
* a column differently from the others. The `varchar(255)` note in
|
|
4581
|
+
* `generate-postgres-ddl-logic` is one such scar: the same property produced a
|
|
4582
|
+
* capped column down one path and an uncapped one down the other, and nothing
|
|
4583
|
+
* failed until a user hit the cap.
|
|
4584
|
+
*
|
|
4585
|
+
* So the search column is not implemented four times. It is computed once,
|
|
4586
|
+
* here, and every generator renders the same {@link SearchColumnSpec}. There is
|
|
4587
|
+
* a test asserting exactly that (`search-column-contract.test.ts`); the point of
|
|
4588
|
+
* this module is that the test has something to assert *about*.
|
|
4589
|
+
*
|
|
4590
|
+
* ## Why the expressions look the way they do
|
|
4591
|
+
*
|
|
4592
|
+
* A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
|
|
4593
|
+
* Postgres is stricter here than intuition. Verified against PostgreSQL 18:
|
|
4594
|
+
*
|
|
4595
|
+
* | expression | immutable |
|
|
4596
|
+
* |-----------------------------------------|-----------|
|
|
4597
|
+
* | `to_tsvector('spanish', col)` | yes |
|
|
4598
|
+
* | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
|
|
4599
|
+
* | `array_to_string(col, ' ')` | **no** |
|
|
4600
|
+
* | `col::text` on `text[]` | **no** |
|
|
4601
|
+
* | `to_jsonb(col)` | **no** |
|
|
4602
|
+
* | `unaccent(col)` | **no** — dictionary lookup is STABLE |
|
|
4603
|
+
* | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
|
|
4604
|
+
* | `setweight(...) || setweight(...)` | yes |
|
|
4605
|
+
*
|
|
4606
|
+
* Three of the four things a real search column needs are therefore unavailable
|
|
4607
|
+
* directly, which is why {@link searchHelperFunctions} exists: each wraps a
|
|
4608
|
+
* stable built-in in an SQL function declared IMMUTABLE. That declaration is a
|
|
4609
|
+
* promise, and it is a true one for these three — array joining, JSON string
|
|
4610
|
+
* extraction and accent folding are all deterministic for a given input; the
|
|
4611
|
+
* built-ins are marked stable only because they must account for element types
|
|
4612
|
+
* and dictionaries in general.
|
|
4613
|
+
*
|
|
4614
|
+
* The alternative was to skip `unaccent` and text arrays entirely. That is not
|
|
4615
|
+
* a real option in an accented language: Postgres stems `auditoría` to
|
|
4616
|
+
* `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
|
|
4617
|
+
* typed without accents misses every row that carries them.
|
|
4618
|
+
*/
|
|
4619
|
+
/** Schema-qualified so a collection outside `public` still resolves them. */
|
|
4620
|
+
var HELPER_SCHEMA = "public";
|
|
4621
|
+
/**
|
|
4622
|
+
* Names of the helper functions. Frozen: they are recorded in the stored
|
|
4623
|
+
* generation expression of every search column ever created, so renaming one
|
|
4624
|
+
* orphans every table that already has a search column.
|
|
4625
|
+
*/
|
|
4626
|
+
var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
|
|
4627
|
+
var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
|
|
4628
|
+
/** Raised when a `search` block names something that cannot be searched. */
|
|
4629
|
+
var SearchConfigError = class extends Error {
|
|
4630
|
+
constructor(message) {
|
|
4631
|
+
super(message);
|
|
4632
|
+
this.name = "SearchConfigError";
|
|
4633
|
+
}
|
|
4634
|
+
};
|
|
4635
|
+
/** The `search` block of a collection, or undefined when it has none. */
|
|
4636
|
+
var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
|
|
4637
|
+
/**
|
|
4638
|
+
* Refuse a `search` block on a collection this engine does not store.
|
|
4639
|
+
*
|
|
4640
|
+
* The type only permits one on a `PostgresCollectionConfig`, so TypeScript
|
|
4641
|
+
* already stops the ordinary case. This catches the rest — a JS config, a cast,
|
|
4642
|
+
* a collection whose `engine` was changed after the block was written — because
|
|
4643
|
+
* the alternative is the exact failure the block exists to prevent: a developer
|
|
4644
|
+
* who declared what to index, saw no error, and got the substring fallback.
|
|
4645
|
+
*
|
|
4646
|
+
* Called with *every* collection, before the Postgres ones are filtered out.
|
|
4647
|
+
*/
|
|
4648
|
+
var assertSearchIsPostgresOnly = (collections) => {
|
|
4649
|
+
for (const collection of collections) {
|
|
4650
|
+
if (isPostgresCollectionConfig(collection)) continue;
|
|
4651
|
+
if (!collection.search) continue;
|
|
4652
|
+
const engine = collection.engine ?? "non-postgres";
|
|
4653
|
+
throw new SearchConfigError(`${collection.slug}.search: full-text search is a Postgres feature, and this collection is served by \`${engine}\`. Remove the block — it would otherwise look configured while \`.search()\` kept using the default substring match.`);
|
|
4654
|
+
}
|
|
4655
|
+
};
|
|
4656
|
+
var columnNameOf = (propName, prop) => prop && "columnName" in prop && typeof prop.columnName === "string" ? prop.columnName : toSnakeCase(propName);
|
|
4657
|
+
/**
|
|
4658
|
+
* Classify a property for search purposes.
|
|
4659
|
+
*
|
|
4660
|
+
* Deliberately narrower than `getSqlColumnType`: search only cares whether a
|
|
4661
|
+
* value reaches text, and the mapping from property to *physical* type is
|
|
4662
|
+
* asserted against `getSqlColumnType` in the contract test rather than
|
|
4663
|
+
* duplicated here.
|
|
4664
|
+
*
|
|
4665
|
+
* Returns null for anything that is not text-bearing, which the caller turns
|
|
4666
|
+
* into a boot error naming the property.
|
|
4667
|
+
*/
|
|
4668
|
+
var classify = (prop) => {
|
|
4669
|
+
switch (prop.type) {
|
|
4670
|
+
case "string": {
|
|
4671
|
+
const sp = prop;
|
|
4672
|
+
if (sp.enum) return {
|
|
4673
|
+
kind: "text",
|
|
4674
|
+
reason: "enum"
|
|
4675
|
+
};
|
|
4676
|
+
if (sp.isId === "uuid" || sp.columnType === "uuid") return {
|
|
4677
|
+
kind: "text",
|
|
4678
|
+
reason: "uuid"
|
|
4679
|
+
};
|
|
4680
|
+
return { kind: "text" };
|
|
4681
|
+
}
|
|
4682
|
+
case "map":
|
|
4683
|
+
if (prop.columnType === "json") return {
|
|
4684
|
+
kind: "jsonb",
|
|
4685
|
+
reason: "json"
|
|
4686
|
+
};
|
|
4687
|
+
return { kind: "jsonb" };
|
|
4688
|
+
case "array": {
|
|
4689
|
+
const ap = prop;
|
|
4690
|
+
let colType = ap.columnType;
|
|
4691
|
+
if (!colType && ap.of && !Array.isArray(ap.of)) {
|
|
4692
|
+
const of = ap.of;
|
|
4693
|
+
if (of.type === "string") colType = "text[]";
|
|
4694
|
+
else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
|
|
4695
|
+
else if (of.type === "boolean") colType = "boolean[]";
|
|
4696
|
+
}
|
|
4697
|
+
if (colType === "text[]") return { kind: "text_array" };
|
|
4698
|
+
if (colType === "json") return {
|
|
4699
|
+
kind: "jsonb",
|
|
4700
|
+
reason: "json"
|
|
4701
|
+
};
|
|
4702
|
+
if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
|
|
4703
|
+
kind: "text_array",
|
|
4704
|
+
reason: "non_text_array"
|
|
4705
|
+
};
|
|
4706
|
+
return { kind: "jsonb" };
|
|
4707
|
+
}
|
|
4708
|
+
default: return null;
|
|
4709
|
+
}
|
|
4710
|
+
};
|
|
4711
|
+
var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
|
|
4712
|
+
/** SQL reading one field as plain text, before normalization. */
|
|
4713
|
+
var rawTextSql = (field) => {
|
|
4714
|
+
const col = `"${field.column}"`;
|
|
4715
|
+
if (field.kind === "text") return `coalesce(${col}, '')`;
|
|
4716
|
+
if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
|
|
4717
|
+
return `${SEARCH_TEXT_FN}(coalesce(${field.jsonPath.length === 0 ? col : field.jsonPath.length === 1 ? `${col} -> ${quote(field.jsonPath[0])}` : `${col} #> ${quote(`{${field.jsonPath.join(",")}}`)}`}, '{}'::jsonb))`;
|
|
4718
|
+
};
|
|
4719
|
+
var quote = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
4114
4720
|
/**
|
|
4115
|
-
*
|
|
4721
|
+
* Resolve and validate one declared field path.
|
|
4116
4722
|
*
|
|
4117
|
-
*
|
|
4118
|
-
*
|
|
4119
|
-
*
|
|
4120
|
-
*
|
|
4723
|
+
* A path that does not resolve throws. The whole point of an explicit block is
|
|
4724
|
+
* that the author knows what is indexed; a silently dropped field would make it
|
|
4725
|
+
* a guess again, and the failure — a search that returns nothing for content
|
|
4726
|
+
* that is plainly in the row — is invisible from the outside.
|
|
4727
|
+
*/
|
|
4728
|
+
var resolveField = (entry, collection, cfg) => {
|
|
4729
|
+
const path = typeof entry === "string" ? entry : entry.path;
|
|
4730
|
+
const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? "B";
|
|
4731
|
+
const where = `${collection.slug}.search`;
|
|
4732
|
+
if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
|
|
4733
|
+
const [head, ...rest] = path.split(".");
|
|
4734
|
+
const prop = collection.properties?.[head];
|
|
4735
|
+
if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
|
|
4736
|
+
const classified = classify(prop);
|
|
4737
|
+
if (!classified) throw new SearchConfigError(`${where}: "${path}" is a \`${prop.type}\` property, which holds no text to search. Searchable kinds are \`string\`, \`string[]\` and \`map\` (or a path inside one).`);
|
|
4738
|
+
if (classified.reason === "enum") throw new SearchConfigError(`${where}: "${path}" is an enum. Enums are a fixed vocabulary — filter on them with \`where\` instead, which is exact and uses an index.`);
|
|
4739
|
+
if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
|
|
4740
|
+
if (classified.reason === "json") throw new SearchConfigError(`${where}: "${path}" is a \`json\` column, and the cast from \`json\` to \`jsonb\` is not immutable, so it cannot feed a generated column. Declare the property as \`jsonb\` (the default) to search it.`);
|
|
4741
|
+
if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
|
|
4742
|
+
if (rest.length > 0 && classified.kind !== "jsonb") throw new SearchConfigError(`${where}: "${path}" addresses a path inside "${head}", but "${head}" is a \`${prop.type}\` property, not a \`map\`. Only map properties have paths inside them.`);
|
|
4743
|
+
const column = columnNameOf(head, prop);
|
|
4744
|
+
const textSql = normalize(rawTextSql({
|
|
4745
|
+
column,
|
|
4746
|
+
jsonPath: rest,
|
|
4747
|
+
kind: classified.kind
|
|
4748
|
+
}), cfg.unaccent === true);
|
|
4749
|
+
const language = cfg.language ?? "simple";
|
|
4750
|
+
return {
|
|
4751
|
+
path,
|
|
4752
|
+
column,
|
|
4753
|
+
jsonPath: rest,
|
|
4754
|
+
kind: classified.kind,
|
|
4755
|
+
weight,
|
|
4756
|
+
sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
|
|
4757
|
+
textSql
|
|
4758
|
+
};
|
|
4759
|
+
};
|
|
4760
|
+
/**
|
|
4761
|
+
* Build the full spec for a collection, or undefined when it has not opted in.
|
|
4762
|
+
*
|
|
4763
|
+
* Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
|
|
4764
|
+
* at boot surface that as a startup failure — a search block that half-works is
|
|
4765
|
+
* worse than one that refuses.
|
|
4766
|
+
*/
|
|
4767
|
+
var buildSearchColumnSpec = (collection) => {
|
|
4768
|
+
const cfg = getSearchConfig(collection);
|
|
4769
|
+
if (!cfg) return void 0;
|
|
4770
|
+
if (!Array.isArray(cfg.fields) || cfg.fields.length === 0) throw new SearchConfigError(`${collection.slug}.search: \`fields\` is empty. Name the properties to index, or remove the \`search\` block to keep the default ILIKE behaviour.`);
|
|
4771
|
+
const table = getTableName(collection);
|
|
4772
|
+
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
4773
|
+
const column = cfg.column ?? "search_vector";
|
|
4774
|
+
if (collection.properties?.[column]) throw new SearchConfigError(`${collection.slug}.search: the generated column "${column}" collides with a declared property of the same name. Set \`search.column\` to something else.`);
|
|
4775
|
+
const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
|
|
4776
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4777
|
+
for (const f of fields) {
|
|
4778
|
+
if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
|
|
4779
|
+
seen.add(f.path);
|
|
4780
|
+
}
|
|
4781
|
+
const extensions = [];
|
|
4782
|
+
if (cfg.unaccent) extensions.push("unaccent");
|
|
4783
|
+
if (cfg.fuzzy) extensions.push("pg_trgm");
|
|
4784
|
+
const spec = {
|
|
4785
|
+
schema,
|
|
4786
|
+
table,
|
|
4787
|
+
column,
|
|
4788
|
+
language: cfg.language ?? "simple",
|
|
4789
|
+
unaccent: cfg.unaccent === true,
|
|
4790
|
+
fields,
|
|
4791
|
+
expression: fields.map((f) => f.sql).join(" || "),
|
|
4792
|
+
indexName: toPostgresIdentifier(`${table}_${column}_gin`),
|
|
4793
|
+
extensions
|
|
4794
|
+
};
|
|
4795
|
+
if (cfg.fuzzy) {
|
|
4796
|
+
const fuzzyColumn = `${column}_text`;
|
|
4797
|
+
if (collection.properties?.[fuzzyColumn]) throw new SearchConfigError(`${collection.slug}.search: \`fuzzy\` needs the column "${fuzzyColumn}", which collides with a declared property. Set \`search.column\` to something else.`);
|
|
4798
|
+
spec.fuzzy = {
|
|
4799
|
+
column: fuzzyColumn,
|
|
4800
|
+
expression: fields.map((f) => f.textSql).join(" || ' ' || "),
|
|
4801
|
+
indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
|
|
4802
|
+
threshold: cfg.fuzzyThreshold ?? .3
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
return spec;
|
|
4806
|
+
};
|
|
4807
|
+
/**
|
|
4808
|
+
* The IMMUTABLE wrappers the generated expressions call.
|
|
4809
|
+
*
|
|
4810
|
+
* `CREATE OR REPLACE` so a boot against an existing database is a no-op rather
|
|
4811
|
+
* than an error, and idempotent for the same reason every other boot-time DDL
|
|
4812
|
+
* statement here is.
|
|
4813
|
+
*
|
|
4814
|
+
* The bodies are stable built-ins wrapped in an immutable promise — see the
|
|
4815
|
+
* module comment for why that promise is sound. `STRICT` matters: it makes NULL
|
|
4816
|
+
* in mean NULL out without executing the body, which is what the `coalesce` at
|
|
4817
|
+
* each call site then absorbs.
|
|
4818
|
+
*/
|
|
4819
|
+
var searchHelperFunctions = (spec) => {
|
|
4820
|
+
const statements = [`CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(text[]) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT array_to_string($1, ' ') $$;`, `CREATE OR REPLACE FUNCTION ${SEARCH_TEXT_FN}(jsonb) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT coalesce(string_agg(v, ' '), '')\n FROM jsonb_array_elements_text(jsonb_path_query_array($1, 'strict $.**?(@.type() == "string")')) AS v $$;`];
|
|
4821
|
+
if (spec.unaccent) statements.push(`CREATE OR REPLACE FUNCTION ${SEARCH_UNACCENT_FN}(text) RETURNS text\n LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS\n $$ SELECT ${HELPER_SCHEMA}.unaccent('${HELPER_SCHEMA}.unaccent'::regdictionary, $1) $$;`);
|
|
4822
|
+
return statements;
|
|
4823
|
+
};
|
|
4824
|
+
/**
|
|
4825
|
+
* `CREATE EXTENSION` statements the spec's expressions depend on.
|
|
4826
|
+
*
|
|
4827
|
+
* `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified
|
|
4828
|
+
* `CREATE EXTENSION` installs into the first schema on `search_path`, which
|
|
4829
|
+
* defaults to `"$user", public` — and the scaffold's database role is named
|
|
4830
|
+
* `rebase`, the same as the schema the generator creates one statement earlier.
|
|
4831
|
+
* So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
|
|
4832
|
+
* dictionary in `rebase`, and every reference to `public.unaccent` below fails
|
|
4833
|
+
* with "text search dictionary does not exist". Observed, not theorised.
|
|
4834
|
+
*/
|
|
4835
|
+
var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
|
|
4836
|
+
/**
|
|
4837
|
+
* Index statements for the spec.
|
|
4838
|
+
*
|
|
4839
|
+
* `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
|
|
4840
|
+
* SQL file replayed as one unit — a migration, or `search.sql` — where a
|
|
4841
|
+
* concurrent build is not allowed. The boot-time ensure path runs statement by
|
|
4842
|
+
* statement against tables that are live and populated, and uses the
|
|
4843
|
+
* concurrent form instead; see `ensureSearchColumns`.
|
|
4844
|
+
*/
|
|
4845
|
+
var searchIndexStatements = (spec) => {
|
|
4846
|
+
const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
|
|
4847
|
+
if (spec.fuzzy) statements.push(`CREATE INDEX IF NOT EXISTS "${spec.fuzzy.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.fuzzy.column}" ${HELPER_SCHEMA}.gin_trgm_ops);`);
|
|
4848
|
+
return statements;
|
|
4849
|
+
};
|
|
4850
|
+
/**
|
|
4851
|
+
* Marker on the comment of every generated search column this module creates.
|
|
4852
|
+
*
|
|
4853
|
+
* Versioned because the fingerprint below is only comparable against itself: a
|
|
4854
|
+
* future change to how it is computed has to read as "not stamped by this
|
|
4855
|
+
* version" rather than as drift on every existing column.
|
|
4856
|
+
*/
|
|
4857
|
+
var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
|
|
4858
|
+
/**
|
|
4859
|
+
* A stable fingerprint of one generated column's expression.
|
|
4860
|
+
*
|
|
4861
|
+
* Why a stamp rather than reading the expression back: Postgres stores a
|
|
4862
|
+
* generated column's expression *parsed*, and hands it back deparsed — casts
|
|
4863
|
+
* made explicit, identifiers requoted, schema qualifications added or dropped
|
|
4864
|
+
* according to `search_path`. Comparing that text to the text we generated
|
|
4865
|
+
* would report drift on wording, and this comparison decides whether a boot
|
|
4866
|
+
* refuses, so a false positive is an outage. The stamp is written by the same
|
|
4867
|
+
* code that writes the column, so equality means what it says.
|
|
4868
|
+
*/
|
|
4869
|
+
var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
|
|
4870
|
+
/**
|
|
4871
|
+
* The stamps for a spec's generated columns — one per column, never shared.
|
|
4121
4872
|
*
|
|
4122
|
-
*
|
|
4123
|
-
*
|
|
4124
|
-
*
|
|
4125
|
-
*
|
|
4873
|
+
* Per column on purpose: turning `fuzzy` on adds a second column and changes
|
|
4874
|
+
* nothing about the first, and a spec-wide fingerprint would report the
|
|
4875
|
+
* untouched `tsvector` column as drifted and refuse a boot over a change that
|
|
4876
|
+
* is purely additive.
|
|
4126
4877
|
*/
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4878
|
+
var searchColumnStamps = (spec) => {
|
|
4879
|
+
const stamp = (column, expression) => {
|
|
4880
|
+
const fingerprint = searchExpressionFingerprint(expression);
|
|
4881
|
+
return {
|
|
4882
|
+
column,
|
|
4883
|
+
expression,
|
|
4884
|
+
fingerprint,
|
|
4885
|
+
sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
|
|
4886
|
+
};
|
|
4887
|
+
};
|
|
4888
|
+
const stamps = [stamp(spec.column, spec.expression)];
|
|
4889
|
+
if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
|
|
4890
|
+
return stamps;
|
|
4891
|
+
};
|
|
4892
|
+
/**
|
|
4893
|
+
* The generated column names a collection's search block adds, if any.
|
|
4894
|
+
*
|
|
4895
|
+
* These are physical columns on the table, so `SELECT *` returns them. They are
|
|
4896
|
+
* an index in column form — a list of lexeme positions, or a concatenation of
|
|
4897
|
+
* every searchable field on the row — and nothing outside the query planner has
|
|
4898
|
+
* any use for them. Left in, every list response carries a second, larger copy
|
|
4899
|
+
* of the row's text.
|
|
4900
|
+
*/
|
|
4901
|
+
var searchColumnNames = (collection) => {
|
|
4902
|
+
let spec;
|
|
4903
|
+
try {
|
|
4904
|
+
spec = buildSearchColumnSpec(collection);
|
|
4905
|
+
} catch {
|
|
4906
|
+
return [];
|
|
4907
|
+
}
|
|
4908
|
+
if (!spec) return [];
|
|
4909
|
+
return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
|
|
4910
|
+
};
|
|
4154
4911
|
/**
|
|
4155
|
-
*
|
|
4912
|
+
* True for a column whose type only ever holds a search index.
|
|
4156
4913
|
*
|
|
4157
|
-
*
|
|
4158
|
-
*
|
|
4914
|
+
* Independent of any collection config on purpose: an introspected database
|
|
4915
|
+
* (BaaS mode) can carry a `tsvector` column this framework never created —
|
|
4916
|
+
* Pagila's `film.fulltext` is the canonical one — and it should not be returned
|
|
4917
|
+
* to callers either. `isDerivedIndexColumn` already keeps such a column out of
|
|
4918
|
+
* the *properties*; this keeps it out of the *rows*.
|
|
4919
|
+
*/
|
|
4920
|
+
var isSearchIndexColumn = (column) => {
|
|
4921
|
+
const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
|
|
4922
|
+
return sqlType === "tsvector" || sqlType === "tsquery";
|
|
4923
|
+
};
|
|
4924
|
+
/**
|
|
4925
|
+
* A drizzle select projection over `table` with the search columns dropped.
|
|
4926
|
+
*
|
|
4927
|
+
* Returns undefined when nothing needs dropping, so the common case keeps using
|
|
4928
|
+
* a plain `select()` and this stays invisible in the generated SQL.
|
|
4929
|
+
*/
|
|
4930
|
+
var visibleColumnProjection = (tableColumns, collection) => {
|
|
4931
|
+
const excluded = excludedColumnNames(tableColumns, collection);
|
|
4932
|
+
if (!tableColumns || excluded.length === 0) return void 0;
|
|
4933
|
+
const projection = {};
|
|
4934
|
+
for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
|
|
4935
|
+
return projection;
|
|
4936
|
+
};
|
|
4937
|
+
/** The same exclusion as a drizzle `db.query` `columns` denylist. */
|
|
4938
|
+
var hiddenColumnsOption = (tableColumns, collection) => {
|
|
4939
|
+
const excluded = excludedColumnNames(tableColumns, collection);
|
|
4940
|
+
if (excluded.length === 0) return void 0;
|
|
4941
|
+
return Object.fromEntries(excluded.map((name) => [name, false]));
|
|
4942
|
+
};
|
|
4943
|
+
/**
|
|
4944
|
+
* The columns to keep out of a response, by name.
|
|
4159
4945
|
*
|
|
4160
|
-
*
|
|
4161
|
-
*
|
|
4162
|
-
*
|
|
4946
|
+
* `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
|
|
4947
|
+
* for anything that is not a real drizzle table — a stub in a test, a derived
|
|
4948
|
+
* or nested path with no table behind it. Nothing to exclude is the right
|
|
4949
|
+
* answer there, and it has to be an answer rather than a throw: this runs on
|
|
4950
|
+
* the read path of every collection, opted in or not.
|
|
4163
4951
|
*/
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
const
|
|
4167
|
-
|
|
4168
|
-
|
|
4952
|
+
var excludedColumnNames = (tableColumns, collection) => {
|
|
4953
|
+
if (!tableColumns || typeof tableColumns !== "object") return [];
|
|
4954
|
+
const byName = new Set(collection ? searchColumnNames(collection) : []);
|
|
4955
|
+
return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
|
|
4956
|
+
};
|
|
4957
|
+
//#endregion
|
|
4958
|
+
//#region src/schema/auth-users-columns.ts
|
|
4959
|
+
/**
|
|
4960
|
+
* `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
|
|
4961
|
+
* — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
|
|
4962
|
+
* The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
|
|
4963
|
+
* separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
|
|
4964
|
+
* table that already holds a longer row.
|
|
4965
|
+
*/
|
|
4966
|
+
var AUTH_USERS_COLUMNS = [
|
|
4967
|
+
{
|
|
4968
|
+
column: "email",
|
|
4969
|
+
type: "TEXT",
|
|
4970
|
+
notNull: true
|
|
4971
|
+
},
|
|
4972
|
+
{
|
|
4973
|
+
column: "display_name",
|
|
4974
|
+
type: "TEXT"
|
|
4975
|
+
},
|
|
4976
|
+
{
|
|
4977
|
+
column: "photo_url",
|
|
4978
|
+
type: "TEXT"
|
|
4979
|
+
},
|
|
4980
|
+
{
|
|
4981
|
+
column: "roles",
|
|
4982
|
+
type: "TEXT[]",
|
|
4983
|
+
default: "'{}'",
|
|
4984
|
+
notNull: true
|
|
4985
|
+
},
|
|
4986
|
+
{
|
|
4987
|
+
column: "password_hash",
|
|
4988
|
+
type: "TEXT"
|
|
4989
|
+
},
|
|
4990
|
+
{
|
|
4991
|
+
column: "email_verified",
|
|
4992
|
+
type: "BOOLEAN",
|
|
4993
|
+
default: "FALSE",
|
|
4994
|
+
notNull: true
|
|
4995
|
+
},
|
|
4996
|
+
{
|
|
4997
|
+
column: "email_verification_token",
|
|
4998
|
+
type: "TEXT"
|
|
4999
|
+
},
|
|
5000
|
+
{
|
|
5001
|
+
column: "email_verification_sent_at",
|
|
5002
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
5003
|
+
},
|
|
5004
|
+
{
|
|
5005
|
+
column: "is_anonymous",
|
|
5006
|
+
type: "BOOLEAN",
|
|
5007
|
+
default: "FALSE",
|
|
5008
|
+
notNull: true
|
|
5009
|
+
},
|
|
5010
|
+
{
|
|
5011
|
+
column: "metadata",
|
|
5012
|
+
type: "JSONB",
|
|
5013
|
+
default: "'{}'",
|
|
5014
|
+
notNull: true
|
|
5015
|
+
},
|
|
5016
|
+
{
|
|
5017
|
+
column: "tokens_valid_after",
|
|
5018
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
5019
|
+
},
|
|
5020
|
+
{
|
|
5021
|
+
column: "created_at",
|
|
5022
|
+
type: "TIMESTAMP WITH TIME ZONE",
|
|
5023
|
+
default: "NOW()",
|
|
5024
|
+
notNull: true
|
|
5025
|
+
},
|
|
5026
|
+
{
|
|
5027
|
+
column: "updated_at",
|
|
5028
|
+
type: "TIMESTAMP WITH TIME ZONE",
|
|
5029
|
+
default: "NOW()",
|
|
5030
|
+
notNull: true
|
|
5031
|
+
}
|
|
5032
|
+
];
|
|
5033
|
+
var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
|
|
5034
|
+
/** Type + inline constraints, as they appear after the column name. */
|
|
5035
|
+
function authUsersColumnSql(spec) {
|
|
5036
|
+
return [
|
|
5037
|
+
spec.type,
|
|
5038
|
+
spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
|
|
5039
|
+
spec.notNull ? "NOT NULL" : ""
|
|
5040
|
+
].filter(Boolean).join(" ");
|
|
5041
|
+
}
|
|
5042
|
+
/**
|
|
5043
|
+
* The auth-owned definition for a physical column name, or `undefined` when
|
|
5044
|
+
* auth does not own it.
|
|
5045
|
+
*
|
|
5046
|
+
* Callers pass the RESOLVED column name (after `columnName` mapping), because
|
|
5047
|
+
* that is the only name the three creators agree on: the scaffold's users
|
|
5048
|
+
* collection spells the property `displayName` and the column `display_name`.
|
|
5049
|
+
*/
|
|
5050
|
+
function authUsersColumnDefinition(column) {
|
|
5051
|
+
const spec = BY_COLUMN.get(column);
|
|
5052
|
+
return spec ? authUsersColumnSql(spec) : void 0;
|
|
5053
|
+
}
|
|
5054
|
+
/**
|
|
5055
|
+
* Whether a collection is an auth collection, i.e. whether the definitions in
|
|
5056
|
+
* this module apply to its table at all.
|
|
5057
|
+
*
|
|
5058
|
+
* Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
|
|
5059
|
+
* that one takes a `CollectionConfig`, this one is called from DDL code paths
|
|
5060
|
+
* that hold looser objects, and both spellings must accept `auth: true` as well
|
|
5061
|
+
* as `auth: { enabled: true }`.
|
|
5062
|
+
*/
|
|
5063
|
+
function isAuthCollection(collection) {
|
|
5064
|
+
const auth = collection?.auth;
|
|
5065
|
+
if (auth === true) return true;
|
|
5066
|
+
return typeof auth === "object" && auth !== null && auth.enabled === true;
|
|
4169
5067
|
}
|
|
4170
5068
|
//#endregion
|
|
4171
|
-
export {
|
|
5069
|
+
export { isManyToMany as $, getTableName as A, normalizeToEntityRelation as B, getEffectiveSecurityRules as C, findRelation as D, findAnonymousGrants as E, isAddressableId as F, getPolicyNamesForRule as G, generateForeignKeyName as H, parseIdValues as I, camelCase as J, isPrototypePollutingKey as K, sortCollectionsBySlug as L, resolveCollectionRelations as M, buildCompositeId as N, getColumnName as O, getDeclaredPrimaryKeys as P, hasForeignKeyOnTarget as Q, createRelationRef as R, resolveJunctionSpecs as S, securityRuleToConditions as T, legacyForeignKeyName as U, updateDateAutoValues as V, toPostgresIdentifier as W, DEFAULT_ONE_OF_TYPE as X, toSnakeCase as Y, DEFAULT_ONE_OF_VALUE as Z, CollectionRegistry as _, SEARCH_STAMP_PREFIX as a, getJunctionCollectionConfig as b, assertSearchIsPostgresOnly as c, searchColumnStamps as d, Vector as et, searchExtensionStatements as f, buildSdkData as g, visibleColumnProjection as h, isAuthCollection as i, getTableVarName as j, getEnumVarName as k, buildSearchColumnSpec as l, searchIndexStatements as m, authUsersColumnDefinition as n, SEARCH_TEXT_FN as o, searchHelperFunctions as p, mergeDeep as q, authUsersColumnSql as r, SEARCH_UNACCENT_FN as s, AUTH_USERS_COLUMNS as t, hiddenColumnsOption as u, relationalCollections as v, policyToPostgres as w, getJunctionSecurityRules as x, resolveStringColumnLength as y, createRelationRefWithData as z };
|
|
4172
5070
|
|
|
4173
|
-
//# sourceMappingURL=
|
|
5071
|
+
//# sourceMappingURL=auth-users-columns-CBEOeYqa.js.map
|