@rebasepro/server-postgres 0.13.1-canary.gf57a27e → 0.14.1-canary.g7e666eb
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 +1 -1
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +31 -0
- package/dist/{auth-users-columns-Dt9g712t.js → auth-users-columns-C-FDnL_e.js} +768 -76
- package/dist/auth-users-columns-C-FDnL_e.js.map +1 -0
- package/dist/{backup-service-Bww-Lg0s.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-Bww-Lg0s.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-output.d.ts +34 -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-BedO2aNX.js +124 -0
- package/dist/ensure-collection-policies-BedO2aNX.js.map +1 -0
- package/dist/{ensure-collection-tables-DRxaUG96.js → ensure-collection-tables-B1qKdXA4.js} +89 -10
- package/dist/ensure-collection-tables-B1qKdXA4.js.map +1 -0
- package/dist/index.es.js +1615 -1210
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js → rls-bootstrap-sql-69hYT8nr.js} +2 -2
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js.map → rls-bootstrap-sql-69hYT8nr.js.map} +1 -1
- package/dist/rls-enforcement-gUNDfm7l.js +425 -0
- package/dist/rls-enforcement-gUNDfm7l.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- package/dist/schema/ensure-collection-tables.d.ts +60 -6
- package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
- package/dist/schema/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/search-column.d.ts +49 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +8 -3
- package/dist/services/FetchService.d.ts +63 -5
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +11 -59
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +8 -4
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +191 -12
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-C_wvdMnl.js → src-DCdn3Val.js} +35 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +54 -1
- package/dist/{websocket-D0TBU3ia.js → websocket-D2jXv0Ds.js} +103 -19
- package/dist/websocket-D2jXv0Ds.js.map +1 -0
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +14 -6
- package/src/PostgresBootstrapper.ts +95 -9
- package/src/auth/ensure-tables.ts +27 -5
- package/src/auth/services.ts +108 -10
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +4 -4
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +155 -147
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +111 -25
- package/src/history/ensure-history-table.ts +2 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +149 -72
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +214 -17
- package/src/schema/generate-drizzle-schema-logic.ts +140 -66
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +28 -1
- package/src/schema/generate-postgres-ddl.ts +14 -13
- package/src/schema/generated-schema-staleness.ts +7 -5
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- package/src/schema/search-column.ts +85 -0
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +9 -4
- package/src/services/FetchService.ts +289 -69
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +38 -698
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +9 -3
- package/src/services/channel-presence.ts +10 -3
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +8 -4
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +15 -1
- package/src/services/realtimeService.ts +496 -121
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +211 -34
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +157 -17
- package/dist/auth-users-columns-Dt9g712t.js.map +0 -1
- package/dist/ensure-collection-policies-CwYUliAa.js +0 -57
- package/dist/ensure-collection-policies-CwYUliAa.js.map +0 -1
- package/dist/ensure-collection-tables-DRxaUG96.js.map +0 -1
- package/dist/policy-CPkCqVTz.js +0 -105
- package/dist/policy-CPkCqVTz.js.map +0 -1
- package/dist/src-C_wvdMnl.js.map +0 -1
- package/dist/websocket-D0TBU3ia.js.map +0 -1
|
@@ -2,8 +2,9 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { l as __require, s as __commonJSMin } from "./connection-BuZ97wsr.js";
|
|
5
|
-
import { a as
|
|
6
|
-
import {
|
|
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 ALL_WHERE_FILTER_OPS, g as toCanonicalOp, h as REST_TO_CANONICAL, i as RLS_UID_SQL, l as isRelationalCollectionConfig, m as NULL_OPS, p as CANONICAL_TO_REST, r as RLS_ROLES_SQL, s as getDeclaredSubcollections } from "./src-DCdn3Val.js";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
//#region ../types/src/types/entities.ts
|
|
8
9
|
/**
|
|
9
10
|
* Class used to create a reference to a entity in a different path
|
|
@@ -55,25 +56,6 @@ function hasForeignKeyOnTarget(relation) {
|
|
|
55
56
|
function isManyToMany(relation) {
|
|
56
57
|
return relation.kind === "manyToMany";
|
|
57
58
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
60
|
-
*
|
|
61
|
-
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
62
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
63
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
64
|
-
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
65
|
-
*
|
|
66
|
-
* The return is never `undefined` — no ingress that routes its client limit
|
|
67
|
-
* through this can produce an unbounded read.
|
|
68
|
-
*/
|
|
69
|
-
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
70
|
-
const maxLimit = opts.maxLimit ?? 1e3;
|
|
71
|
-
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
72
|
-
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
73
|
-
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
74
|
-
}
|
|
75
|
-
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
76
|
-
}
|
|
77
59
|
//#endregion
|
|
78
60
|
//#region ../common/src/util/common.ts
|
|
79
61
|
var DEFAULT_ONE_OF_TYPE = "type";
|
|
@@ -1430,6 +1412,73 @@ function toPostgresIdentifier(name) {
|
|
|
1430
1412
|
if (bytes.byteLength <= 63) return name;
|
|
1431
1413
|
return new TextDecoder("utf-8").decode(bytes.subarray(0, 63)).replace(/�+$/, "");
|
|
1432
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* The API name a database column is served under.
|
|
1417
|
+
*
|
|
1418
|
+
* The wire name of a field is its property key, and Rebase's property keys are
|
|
1419
|
+
* camelCase — `displayName`, `createdAt`, `photoURL`. Columns are snake_case,
|
|
1420
|
+
* because an unquoted Postgres identifier folds to lower case and a camelCase
|
|
1421
|
+
* column is therefore reachable only as `"authorId"` forever: in hand-written
|
|
1422
|
+
* SQL, in psql, in an RLS policy body, in a dump, and in every third-party tool
|
|
1423
|
+
* that ever touches the database. So the two conventions are both right, and
|
|
1424
|
+
* this is the function that crosses between them.
|
|
1425
|
+
*
|
|
1426
|
+
* It exists because two sources of field names never crossed: a foreign key
|
|
1427
|
+
* derived from a relation (`author_id`) and a column read back by introspection
|
|
1428
|
+
* (`user_id`) both landed on the wire under their column name, while every
|
|
1429
|
+
* hand-authored collection next to them used camelCase. One API, two
|
|
1430
|
+
* conventions, and no rule a caller could infer from outside — those names are
|
|
1431
|
+
* also the `where` and `orderBy` keys, so it was not a matter of taste.
|
|
1432
|
+
*
|
|
1433
|
+
* Rules, in the order they matter:
|
|
1434
|
+
*
|
|
1435
|
+
* - **A name with no separator is returned unchanged.** `photoURL` stays
|
|
1436
|
+
* `photoURL` and `id` stays `id`. Lower-casing a single token is what makes
|
|
1437
|
+
* a "camelCase" helper destructive — `camelCase("photoURL")` is `photourl` —
|
|
1438
|
+
* and this function is applied to names that are *already* keys.
|
|
1439
|
+
* - **Each following segment keeps its own casing** apart from an upper-cased
|
|
1440
|
+
* first letter, so `photo_URL` → `photoURL` rather than `photoUrl`.
|
|
1441
|
+
* - **The result may still not be a JavaScript identifier.** `2fa_enabled`
|
|
1442
|
+
* becomes `2faEnabled`, which is a perfectly good object key and still needs
|
|
1443
|
+
* quoting where one is written into generated source.
|
|
1444
|
+
*
|
|
1445
|
+
* Not the inverse of {@link toSnakeCase}: `toSnakeCase` tokenises on case
|
|
1446
|
+
* boundaries and would turn `photoURL` into `photo_url`. Round-tripping is not
|
|
1447
|
+
* a property either function promises, which is why a column name that a
|
|
1448
|
+
* property maps explicitly is always read off `columnName` rather than derived.
|
|
1449
|
+
*/
|
|
1450
|
+
function toWireKey(columnName) {
|
|
1451
|
+
if (!columnName) return columnName;
|
|
1452
|
+
const segments = columnName.split(/[-_ ]+/).filter(Boolean);
|
|
1453
|
+
if (segments.length <= 1) return columnName;
|
|
1454
|
+
return segments.map((segment, index) => index === 0 ? segment.charAt(0).toLowerCase() + segment.slice(1) : segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* The first candidate key not already used, or a numbered fallback.
|
|
1458
|
+
*
|
|
1459
|
+
* Introspection turns a set of column names into a set of object keys, and the
|
|
1460
|
+
* mapping is not injective: `user_id` and `userId` are two columns and one
|
|
1461
|
+
* {@link toWireKey}, and two foreign keys can strip to the same relation name.
|
|
1462
|
+
* A duplicate key in a generated object literal is a TypeScript error, so the
|
|
1463
|
+
* whole collection stops compiling — and a duplicate key in a `Record` built at
|
|
1464
|
+
* runtime is worse, because it silently drops a column instead.
|
|
1465
|
+
*
|
|
1466
|
+
* The numbered tail is what makes this total: a function that returns a key it
|
|
1467
|
+
* cannot guarantee is free has only moved the duplicate one line down.
|
|
1468
|
+
*
|
|
1469
|
+
* Structurally typed on `has` so a `Map` of emitted blocks and a `Set` of taken
|
|
1470
|
+
* names both satisfy it. Lives here, in the package both introspection
|
|
1471
|
+
* producers and the admin's table import can reach, because they must resolve a
|
|
1472
|
+
* collision the same way or one database describes itself three ways.
|
|
1473
|
+
*/
|
|
1474
|
+
function firstFreeKey(candidates, taken) {
|
|
1475
|
+
for (const candidate of candidates) if (!taken.has(candidate)) return candidate;
|
|
1476
|
+
const base = candidates[candidates.length - 1];
|
|
1477
|
+
for (let suffix = 2;; suffix++) {
|
|
1478
|
+
const candidate = `${base}_${suffix}`;
|
|
1479
|
+
if (!taken.has(candidate)) return candidate;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1433
1482
|
//#endregion
|
|
1434
1483
|
//#region ../common/src/util/entities.ts
|
|
1435
1484
|
/**
|
|
@@ -1539,6 +1588,25 @@ function createRelationRefWithData(id, path, data) {
|
|
|
1539
1588
|
data
|
|
1540
1589
|
};
|
|
1541
1590
|
}
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region ../common/src/util/collections.ts
|
|
1593
|
+
/**
|
|
1594
|
+
* A copy of `collections` ordered by slug.
|
|
1595
|
+
*
|
|
1596
|
+
* Every generator that turns collections into a file is order-dependent, and
|
|
1597
|
+
* every one of them is compared against its own output — `rebase doctor`
|
|
1598
|
+
* regenerates in memory and diffs, `generate-sdk && git diff --exit-code` gates
|
|
1599
|
+
* CI. While only the *writers* sorted, a project whose `readdirSync` order
|
|
1600
|
+
* differed from its slug order was reported permanently out of date, and the
|
|
1601
|
+
* fix the message printed rewrote the file in the order it was already in. The
|
|
1602
|
+
* generators sort themselves now, so no caller can get this wrong.
|
|
1603
|
+
*
|
|
1604
|
+
* A slug-less collection is left to the generator's own validation, which names
|
|
1605
|
+
* the offending collection; sorting must not throw first.
|
|
1606
|
+
*/
|
|
1607
|
+
function sortCollectionsBySlug(collections) {
|
|
1608
|
+
return [...collections].sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""));
|
|
1609
|
+
}
|
|
1542
1610
|
/** The eight-four-four-four-twelve shape of a UUID, any version. */
|
|
1543
1611
|
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1544
1612
|
/** Whether one address part can be a value of the column it addresses. */
|
|
@@ -1875,6 +1943,48 @@ function getColumnName(fullColumn) {
|
|
|
1875
1943
|
return fullColumn.includes(".") ? fullColumn.split(".").pop() : fullColumn;
|
|
1876
1944
|
}
|
|
1877
1945
|
/**
|
|
1946
|
+
* The field key a database column is served and addressed under.
|
|
1947
|
+
*
|
|
1948
|
+
* A column has two names and they are not the same name. `author_id` is what
|
|
1949
|
+
* Postgres stores; `authorId` is the key on the JSON row, the key in the
|
|
1950
|
+
* generated Drizzle table, and the key a caller writes in `where` and
|
|
1951
|
+
* `orderBy`. Every place that starts from a column and has to reach a row, a
|
|
1952
|
+
* Drizzle table or a payload goes through here, so there is one answer rather
|
|
1953
|
+
* than one per call site — the two that disagreed put `displayName` and
|
|
1954
|
+
* `author_id` on the same API.
|
|
1955
|
+
*
|
|
1956
|
+
* A declared property is the authority when there is one, because its key *is*
|
|
1957
|
+
* the wire name and `columnName` is the only thing that ever renamed the
|
|
1958
|
+
* column:
|
|
1959
|
+
*
|
|
1960
|
+
* 1. an explicit `columnName` equal to this column;
|
|
1961
|
+
* 2. a property whose key is literally the column (an author who wrote
|
|
1962
|
+
* `author_id:` meant `author_id` on the wire, and gets it);
|
|
1963
|
+
* 3. a property whose key snake-cases to the column, which is the default
|
|
1964
|
+
* mapping — `authorId` → `author_id`.
|
|
1965
|
+
*
|
|
1966
|
+
* With no property in the way — a foreign key derived from a relation, which
|
|
1967
|
+
* usually has none — the name is derived: {@link toWireKey}.
|
|
1968
|
+
*
|
|
1969
|
+
* Note the fallback is *not* the column verbatim. That was the old behaviour
|
|
1970
|
+
* and it is precisely the defect: a derived foreign key reached the wire under
|
|
1971
|
+
* its column name while every hand-authored field beside it was camelCase.
|
|
1972
|
+
*/
|
|
1973
|
+
function fieldKeyForColumn(collection, column) {
|
|
1974
|
+
const properties = collection?.properties;
|
|
1975
|
+
if (properties) {
|
|
1976
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
1977
|
+
const columnName = prop?.columnName;
|
|
1978
|
+
if (typeof columnName === "string" && columnName === column) return key;
|
|
1979
|
+
}
|
|
1980
|
+
for (const key of Object.keys(properties)) {
|
|
1981
|
+
if (key === column) return key;
|
|
1982
|
+
if (toSnakeCase(key) === column) return key;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return toWireKey(column);
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1878
1988
|
* Look up a relation by key with forgiving normalization.
|
|
1879
1989
|
*
|
|
1880
1990
|
* `resolveCollectionRelations` stores each relation under a single canonical
|
|
@@ -2114,11 +2224,32 @@ function sqlToPolicy(sql) {
|
|
|
2114
2224
|
* is the more dangerous spelling, because it inverts a rule instead of
|
|
2115
2225
|
* emptying a table.
|
|
2116
2226
|
*/
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2227
|
+
/**
|
|
2228
|
+
* A `Map`, not an object literal.
|
|
2229
|
+
*
|
|
2230
|
+
* As `Record<string, string>` this was indexed with a literal taken straight
|
|
2231
|
+
* out of a policy, so every key on `Object.prototype` answered: a rule
|
|
2232
|
+
* comparing `rebase.uid()` to `"valueOf"`, `"toString"`, `"constructor"` or
|
|
2233
|
+
* `"hasOwnProperty"` found a truthy "platform" and reported an anonymous-grant
|
|
2234
|
+
* risk that does not exist — with the matched function interpolated into the
|
|
2235
|
+
* explanation as the platform's name. A security warning that fires on
|
|
2236
|
+
* innocent input is worse than none: it is what teaches people to skip the
|
|
2237
|
+
* warnings that are real.
|
|
2238
|
+
*
|
|
2239
|
+
* Same shape as the prototype-pollution class swept out of `setIn`, `getIn`,
|
|
2240
|
+
* `mergeDeep` and `unflattenObject` — a data-derived key reaching a plain
|
|
2241
|
+
* object. Found by a property test, on the input `"valueOf"`.
|
|
2242
|
+
*/
|
|
2243
|
+
var FOREIGN_CONVENTION_UIDS = /* @__PURE__ */ new Map([
|
|
2244
|
+
["anon", "Supabase"],
|
|
2245
|
+
["authenticated", "Supabase"],
|
|
2246
|
+
["service_role", "Supabase"]
|
|
2247
|
+
]);
|
|
2248
|
+
/**
|
|
2249
|
+
* The same foreign literals, as a pattern for SQL that could not be parsed
|
|
2250
|
+
* back into structure.
|
|
2251
|
+
*/
|
|
2252
|
+
var FOREIGN_UID_LITERAL_SQL = new RegExp(String.raw`rebase\.uid\(\)\s*=\s*'(${[...FOREIGN_CONVENTION_UIDS.keys()].join("|")})'`, "i");
|
|
2122
2253
|
/**
|
|
2123
2254
|
* `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
|
|
2124
2255
|
*
|
|
@@ -2165,17 +2296,27 @@ function findAnonymousGrants(expr) {
|
|
|
2165
2296
|
case "existsIn":
|
|
2166
2297
|
visit(e.where);
|
|
2167
2298
|
return;
|
|
2168
|
-
case "raw":
|
|
2299
|
+
case "raw": {
|
|
2169
2300
|
if (UID_NOT_NULL.test(e.sql)) found.push({
|
|
2170
2301
|
pattern: "uid-not-null",
|
|
2171
2302
|
detail: e.sql,
|
|
2172
2303
|
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".`
|
|
2173
2304
|
});
|
|
2305
|
+
const foreign = FOREIGN_UID_LITERAL_SQL.exec(e.sql);
|
|
2306
|
+
if (foreign) {
|
|
2307
|
+
const literal = foreign[1];
|
|
2308
|
+
found.push({
|
|
2309
|
+
pattern: "foreign-uid-literal",
|
|
2310
|
+
detail: literal,
|
|
2311
|
+
explanation: `'${literal}' is a ${FOREIGN_CONVENTION_UIDS.get(literal)} convention. Rebase reports an anonymous request as '${ANONYMOUS_USER_ID}', so comparing against '${literal}' passes for every caller. Use \`condition: policy.authenticated()\` to mean "signed in".`
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2174
2314
|
return;
|
|
2315
|
+
}
|
|
2175
2316
|
case "compare": {
|
|
2176
2317
|
const literal = [e.left, e.right].find((o) => o.kind === "literal");
|
|
2177
2318
|
if (!(e.left.kind === "authUid" || e.right.kind === "authUid") || typeof literal?.value !== "string") return;
|
|
2178
|
-
const platform = FOREIGN_CONVENTION_UIDS
|
|
2319
|
+
const platform = FOREIGN_CONVENTION_UIDS.get(literal.value);
|
|
2179
2320
|
if (!platform) return;
|
|
2180
2321
|
found.push({
|
|
2181
2322
|
pattern: "foreign-uid-literal",
|
|
@@ -2191,12 +2332,43 @@ function findAnonymousGrants(expr) {
|
|
|
2191
2332
|
return found;
|
|
2192
2333
|
}
|
|
2193
2334
|
function parseOperand(str) {
|
|
2194
|
-
if (
|
|
2195
|
-
const
|
|
2196
|
-
if (
|
|
2197
|
-
if (
|
|
2335
|
+
if (/^current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)$/i.test(str) || /^rebase\.uid\(\)$/i.test(str)) return policy.authUid();
|
|
2336
|
+
const literal = parseSingleQuoted(str);
|
|
2337
|
+
if (literal !== null) return policy.literal(literal);
|
|
2338
|
+
if (/^-?\d+$/.test(str)) return policy.literal(Number(str));
|
|
2339
|
+
if (/^-?\d*\.\d+$/.test(str)) return policy.literal(Number(str));
|
|
2340
|
+
if (/^true$/i.test(str)) return policy.literal(true);
|
|
2341
|
+
if (/^false$/i.test(str)) return policy.literal(false);
|
|
2342
|
+
if (/^null$/i.test(str)) return policy.literal(null);
|
|
2343
|
+
if (/^\w+$/.test(str) && toSnakeCase(str) !== "") return policy.field(str);
|
|
2198
2344
|
return null;
|
|
2199
2345
|
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Decode a single-quoted SQL literal, or null when `str` is not exactly one.
|
|
2348
|
+
*
|
|
2349
|
+
* Rejecting is as important as decoding: `'a' = 'b'` is two literals and an
|
|
2350
|
+
* operator, not one literal whose body contains a quote, and a regex anchored
|
|
2351
|
+
* on the outer quotes would happily read it as the latter. Every interior quote
|
|
2352
|
+
* must therefore be part of a `''` pair.
|
|
2353
|
+
*/
|
|
2354
|
+
function parseSingleQuoted(str) {
|
|
2355
|
+
if (str.length < 2 || !str.startsWith("'") || !str.endsWith("'")) return null;
|
|
2356
|
+
const body = str.slice(1, -1);
|
|
2357
|
+
let out = "";
|
|
2358
|
+
for (let i = 0; i < body.length; i++) {
|
|
2359
|
+
if (body[i] !== "'") {
|
|
2360
|
+
out += body[i];
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
if (body[i + 1] === "'") {
|
|
2364
|
+
out += "'";
|
|
2365
|
+
i++;
|
|
2366
|
+
continue;
|
|
2367
|
+
}
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
return out;
|
|
2371
|
+
}
|
|
2200
2372
|
//#endregion
|
|
2201
2373
|
//#region ../common/src/util/policy/securityRuleToConditions.ts
|
|
2202
2374
|
/**
|
|
@@ -2333,8 +2505,147 @@ function schemaOf(collection) {
|
|
|
2333
2505
|
}
|
|
2334
2506
|
function resolveColumnName(propName, collection) {
|
|
2335
2507
|
const prop = collection?.properties?.[propName];
|
|
2336
|
-
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
2337
|
-
return toSnakeCase(propName);
|
|
2508
|
+
if (prop && "columnName" in prop && typeof prop.columnName === "string") return quoteColumnIdentifier(prop.columnName);
|
|
2509
|
+
return quoteColumnIdentifier(toSnakeCase(propName));
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Every PostgreSQL keyword that cannot stand as a bare column reference.
|
|
2513
|
+
* Appendix C's two reserved categories — plain "reserved", and "reserved (can
|
|
2514
|
+
* be function or type name)" — since neither may name a column unquoted.
|
|
2515
|
+
*/
|
|
2516
|
+
var RESERVED_SQL_WORDS = /* @__PURE__ */ new Set([
|
|
2517
|
+
"all",
|
|
2518
|
+
"analyse",
|
|
2519
|
+
"analyze",
|
|
2520
|
+
"and",
|
|
2521
|
+
"any",
|
|
2522
|
+
"array",
|
|
2523
|
+
"as",
|
|
2524
|
+
"asc",
|
|
2525
|
+
"asymmetric",
|
|
2526
|
+
"authorization",
|
|
2527
|
+
"binary",
|
|
2528
|
+
"both",
|
|
2529
|
+
"case",
|
|
2530
|
+
"cast",
|
|
2531
|
+
"check",
|
|
2532
|
+
"collate",
|
|
2533
|
+
"collation",
|
|
2534
|
+
"column",
|
|
2535
|
+
"concurrently",
|
|
2536
|
+
"constraint",
|
|
2537
|
+
"create",
|
|
2538
|
+
"cross",
|
|
2539
|
+
"current_catalog",
|
|
2540
|
+
"current_date",
|
|
2541
|
+
"current_role",
|
|
2542
|
+
"current_schema",
|
|
2543
|
+
"current_time",
|
|
2544
|
+
"current_timestamp",
|
|
2545
|
+
"current_user",
|
|
2546
|
+
"default",
|
|
2547
|
+
"deferrable",
|
|
2548
|
+
"desc",
|
|
2549
|
+
"distinct",
|
|
2550
|
+
"do",
|
|
2551
|
+
"else",
|
|
2552
|
+
"end",
|
|
2553
|
+
"except",
|
|
2554
|
+
"false",
|
|
2555
|
+
"fetch",
|
|
2556
|
+
"for",
|
|
2557
|
+
"foreign",
|
|
2558
|
+
"freeze",
|
|
2559
|
+
"from",
|
|
2560
|
+
"full",
|
|
2561
|
+
"grant",
|
|
2562
|
+
"group",
|
|
2563
|
+
"having",
|
|
2564
|
+
"ilike",
|
|
2565
|
+
"in",
|
|
2566
|
+
"initially",
|
|
2567
|
+
"inner",
|
|
2568
|
+
"intersect",
|
|
2569
|
+
"into",
|
|
2570
|
+
"is",
|
|
2571
|
+
"isnull",
|
|
2572
|
+
"join",
|
|
2573
|
+
"lateral",
|
|
2574
|
+
"leading",
|
|
2575
|
+
"left",
|
|
2576
|
+
"like",
|
|
2577
|
+
"limit",
|
|
2578
|
+
"localtime",
|
|
2579
|
+
"localtimestamp",
|
|
2580
|
+
"natural",
|
|
2581
|
+
"not",
|
|
2582
|
+
"notnull",
|
|
2583
|
+
"null",
|
|
2584
|
+
"offset",
|
|
2585
|
+
"on",
|
|
2586
|
+
"only",
|
|
2587
|
+
"or",
|
|
2588
|
+
"order",
|
|
2589
|
+
"outer",
|
|
2590
|
+
"overlaps",
|
|
2591
|
+
"placing",
|
|
2592
|
+
"primary",
|
|
2593
|
+
"references",
|
|
2594
|
+
"returning",
|
|
2595
|
+
"right",
|
|
2596
|
+
"select",
|
|
2597
|
+
"session_user",
|
|
2598
|
+
"similar",
|
|
2599
|
+
"some",
|
|
2600
|
+
"symmetric",
|
|
2601
|
+
"system_user",
|
|
2602
|
+
"table",
|
|
2603
|
+
"tablesample",
|
|
2604
|
+
"then",
|
|
2605
|
+
"to",
|
|
2606
|
+
"trailing",
|
|
2607
|
+
"true",
|
|
2608
|
+
"union",
|
|
2609
|
+
"unique",
|
|
2610
|
+
"user",
|
|
2611
|
+
"using",
|
|
2612
|
+
"variadic",
|
|
2613
|
+
"verbose",
|
|
2614
|
+
"when",
|
|
2615
|
+
"where",
|
|
2616
|
+
"window",
|
|
2617
|
+
"with"
|
|
2618
|
+
]);
|
|
2619
|
+
/** An identifier Postgres reads back unchanged without quotes. */
|
|
2620
|
+
var BARE_IDENTIFIER = /^[a-z_][a-z0-9_$]*$/;
|
|
2621
|
+
/**
|
|
2622
|
+
* Quote a column reference when Postgres would not read the bare name as that
|
|
2623
|
+
* column — and only then.
|
|
2624
|
+
*
|
|
2625
|
+
* Three ways a bare name goes wrong, in ascending order of how long it takes to
|
|
2626
|
+
* notice:
|
|
2627
|
+
*
|
|
2628
|
+
* - **Case.** `columnName` is used verbatim, and `rebase schema introspect`
|
|
2629
|
+
* populates it from a live database, so a legacy `"createdAt"` column arrives
|
|
2630
|
+
* spelled exactly that way. Unquoted, Postgres folds it to `createdat` and
|
|
2631
|
+
* `CREATE POLICY` fails with "column does not exist" — the collection keeps
|
|
2632
|
+
* RLS enabled with no policy, which denies every row.
|
|
2633
|
+
* - **Syntax.** A column named `order` or `default` is a syntax error mid-clause.
|
|
2634
|
+
* - **Silent rebinding.** `user`, `current_user`, `session_user`, `current_date`
|
|
2635
|
+
* and friends are *valid bare expressions*, so the policy compiles, applies,
|
|
2636
|
+
* and is reported as a success — while comparing against the connected role
|
|
2637
|
+
* or the wall clock instead of the column. Under RLS every request runs as the
|
|
2638
|
+
* same `rebase_user` role, so `USING (user = rebase.uid())` is a constant: it
|
|
2639
|
+
* denies everything, and its negation admits everything.
|
|
2640
|
+
*
|
|
2641
|
+
* Only the names that need it are quoted, so an ordinary snake_case policy body
|
|
2642
|
+
* is emitted byte-for-byte as before. That keeps generated artifacts and the
|
|
2643
|
+
* policies already stored in shipped databases stable — this fix reaches the
|
|
2644
|
+
* clauses that were broken and no others.
|
|
2645
|
+
*/
|
|
2646
|
+
function quoteColumnIdentifier(name) {
|
|
2647
|
+
if (BARE_IDENTIFIER.test(name) && !RESERVED_SQL_WORDS.has(name)) return name;
|
|
2648
|
+
return `"${name.replace(/"/g, "\"\"")}"`;
|
|
2338
2649
|
}
|
|
2339
2650
|
function quoteLiteral(value) {
|
|
2340
2651
|
if (value === null) return "NULL";
|
|
@@ -2354,8 +2665,14 @@ function rolesArraySql(roles) {
|
|
|
2354
2665
|
* Rebase's enforcement model is unified: authenticated (user-context) requests
|
|
2355
2666
|
* run under the restricted `rebase_user` role, so Postgres RLS binds *every*
|
|
2356
2667
|
* statement — reads and writes. A collection's `securityRules` are the whole
|
|
2357
|
-
* authorization model. The server context (auth flows, migrations,
|
|
2358
|
-
* `
|
|
2668
|
+
* authorization model. The server context (auth flows, migrations, raw
|
|
2669
|
+
* `rebase.sql`) runs as the owner and bypasses RLS.
|
|
2670
|
+
*
|
|
2671
|
+
* `rebase.dataAsAdmin` is **not** in that set, despite the name: it is scoped as
|
|
2672
|
+
* `{ uid: "service", roles: ["admin"] }`, so it runs as `rebase_user` like any
|
|
2673
|
+
* other caller and clears the baseline below through the *admin* arm, not the
|
|
2674
|
+
* server arm. Which is why `disableDefaultPolicies` plus a lone
|
|
2675
|
+
* `policy.serverContext()` rule locks it out too.
|
|
2359
2676
|
*
|
|
2360
2677
|
* Because RLS default-denies, every collection is **locked by default**: with
|
|
2361
2678
|
* no rules, only the server context and admins can touch it. The generator
|
|
@@ -2413,11 +2730,28 @@ function getIdPropertyName$1(collection) {
|
|
|
2413
2730
|
*
|
|
2414
2731
|
* Collections that opt out via `disableDefaultPolicies` are returned unchanged.
|
|
2415
2732
|
*/
|
|
2733
|
+
/**
|
|
2734
|
+
* The restrictive write gate for an auth collection.
|
|
2735
|
+
*
|
|
2736
|
+
* Restrictive, so it is ANDed with everything else: whatever an author's
|
|
2737
|
+
* permissive rules allow, a write to this table still has to satisfy this too.
|
|
2738
|
+
* It is the only thing standing between "users may edit their own row" and
|
|
2739
|
+
* "users may grant themselves any role".
|
|
2740
|
+
*/
|
|
2741
|
+
function adminWriteGate(tableName) {
|
|
2742
|
+
return {
|
|
2743
|
+
name: `${tableName}_require_admin_write`,
|
|
2744
|
+
mode: "restrictive",
|
|
2745
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
2746
|
+
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2747
|
+
check: SERVER_OR_ADMIN_EXPR$1
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2416
2750
|
function getEffectiveSecurityRules(collection) {
|
|
2417
2751
|
const explicit = [...collection.securityRules ?? []];
|
|
2418
|
-
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return explicit;
|
|
2419
2752
|
const tableName = getTableName(collection);
|
|
2420
2753
|
const injected = [];
|
|
2754
|
+
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return isAuthCollection$1(collection) ? [...explicit, adminWriteGate(tableName)] : explicit;
|
|
2421
2755
|
injected.push({
|
|
2422
2756
|
name: `${tableName}_default_admin_read`,
|
|
2423
2757
|
operations: ["select"],
|
|
@@ -2435,13 +2769,7 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2435
2769
|
operations: ["select"],
|
|
2436
2770
|
condition: policy.compare(policy.field(getIdPropertyName$1(collection)), "eq", policy.authUid())
|
|
2437
2771
|
});
|
|
2438
|
-
injected.push(
|
|
2439
|
-
name: `${tableName}_require_admin_write`,
|
|
2440
|
-
mode: "restrictive",
|
|
2441
|
-
operations: [...DEFAULT_GUARDED_OPS],
|
|
2442
|
-
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2443
|
-
check: SERVER_OR_ADMIN_EXPR$1
|
|
2444
|
-
});
|
|
2772
|
+
injected.push(adminWriteGate(tableName));
|
|
2445
2773
|
}
|
|
2446
2774
|
return [...explicit, ...injected];
|
|
2447
2775
|
}
|
|
@@ -3678,6 +4006,90 @@ var CollectionRegistry = class {
|
|
|
3678
4006
|
}
|
|
3679
4007
|
};
|
|
3680
4008
|
//#endregion
|
|
4009
|
+
//#region ../common/src/data/sort-dialect.ts
|
|
4010
|
+
/**
|
|
4011
|
+
* Sort-order wire codec.
|
|
4012
|
+
*
|
|
4013
|
+
* This is the ONLY module that knows about the colon-delimited wire format
|
|
4014
|
+
* (`"field:direction"`) used in HTTP query parameters, and about the JSON-array
|
|
4015
|
+
* form that carries a multi-column sort over the same parameter.
|
|
4016
|
+
* Everything else speaks {@link OrderByTuple} exclusively.
|
|
4017
|
+
*
|
|
4018
|
+
* Mirrors the filter architecture in `filter-dialect.ts`.
|
|
4019
|
+
*
|
|
4020
|
+
* @module
|
|
4021
|
+
*/
|
|
4022
|
+
/**
|
|
4023
|
+
* Collapse the one-key and many-key spellings of a sort into the list form.
|
|
4024
|
+
*
|
|
4025
|
+
* `["a", "desc"]` and `[["a", "desc"]]` mean the same thing and normalize to
|
|
4026
|
+
* the same value; the two are told apart by whether the first element is
|
|
4027
|
+
* itself an array, which no field name ever is.
|
|
4028
|
+
*
|
|
4029
|
+
* @returns The keys in order of significance, or `undefined` for no sort. An
|
|
4030
|
+
* empty list also returns `undefined` — "sort by nothing" is no sort, and
|
|
4031
|
+
* letting `[]` through would have every layer below re-deciding what it meant.
|
|
4032
|
+
*/
|
|
4033
|
+
function normalizeOrderBy(orderBy) {
|
|
4034
|
+
if (!orderBy || orderBy.length === 0) return void 0;
|
|
4035
|
+
const list = Array.isArray(orderBy[0]) ? orderBy : [orderBy];
|
|
4036
|
+
return list.length > 0 ? list : void 0;
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Collapse the driver-level `{orderBy, order}` pair into the list form.
|
|
4040
|
+
*
|
|
4041
|
+
* The driver contract spells a single-column sort as a field name plus a
|
|
4042
|
+
* separate direction, and a multi-column one as a list of tuples that leaves
|
|
4043
|
+
* `order` meaningless. Every driver reads both through here so neither
|
|
4044
|
+
* spelling has to be handled twice.
|
|
4045
|
+
*
|
|
4046
|
+
* An absent direction means ascending — the same thing a bare `?orderBy=name`
|
|
4047
|
+
* has always meant over HTTP. The Postgres driver used to read the same pair as
|
|
4048
|
+
* *descending* while Mongo read it as ascending, so one field name and no
|
|
4049
|
+
* direction described two different queries depending on which database was
|
|
4050
|
+
* underneath. Neither had a caller: every path in the workspace passes a
|
|
4051
|
+
* direction, which is why the disagreement went unnoticed rather than being
|
|
4052
|
+
* load-bearing.
|
|
4053
|
+
*/
|
|
4054
|
+
function normalizeDriverOrderBy(orderBy, order) {
|
|
4055
|
+
if (!orderBy) return void 0;
|
|
4056
|
+
if (typeof orderBy === "string") return [[orderBy, order === "desc" ? "desc" : "asc"]];
|
|
4057
|
+
return orderBy.length > 0 ? orderBy : void 0;
|
|
4058
|
+
}
|
|
4059
|
+
/** A sort whose *shape* is unusable, as opposed to one naming a field that does not exist. */
|
|
4060
|
+
var OrderBySpecError = class extends Error {
|
|
4061
|
+
code = "INVALID_ORDER_BY";
|
|
4062
|
+
constructor(detail) {
|
|
4063
|
+
super(`Invalid \`orderBy\`: ${detail}. Expected a field name, or a list of [field, direction] pairs like [["roles","asc"],["created_at","desc"]]`);
|
|
4064
|
+
this.name = "OrderBySpecError";
|
|
4065
|
+
}
|
|
4066
|
+
};
|
|
4067
|
+
/**
|
|
4068
|
+
* Validate an `orderBy` that arrived from outside this process — a WebSocket
|
|
4069
|
+
* subscribe frame, a driver call from untyped JavaScript — and return it in the
|
|
4070
|
+
* list form.
|
|
4071
|
+
*
|
|
4072
|
+
* Strict on purpose, in the same way the REST `parseOrderByParam` is: the
|
|
4073
|
+
* failure mode for a shape nobody checks is not a crash but a *silently
|
|
4074
|
+
* different query*. A malformed entry read as a field name resolves to no
|
|
4075
|
+
* column, and under the lenient unknown-field mode the sort is then dropped and
|
|
4076
|
+
* the rows come back in whatever order the database pleased — sorted, as far as
|
|
4077
|
+
* the subscriber can tell, by whatever they asked for.
|
|
4078
|
+
*/
|
|
4079
|
+
function parseOrderBySpecStrict(raw, order) {
|
|
4080
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
4081
|
+
if (typeof raw === "string") return normalizeDriverOrderBy(raw, order);
|
|
4082
|
+
if (!Array.isArray(raw) || raw.length === 0) throw new OrderBySpecError(`${typeof raw} is not a field name or a list of sort keys`);
|
|
4083
|
+
if (typeof raw[0] === "string") return [toStrictTuple(raw, 0)];
|
|
4084
|
+
return raw.map(toStrictTuple);
|
|
4085
|
+
}
|
|
4086
|
+
function toStrictTuple(raw, index) {
|
|
4087
|
+
if (!Array.isArray(raw) || typeof raw[0] !== "string" || raw[0].trim() === "") throw new OrderBySpecError(`entry ${index} has no field name`);
|
|
4088
|
+
const direction = raw[1];
|
|
4089
|
+
if (direction !== void 0 && direction !== "asc" && direction !== "desc") throw new OrderBySpecError(`entry ${index} has direction '${String(direction)}'`);
|
|
4090
|
+
return [raw[0], direction ?? "asc"];
|
|
4091
|
+
}
|
|
4092
|
+
//#endregion
|
|
3681
4093
|
//#region ../common/src/data/query_builder.ts
|
|
3682
4094
|
var QueryBuilder = class {
|
|
3683
4095
|
collection;
|
|
@@ -3706,11 +4118,18 @@ var QueryBuilder = class {
|
|
|
3706
4118
|
}
|
|
3707
4119
|
/**
|
|
3708
4120
|
* Order the results by a specific column.
|
|
4121
|
+
*
|
|
4122
|
+
* Called again, this adds a tie-breaker rather than replacing the sort:
|
|
4123
|
+
* keys apply in the order they were added.
|
|
4124
|
+
*
|
|
3709
4125
|
* @example
|
|
3710
4126
|
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
4127
|
+
* @example
|
|
4128
|
+
* client.collection('users').orderBy('roles').orderBy('createdAt', 'desc').find()
|
|
3711
4129
|
*/
|
|
3712
4130
|
orderBy(column, direction = "asc") {
|
|
3713
|
-
this.params.orderBy
|
|
4131
|
+
const existing = normalizeOrderBy(this.params.orderBy) ?? [];
|
|
4132
|
+
this.params.orderBy = [...existing, [column, direction]];
|
|
3714
4133
|
return this;
|
|
3715
4134
|
}
|
|
3716
4135
|
/**
|
|
@@ -3884,9 +4303,10 @@ async function* paginateFind(find, params, label = "collection") {
|
|
|
3884
4303
|
const requestedDirection = typeof cursor === "object" && cursor !== null ? cursor.direction : void 0;
|
|
3885
4304
|
let direction = "asc";
|
|
3886
4305
|
if (cursorField) {
|
|
3887
|
-
const orderBy = findParams.orderBy;
|
|
3888
|
-
if (orderBy && orderBy
|
|
3889
|
-
|
|
4306
|
+
const orderBy = normalizeOrderBy(findParams.orderBy);
|
|
4307
|
+
if (orderBy && orderBy.length > 1) throw new RebasePaginationError("cursor-order-mismatch", `Cannot seek on "${cursorField}" while ordering "${label}" by ${orderBy.map(([field]) => `"${field}"`).join(", ")}: keyset pagination advances along a single column. Order by "${cursorField}" alone, or drop the cursor and page by offset.`);
|
|
4308
|
+
if (orderBy && orderBy[0][0] !== cursorField) throw new RebasePaginationError("cursor-order-mismatch", `Cannot seek on "${cursorField}" while ordering "${label}" by "${orderBy[0][0]}": keyset pagination only advances along the column the query is sorted by. Order by "${cursorField}", or drop the cursor and page by offset.`);
|
|
4309
|
+
direction = requestedDirection ?? orderBy?.[0][1] ?? "asc";
|
|
3890
4310
|
findParams.orderBy = [cursorField, direction];
|
|
3891
4311
|
}
|
|
3892
4312
|
const seekOp = direction === "desc" ? "<" : ">";
|
|
@@ -3947,21 +4367,50 @@ async function collectAllPages(find, params, label = "collection") {
|
|
|
3947
4367
|
* metadata, so type coercion is the responsibility of the server-side data
|
|
3948
4368
|
* driver which has access to the collection schema.
|
|
3949
4369
|
*
|
|
3950
|
-
*
|
|
3951
|
-
*
|
|
4370
|
+
* Structural characters inside a value are backslash-escaped: `,` → `\,`,
|
|
4371
|
+
* `(` → `\(`, `)` → `\)`, and a literal backslash as `\\`. Decoding is
|
|
4372
|
+
* deliberately conservative — only those four sequences are decoded, so a
|
|
4373
|
+
* backslash that arrives unescaped from an older client survives intact.
|
|
3952
4374
|
*
|
|
3953
4375
|
* @module
|
|
3954
4376
|
*/
|
|
3955
4377
|
/**
|
|
3956
|
-
*
|
|
3957
|
-
*
|
|
4378
|
+
* Escape a value for the wire format: `\` → `\\`, `,` → `\,`, `(` → `\(`,
|
|
4379
|
+
* `)` → `\)`.
|
|
4380
|
+
*/
|
|
4381
|
+
/**
|
|
4382
|
+
* The wire spelling of an empty list.
|
|
4383
|
+
*
|
|
4384
|
+
* A lone backslash: unproducible by {@link escapeWireValue}, which doubles
|
|
4385
|
+
* every backslash it emits, so it cannot collide with any real item.
|
|
4386
|
+
*/
|
|
4387
|
+
var EMPTY_LIST_TOKEN = "\\";
|
|
4388
|
+
/**
|
|
4389
|
+
* Unescape a wire-format value.
|
|
4390
|
+
*
|
|
4391
|
+
* **Conservative**, and deliberately so: only the four sequences
|
|
4392
|
+
* {@link escapeWireValue} actually produces are decoded. A backslash followed
|
|
4393
|
+
* by anything else is left exactly as it is.
|
|
4394
|
+
*
|
|
4395
|
+
* This used to consume the backslash before *any* character, which is
|
|
4396
|
+
* indistinguishable for anything this codec emitted — it only ever emits those
|
|
4397
|
+
* four — but not for input arriving from elsewhere. A client on an older
|
|
4398
|
+
* release sends a Windows path or a LIKE pattern with a literal `C:\x`
|
|
4399
|
+
* unescaped, and greedy unescaping silently turned it into `C:x`, changing
|
|
4400
|
+
* which rows matched. Decoding only what the encoder can produce makes the two
|
|
4401
|
+
* directions agree across versions.
|
|
3958
4402
|
*/
|
|
3959
|
-
function
|
|
4403
|
+
function unescapeWireValue(value) {
|
|
3960
4404
|
let result = "";
|
|
3961
|
-
for (let i = 0; i < value.length; i++)
|
|
3962
|
-
|
|
3963
|
-
i
|
|
3964
|
-
|
|
4405
|
+
for (let i = 0; i < value.length; i++) {
|
|
4406
|
+
const next = value[i + 1];
|
|
4407
|
+
if (value[i] === "\\" && (next === "\\" || next === "," || next === "(" || next === ")")) {
|
|
4408
|
+
result += next;
|
|
4409
|
+
i++;
|
|
4410
|
+
continue;
|
|
4411
|
+
}
|
|
4412
|
+
result += value[i];
|
|
4413
|
+
}
|
|
3965
4414
|
return result;
|
|
3966
4415
|
}
|
|
3967
4416
|
/**
|
|
@@ -3979,13 +4428,205 @@ function splitListItems(inner) {
|
|
|
3979
4428
|
current += inner[i] + inner[i + 1];
|
|
3980
4429
|
i++;
|
|
3981
4430
|
} else if (inner[i] === ",") {
|
|
3982
|
-
items.push(
|
|
4431
|
+
items.push(unescapeWireValue(current));
|
|
3983
4432
|
current = "";
|
|
3984
4433
|
} else current += inner[i];
|
|
3985
|
-
items.push(
|
|
4434
|
+
items.push(unescapeWireValue(current));
|
|
3986
4435
|
return items;
|
|
3987
4436
|
}
|
|
3988
|
-
|
|
4437
|
+
/**
|
|
4438
|
+
* Operator tables as `Map`s, because the key comes off the wire.
|
|
4439
|
+
*
|
|
4440
|
+
* Indexed as plain objects, every `Object.prototype` member answered: a query
|
|
4441
|
+
* string of `?f=valueOf.x` found a truthy "operator" — the inherited function —
|
|
4442
|
+
* and `deserializeTuple` returned it *as the operator*, so a function object
|
|
4443
|
+
* travelled on into the compilers in place of a `WhereFilterOp`. The guard one
|
|
4444
|
+
* line below (`if (!canonicalOp)`) reads as though it rejects anything unknown,
|
|
4445
|
+
* and does not: `Object.prototype` is not unknown to a plain object.
|
|
4446
|
+
*
|
|
4447
|
+
* Same shape as the prototype-key defects swept out of `setIn`, `getIn`,
|
|
4448
|
+
* `mergeDeep`, `unflattenObject` and `FOREIGN_CONVENTION_UIDS`.
|
|
4449
|
+
*/
|
|
4450
|
+
var REST_OP_LOOKUP = new Map(Object.entries(REST_TO_CANONICAL));
|
|
4451
|
+
new Map(Object.entries(CANONICAL_TO_REST));
|
|
4452
|
+
/** The operator spellings a rejection lists back to the caller. */
|
|
4453
|
+
var VALID_OPERATOR_LIST = ALL_WHERE_FILTER_OPS.join(", ");
|
|
4454
|
+
/**
|
|
4455
|
+
* A filter condition named an operator this dialect does not have.
|
|
4456
|
+
*
|
|
4457
|
+
* ## Why this throws, rather than returning a typed rejection
|
|
4458
|
+
*
|
|
4459
|
+
* `deserializeFilter` is the *shared* codec: the REST ingress
|
|
4460
|
+
* (`packages/server/src/api/rest/query-parser.ts`), the browser SDK and the
|
|
4461
|
+
* admin panel (`buildRebaseData.ts`) all decode through it. Two constraints
|
|
4462
|
+
* follow.
|
|
4463
|
+
*
|
|
4464
|
+
* - It cannot throw the server's `ApiError`. `@rebasepro/common` does not
|
|
4465
|
+
* depend on `@rebasepro/server` (the dependency runs the other way), and a
|
|
4466
|
+
* browser client has no error handler to render an `ApiError` with. So the
|
|
4467
|
+
* rejection is this plain `Error` subclass, whose `message` reads correctly
|
|
4468
|
+
* wherever it surfaces — a rejected promise in an app, a 400 body over HTTP.
|
|
4469
|
+
* - It cannot be a returned rejection *value*. Every caller assigns the result
|
|
4470
|
+
* straight into a query it is about to run; a sentinel that none of them
|
|
4471
|
+
* check would be ignored, which is exactly the silently-wrong-filter failure
|
|
4472
|
+
* this exists to stop. Throwing is also what this file already does for the
|
|
4473
|
+
* sibling cases — `serializeTuple` on an unknown canonical operator,
|
|
4474
|
+
* `deserializeLogicalCondition` past the nesting bound — and the REST parser
|
|
4475
|
+
* already converts the latter into a 400.
|
|
4476
|
+
*
|
|
4477
|
+
* `statusCode`, `code` and `details` are carried as fields because the server's
|
|
4478
|
+
* Hono error handler duck-types those off any thrown error: a decode path that
|
|
4479
|
+
* forgets to convert still answers 400 with the canonical envelope instead of a
|
|
4480
|
+
* 500 that says "An unexpected error occurred". `query-parser.ts` converts
|
|
4481
|
+
* explicitly all the same — that is the path the contract is stated on, and an
|
|
4482
|
+
* incidental 400 is not a contract.
|
|
4483
|
+
*/
|
|
4484
|
+
var UnknownFilterOperatorError = class extends Error {
|
|
4485
|
+
/** The field the condition was written against. */
|
|
4486
|
+
field;
|
|
4487
|
+
/** The operator string as it arrived, verbatim. */
|
|
4488
|
+
operator;
|
|
4489
|
+
/** Every operator this dialect accepts, in canonical spelling. */
|
|
4490
|
+
validOperators = ALL_WHERE_FILTER_OPS;
|
|
4491
|
+
/** See the class docblock: read by the server's error handler. */
|
|
4492
|
+
statusCode = 400;
|
|
4493
|
+
code = "UNKNOWN_FILTER_OPERATOR";
|
|
4494
|
+
details;
|
|
4495
|
+
constructor(field, operator) {
|
|
4496
|
+
super(`Unknown filter operator '${operator}' on field '${field}'. Valid operators: ${VALID_OPERATOR_LIST}`);
|
|
4497
|
+
this.name = "UnknownFilterOperatorError";
|
|
4498
|
+
this.field = field;
|
|
4499
|
+
this.operator = operator;
|
|
4500
|
+
this.details = {
|
|
4501
|
+
field,
|
|
4502
|
+
operator,
|
|
4503
|
+
validOperators: ALL_WHERE_FILTER_OPS
|
|
4504
|
+
};
|
|
4505
|
+
}
|
|
4506
|
+
};
|
|
4507
|
+
/**
|
|
4508
|
+
* Two to three characters of ASCII punctuation and nothing else — the shape
|
|
4509
|
+
* every symbolic operator has (`==`, `>=`, `<>`, `~~`, `!!`, `>>`, `===`), and
|
|
4510
|
+
* one a column value effectively never has.
|
|
4511
|
+
*
|
|
4512
|
+
* Two characters minimum on purpose. A *single* punctuation character is a
|
|
4513
|
+
* perfectly ordinary value — `{ grade: ["-", "+"] }` is a two-item list, not a
|
|
4514
|
+
* condition — and the only single-character operator anyone actually mistypes
|
|
4515
|
+
* is `=`, which is named separately below. `<` and `>` need no special case:
|
|
4516
|
+
* they are real operators and resolve.
|
|
4517
|
+
*/
|
|
4518
|
+
var SYMBOLIC_OPERATOR = /^[^\p{L}\p{N}\s]{2,3}$/u;
|
|
4519
|
+
/** Lowercase, strip everything that is not a letter or digit. */
|
|
4520
|
+
function normalizeOperatorName(op) {
|
|
4521
|
+
return op.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4522
|
+
}
|
|
4523
|
+
/**
|
|
4524
|
+
* Every real operator name with its case and separators removed, so a
|
|
4525
|
+
* respelling of one — `arrayContains`, `not_in`, `NOT-LIKE`, `isNull` — is
|
|
4526
|
+
* recognised as an attempt at an operator rather than read as a value.
|
|
4527
|
+
*
|
|
4528
|
+
* These are rejected rather than accepted: admitting a second spelling of an
|
|
4529
|
+
* operator would leave two wire spellings of one thing, and the rejection
|
|
4530
|
+
* message names the one that works.
|
|
4531
|
+
*/
|
|
4532
|
+
var RESPELLED_OPERATORS = new Set([...ALL_WHERE_FILTER_OPS, ...Object.keys(REST_TO_CANONICAL)].map(normalizeOperatorName));
|
|
4533
|
+
/**
|
|
4534
|
+
* Operator names *other* query dialects use, which this one does not have.
|
|
4535
|
+
*
|
|
4536
|
+
* This list is curated, and deliberately so. For a word-shaped string there is
|
|
4537
|
+
* no rule that separates "an operator the caller guessed" from "a value that
|
|
4538
|
+
* happens to be a word": `{ tags: ["a", "b"] }` has to keep meaning a two-item
|
|
4539
|
+
* `in` list, so the codec cannot simply refuse every unrecognised word in
|
|
4540
|
+
* position 0. The line is therefore drawn by name, and only around names whose
|
|
4541
|
+
* use as an operator is far more likely than their use as one of two sibling
|
|
4542
|
+
* values. `contains` is the motivating case — the first thing a developer
|
|
4543
|
+
* reaches for, and until now it compiled to `title IN ('contains', 'Hell')`.
|
|
4544
|
+
*
|
|
4545
|
+
* Genuinely ambiguous single words (`any`, `all`, `exists`, `search`, `not`)
|
|
4546
|
+
* are left off: as operators they are rare, and as enum values they are common.
|
|
4547
|
+
* Everywhere else the tie goes to *rejecting*, because a 400 naming the
|
|
4548
|
+
* supported set costs the caller one round trip, and the alternative — which is
|
|
4549
|
+
* what every name on this list used to produce — is a query that runs, returns
|
|
4550
|
+
* rows, and is wrong.
|
|
4551
|
+
*/
|
|
4552
|
+
var NEAR_MISS_OPERATORS = /* @__PURE__ */ new Set([
|
|
4553
|
+
"contains",
|
|
4554
|
+
"notcontains",
|
|
4555
|
+
"doesnotcontain",
|
|
4556
|
+
"doesnotcontains",
|
|
4557
|
+
"includes",
|
|
4558
|
+
"notincludes",
|
|
4559
|
+
"startswith",
|
|
4560
|
+
"notstartswith",
|
|
4561
|
+
"beginswith",
|
|
4562
|
+
"startingwith",
|
|
4563
|
+
"endswith",
|
|
4564
|
+
"notendswith",
|
|
4565
|
+
"matches",
|
|
4566
|
+
"notmatches",
|
|
4567
|
+
"regex",
|
|
4568
|
+
"regexp",
|
|
4569
|
+
"between",
|
|
4570
|
+
"notbetween",
|
|
4571
|
+
"equals",
|
|
4572
|
+
"notequals",
|
|
4573
|
+
"equalto",
|
|
4574
|
+
"isequalto",
|
|
4575
|
+
"isnotequalto",
|
|
4576
|
+
"greaterthan",
|
|
4577
|
+
"greaterthanorequal",
|
|
4578
|
+
"greaterthanorequalto",
|
|
4579
|
+
"lessthan",
|
|
4580
|
+
"lessthanorequal",
|
|
4581
|
+
"lessthanorequalto",
|
|
4582
|
+
"isempty",
|
|
4583
|
+
"isnotempty",
|
|
4584
|
+
"oneof",
|
|
4585
|
+
"noneof",
|
|
4586
|
+
"anyof",
|
|
4587
|
+
"allof",
|
|
4588
|
+
"null",
|
|
4589
|
+
"isnullorempty"
|
|
4590
|
+
]);
|
|
4591
|
+
/**
|
|
4592
|
+
* Was this string *meant* as an operator?
|
|
4593
|
+
*
|
|
4594
|
+
* Only consulted after {@link toCanonicalOp} has already failed to resolve it,
|
|
4595
|
+
* so a `true` here is always a rejection.
|
|
4596
|
+
*/
|
|
4597
|
+
function isOperatorShaped(op) {
|
|
4598
|
+
if (op === "=") return true;
|
|
4599
|
+
if (SYMBOLIC_OPERATOR.test(op)) return true;
|
|
4600
|
+
const normalized = normalizeOperatorName(op);
|
|
4601
|
+
if (!normalized) return false;
|
|
4602
|
+
return RESPELLED_OPERATORS.has(normalized) || NEAR_MISS_OPERATORS.has(normalized);
|
|
4603
|
+
}
|
|
4604
|
+
/**
|
|
4605
|
+
* Read a `[op, value]` tuple, if that is what this is.
|
|
4606
|
+
*
|
|
4607
|
+
* Three outcomes, and the middle one is the defect this function exists for:
|
|
4608
|
+
*
|
|
4609
|
+
* - the operator resolves (canonical *or* REST spelling) → the canonical tuple;
|
|
4610
|
+
* - the operator does not resolve but was plainly meant as one → throw;
|
|
4611
|
+
* - it does not look like an operator at all → `undefined`, and the caller
|
|
4612
|
+
* falls back to reading the array as a list of values.
|
|
4613
|
+
*
|
|
4614
|
+
* The old test was `toCanonicalOp(raw[0]) === raw[0]`, i.e. canonical spelling
|
|
4615
|
+
* only, with *everything else* — including every REST short-code — dropping
|
|
4616
|
+
* through to `["in", raw]`. So the operator string itself became a value in a
|
|
4617
|
+
* membership test: `["!!", "Hello"]` compiled to `title IN ('!!','Hello')`,
|
|
4618
|
+
* which matches, and the caller got back rows their filter was written to
|
|
4619
|
+
* exclude. `["eq", "active"]` had the same shape of failure.
|
|
4620
|
+
*/
|
|
4621
|
+
function readTuple(field, raw) {
|
|
4622
|
+
if (!Array.isArray(raw) || raw.length !== 2) return void 0;
|
|
4623
|
+
const [op, value] = raw;
|
|
4624
|
+
if (typeof op !== "string") return void 0;
|
|
4625
|
+
const canonical = toCanonicalOp(op);
|
|
4626
|
+
if (canonical) return [canonical, value];
|
|
4627
|
+
if (op.includes(".")) return void 0;
|
|
4628
|
+
if (isOperatorShaped(op)) throw new UnknownFilterOperatorError(field, op);
|
|
4629
|
+
}
|
|
3989
4630
|
/**
|
|
3990
4631
|
* Parse a single PostgREST dot-string into a `[WhereFilterOp, unknown]` tuple.
|
|
3991
4632
|
*
|
|
@@ -4002,10 +4643,13 @@ function deserializeSingle(raw) {
|
|
|
4002
4643
|
if (dotIndex === -1) return ["==", raw];
|
|
4003
4644
|
const prefix = raw.substring(0, dotIndex);
|
|
4004
4645
|
const rest = raw.substring(dotIndex + 1);
|
|
4005
|
-
const canonicalOp = REST_OP_LOOKUP
|
|
4646
|
+
const canonicalOp = REST_OP_LOOKUP.get(prefix);
|
|
4006
4647
|
if (!canonicalOp) return ["==", raw];
|
|
4007
4648
|
if (NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
|
|
4008
|
-
if (rest.startsWith("(") && rest.endsWith(")"))
|
|
4649
|
+
if (rest.startsWith("(") && rest.endsWith(")")) {
|
|
4650
|
+
const inner = rest.slice(1, -1);
|
|
4651
|
+
return [canonicalOp, inner === EMPTY_LIST_TOKEN ? [] : splitListItems(inner)];
|
|
4652
|
+
}
|
|
4009
4653
|
return [canonicalOp, rest];
|
|
4010
4654
|
}
|
|
4011
4655
|
/**
|
|
@@ -4020,20 +4664,27 @@ function deserializeSingle(raw) {
|
|
|
4020
4664
|
*
|
|
4021
4665
|
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
4022
4666
|
* // → { age: [[">=", "18"], ["<", "65"]] }
|
|
4667
|
+
*
|
|
4668
|
+
* @throws {UnknownFilterOperatorError} when a condition names an operator this
|
|
4669
|
+
* dialect does not have. See that class for why a rejection here is a throw.
|
|
4023
4670
|
*/
|
|
4024
4671
|
function deserializeFilter(query) {
|
|
4025
4672
|
const result = {};
|
|
4026
4673
|
for (const [field, raw] of Object.entries(query)) {
|
|
4027
4674
|
if (raw === void 0) continue;
|
|
4028
|
-
|
|
4029
|
-
|
|
4675
|
+
const tuple = readTuple(field, raw);
|
|
4676
|
+
if (tuple) {
|
|
4677
|
+
result[field] = tuple;
|
|
4030
4678
|
continue;
|
|
4031
4679
|
}
|
|
4032
4680
|
if (Array.isArray(raw)) {
|
|
4033
4681
|
if (raw.length === 0) continue;
|
|
4034
|
-
if (Array.isArray(raw[0])
|
|
4035
|
-
|
|
4036
|
-
|
|
4682
|
+
if (Array.isArray(raw[0])) {
|
|
4683
|
+
const tuples = raw.map((item) => readTuple(field, item));
|
|
4684
|
+
if (tuples.every((t) => t !== void 0)) {
|
|
4685
|
+
result[field] = tuples;
|
|
4686
|
+
continue;
|
|
4687
|
+
}
|
|
4037
4688
|
}
|
|
4038
4689
|
if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
|
|
4039
4690
|
else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
|
|
@@ -4132,8 +4783,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4132
4783
|
logical: params?.logical,
|
|
4133
4784
|
limit,
|
|
4134
4785
|
offset: driverOffset,
|
|
4135
|
-
orderBy: params?.orderBy
|
|
4136
|
-
order: params?.orderBy?.[1],
|
|
4786
|
+
orderBy: normalizeOrderBy(params?.orderBy),
|
|
4137
4787
|
searchString: params?.searchString
|
|
4138
4788
|
}, params?.include) : await driver.fetchCollection({
|
|
4139
4789
|
path: slug,
|
|
@@ -4141,8 +4791,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4141
4791
|
offset: driverOffset,
|
|
4142
4792
|
filter,
|
|
4143
4793
|
logical: params?.logical,
|
|
4144
|
-
orderBy: params?.orderBy
|
|
4145
|
-
order: params?.orderBy?.[1],
|
|
4794
|
+
orderBy: normalizeOrderBy(params?.orderBy),
|
|
4146
4795
|
searchString: params?.searchString
|
|
4147
4796
|
});
|
|
4148
4797
|
let total = rows.length + offset;
|
|
@@ -4237,8 +4886,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4237
4886
|
offset: driverOffset,
|
|
4238
4887
|
filter: params?.where,
|
|
4239
4888
|
logical: params?.logical,
|
|
4240
|
-
orderBy: params?.orderBy
|
|
4241
|
-
order: params?.orderBy?.[1],
|
|
4889
|
+
orderBy: normalizeOrderBy(params?.orderBy),
|
|
4242
4890
|
searchString: params?.searchString,
|
|
4243
4891
|
searchExplain: params?.searchExplain,
|
|
4244
4892
|
onUpdate: (entities) => {
|
|
@@ -4358,8 +5006,10 @@ var SdkQueryBuilder = class {
|
|
|
4358
5006
|
}
|
|
4359
5007
|
return this;
|
|
4360
5008
|
}
|
|
5009
|
+
/** Called again, this adds a tie-breaker rather than replacing the sort. */
|
|
4361
5010
|
orderBy(column, direction = "asc") {
|
|
4362
|
-
this.params.orderBy
|
|
5011
|
+
const existing = normalizeOrderBy(this.params.orderBy) ?? [];
|
|
5012
|
+
this.params.orderBy = [...existing, [column, direction]];
|
|
4363
5013
|
return this;
|
|
4364
5014
|
}
|
|
4365
5015
|
limit(count) {
|
|
@@ -4792,6 +5442,48 @@ var searchIndexStatements = (spec) => {
|
|
|
4792
5442
|
return statements;
|
|
4793
5443
|
};
|
|
4794
5444
|
/**
|
|
5445
|
+
* Marker on the comment of every generated search column this module creates.
|
|
5446
|
+
*
|
|
5447
|
+
* Versioned because the fingerprint below is only comparable against itself: a
|
|
5448
|
+
* future change to how it is computed has to read as "not stamped by this
|
|
5449
|
+
* version" rather than as drift on every existing column.
|
|
5450
|
+
*/
|
|
5451
|
+
var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
|
|
5452
|
+
/**
|
|
5453
|
+
* A stable fingerprint of one generated column's expression.
|
|
5454
|
+
*
|
|
5455
|
+
* Why a stamp rather than reading the expression back: Postgres stores a
|
|
5456
|
+
* generated column's expression *parsed*, and hands it back deparsed — casts
|
|
5457
|
+
* made explicit, identifiers requoted, schema qualifications added or dropped
|
|
5458
|
+
* according to `search_path`. Comparing that text to the text we generated
|
|
5459
|
+
* would report drift on wording, and this comparison decides whether a boot
|
|
5460
|
+
* refuses, so a false positive is an outage. The stamp is written by the same
|
|
5461
|
+
* code that writes the column, so equality means what it says.
|
|
5462
|
+
*/
|
|
5463
|
+
var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
|
|
5464
|
+
/**
|
|
5465
|
+
* The stamps for a spec's generated columns — one per column, never shared.
|
|
5466
|
+
*
|
|
5467
|
+
* Per column on purpose: turning `fuzzy` on adds a second column and changes
|
|
5468
|
+
* nothing about the first, and a spec-wide fingerprint would report the
|
|
5469
|
+
* untouched `tsvector` column as drifted and refuse a boot over a change that
|
|
5470
|
+
* is purely additive.
|
|
5471
|
+
*/
|
|
5472
|
+
var searchColumnStamps = (spec) => {
|
|
5473
|
+
const stamp = (column, expression) => {
|
|
5474
|
+
const fingerprint = searchExpressionFingerprint(expression);
|
|
5475
|
+
return {
|
|
5476
|
+
column,
|
|
5477
|
+
expression,
|
|
5478
|
+
fingerprint,
|
|
5479
|
+
sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
|
|
5480
|
+
};
|
|
5481
|
+
};
|
|
5482
|
+
const stamps = [stamp(spec.column, spec.expression)];
|
|
5483
|
+
if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
|
|
5484
|
+
return stamps;
|
|
5485
|
+
};
|
|
5486
|
+
/**
|
|
4795
5487
|
* The generated column names a collection's search block adds, if any.
|
|
4796
5488
|
*
|
|
4797
5489
|
* These are physical columns on the table, so `SELECT *` returns them. They are
|
|
@@ -4968,6 +5660,6 @@ function isAuthCollection(collection) {
|
|
|
4968
5660
|
return typeof auth === "object" && auth !== null && auth.enabled === true;
|
|
4969
5661
|
}
|
|
4970
5662
|
//#endregion
|
|
4971
|
-
export {
|
|
5663
|
+
export { mergeDeep as $, fieldKeyForColumn as A, parseIdValues as B, getJunctionCollectionConfig as C, policyToPostgres as D, getEffectiveSecurityRules as E, getTableVarName as F, updateDateAutoValues as G, createRelationRef as H, resolveCollectionRelations as I, legacyForeignKeyName as J, firstFreeKey as K, buildCompositeId as L, getColumnName as M, getEnumVarName as N, securityRuleToConditions as O, getTableName as P, isPrototypePollutingKey as Q, getDeclaredPrimaryKeys as R, resolveStringColumnLength as S, resolveJunctionSpecs as T, createRelationRefWithData as U, sortCollectionsBySlug as V, normalizeToEntityRelation as W, toWireKey as X, toPostgresIdentifier as Y, getPolicyNamesForRule as Z, OrderBySpecError as _, SEARCH_STAMP_PREFIX as a, isManyToMany as at, CollectionRegistry as b, assertSearchIsPostgresOnly as c, searchColumnStamps as d, camelCase as et, searchExtensionStatements as f, buildSdkData as g, visibleColumnProjection as h, isAuthCollection as i, hasForeignKeyOnTarget as it, findRelation as j, findAnonymousGrants as k, buildSearchColumnSpec as l, searchIndexStatements as m, authUsersColumnDefinition as n, DEFAULT_ONE_OF_TYPE as nt, SEARCH_TEXT_FN as o, Vector as ot, searchHelperFunctions as p, generateForeignKeyName as q, authUsersColumnSql as r, DEFAULT_ONE_OF_VALUE as rt, SEARCH_UNACCENT_FN as s, AUTH_USERS_COLUMNS as t, toSnakeCase as tt, hiddenColumnsOption as u, normalizeDriverOrderBy as v, getJunctionSecurityRules as w, relationalCollections as x, parseOrderBySpecStrict as y, isAddressableId as z };
|
|
4972
5664
|
|
|
4973
|
-
//# sourceMappingURL=auth-users-columns-
|
|
5665
|
+
//# sourceMappingURL=auth-users-columns-C-FDnL_e.js.map
|