@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g249daa1
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 +48 -1
- package/dist/{src-DlPBctw_.js → auth-users-columns-Dt9g712t.js} +721 -47
- package/dist/auth-users-columns-Dt9g712t.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
- package/dist/cli-helpers.d.ts +57 -1
- package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-CwYUliAa.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-CwYUliAa.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DRxaUG96.js} +86 -15
- package/dist/ensure-collection-tables-DRxaUG96.js.map +1 -0
- package/dist/index.es.js +765 -223
- package/dist/index.es.js.map +1 -1
- package/dist/{policy-CeA1JcxP.js → policy-CPkCqVTz.js} +4 -4
- package/dist/policy-CPkCqVTz.js.map +1 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/ensure-collection-tables.d.ts +2 -2
- package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +199 -0
- package/dist/security/rls-enforcement.d.ts +53 -2
- package/dist/services/FetchService.d.ts +25 -7
- package/dist/services/dataService.d.ts +3 -0
- package/dist/services/realtimeService.d.ts +27 -21
- package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +71 -2
- package/dist/{websocket-B2LsrINK.js → websocket-D0TBU3ia.js} +3 -3
- package/dist/websocket-D0TBU3ia.js.map +1 -0
- package/package.json +9 -8
- package/src/PostgresBackendDriver.ts +165 -3
- package/src/PostgresBootstrapper.ts +41 -2
- package/src/auth/ensure-tables.ts +185 -86
- package/src/cli-helpers.ts +129 -10
- package/src/cli.ts +232 -30
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +31 -3
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor.ts +7 -5
- package/src/schema/ensure-collection-tables.ts +165 -20
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/generate-postgres-ddl-logic.ts +266 -15
- package/src/schema/generate-postgres-ddl.ts +25 -2
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-logic.ts +1 -1
- 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 +558 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/rls-enforcement.ts +141 -3
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +148 -108
- package/src/services/PersistService.ts +14 -1
- package/src/services/RelationService.ts +2 -1
- package/src/services/channel-history.ts +8 -0
- package/src/services/channel-presence.ts +6 -0
- package/src/services/dataService.ts +3 -0
- package/src/services/realtimeService.ts +46 -37
- package/src/utils/drizzle-conditions.ts +223 -2
- package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-DlPBctw_.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-B2LsrINK.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -2,8 +2,8 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { l as __require, s as __commonJSMin } from "./connection-BuZ97wsr.js";
|
|
5
|
-
import { a as
|
|
6
|
-
import { n as ANONYMOUS_USER_IDS, r as policy, t as ANONYMOUS_USER_ID } from "./policy-
|
|
5
|
+
import { a as rewriteLegacyRlsFunctions, c as isPostgresCollectionConfig, d as getDataSourceCapabilities, f as NULL_OPS, i as RLS_UID_SQL, l as isRelationalCollectionConfig, m as toCanonicalOp, p as REST_TO_CANONICAL, r as RLS_ROLES_SQL, s as getDeclaredSubcollections } from "./src-C_wvdMnl.js";
|
|
6
|
+
import { n as ANONYMOUS_USER_IDS, r as policy, t as ANONYMOUS_USER_ID } from "./policy-CPkCqVTz.js";
|
|
7
7
|
//#region ../types/src/types/entities.ts
|
|
8
8
|
/**
|
|
9
9
|
* Class used to create a reference to a entity in a different path
|
|
@@ -55,6 +55,25 @@ function hasForeignKeyOnTarget(relation) {
|
|
|
55
55
|
function isManyToMany(relation) {
|
|
56
56
|
return relation.kind === "manyToMany";
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
60
|
+
*
|
|
61
|
+
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
62
|
+
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
63
|
+
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
64
|
+
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
65
|
+
*
|
|
66
|
+
* The return is never `undefined` — no ingress that routes its client limit
|
|
67
|
+
* through this can produce an unbounded read.
|
|
68
|
+
*/
|
|
69
|
+
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
70
|
+
const maxLimit = opts.maxLimit ?? 1e3;
|
|
71
|
+
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
72
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
73
|
+
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
74
|
+
}
|
|
75
|
+
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
76
|
+
}
|
|
58
77
|
//#endregion
|
|
59
78
|
//#region ../common/src/util/common.ts
|
|
60
79
|
var DEFAULT_ONE_OF_TYPE = "type";
|
|
@@ -1007,6 +1026,35 @@ function camelCase(str) {
|
|
|
1007
1026
|
});
|
|
1008
1027
|
})))();
|
|
1009
1028
|
/**
|
|
1029
|
+
* Segments that reach the prototype chain rather than a property of the object.
|
|
1030
|
+
*
|
|
1031
|
+
* The twin of this function in `@rebasepro/forms` could be made to write onto
|
|
1032
|
+
* `Object.prototype` through a path of `__proto__.x`. This copy survives the
|
|
1033
|
+
* write by accident — its `clone` always spreads into a fresh object, while the
|
|
1034
|
+
* form engine's has a "preserve class instances" branch that hands back
|
|
1035
|
+
* `Object.prototype` itself — but `getIn` still *reads* through the chain, and
|
|
1036
|
+
* handing back `Object.prototype` is how a polluted value is read out again.
|
|
1037
|
+
*
|
|
1038
|
+
* Closed on both sides here, so the two implementations agree.
|
|
1039
|
+
*/
|
|
1040
|
+
var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1041
|
+
"__proto__",
|
|
1042
|
+
"constructor",
|
|
1043
|
+
"prototype"
|
|
1044
|
+
]);
|
|
1045
|
+
/**
|
|
1046
|
+
* Whether writing this single key with `obj[key] = …` would reach the prototype
|
|
1047
|
+
* chain instead of creating a property.
|
|
1048
|
+
*
|
|
1049
|
+
* The single-key counterpart of {@link pathTraversesPrototype}, for the many
|
|
1050
|
+
* places that copy an object one key at a time. `JSON.parse` creates
|
|
1051
|
+
* `__proto__` as an *own* property, so it survives `hasOwnProperty` — and then
|
|
1052
|
+
* `target[key] = value` invokes the setter and replaces the target's prototype.
|
|
1053
|
+
*/
|
|
1054
|
+
function isPrototypePollutingKey(key) {
|
|
1055
|
+
return UNSAFE_PATH_SEGMENTS.has(key);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1010
1058
|
* Deep clone a value, preserving function references and class instances.
|
|
1011
1059
|
* Unlike structuredClone, this handles objects that contain functions
|
|
1012
1060
|
* (e.g. CollectionConfig with target(), childCollections(), callbacks).
|
|
@@ -1360,6 +1408,28 @@ function legacyForeignKeyName(name) {
|
|
|
1360
1408
|
const snake = toSnakeCase(name);
|
|
1361
1409
|
return `${snake.endsWith("s") ? snake.slice(0, -1) : snake}_id`;
|
|
1362
1410
|
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Truncate an identifier to what Postgres will actually store.
|
|
1413
|
+
*
|
|
1414
|
+
* Postgres silently truncates identifiers at NAMEDATALEN-1 = 63 **bytes**, so a
|
|
1415
|
+
* name generated longer than that is not the name the database ends up holding.
|
|
1416
|
+
* Anything that later looks the object up by the name it generated then misses.
|
|
1417
|
+
*
|
|
1418
|
+
* Byte length, not string length: NAMEDATALEN is a byte bound, and a multi-byte
|
|
1419
|
+
* character straddling the boundary would be cut mid-sequence by `slice(0, 63)`.
|
|
1420
|
+
*
|
|
1421
|
+
* `TextEncoder` rather than `Buffer`, which is not a matter of taste: `Buffer`
|
|
1422
|
+
* is a Node global, and this package is imported by browser-facing ones. It
|
|
1423
|
+
* typechecked only where `@types/node` happened to be in scope, so
|
|
1424
|
+
* `packages/codegen` — whose tsconfig is `lib: ["ESNext", "dom"]` — could not
|
|
1425
|
+
* compile the file at all, and both of its suites failed to run. `TextEncoder`
|
|
1426
|
+
* and `TextDecoder` are standard in both runtimes and need no ambient types.
|
|
1427
|
+
*/
|
|
1428
|
+
function toPostgresIdentifier(name) {
|
|
1429
|
+
const bytes = new TextEncoder().encode(name);
|
|
1430
|
+
if (bytes.byteLength <= 63) return name;
|
|
1431
|
+
return new TextDecoder("utf-8").decode(bytes.subarray(0, 63)).replace(/�+$/, "");
|
|
1432
|
+
}
|
|
1363
1433
|
//#endregion
|
|
1364
1434
|
//#region ../common/src/util/entities.ts
|
|
1365
1435
|
/**
|
|
@@ -1383,10 +1453,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
|
|
|
1383
1453
|
* have `id` and `path` fields — these are relation-shaped objects from
|
|
1384
1454
|
* edge cases in the data pipeline (REST fallback, stale cache, custom data source).
|
|
1385
1455
|
*
|
|
1456
|
+
* When `targetPath` is given, also accepts a bare id. A relation column is a
|
|
1457
|
+
* foreign key, and the REST layer returns it as the scalar it is; only some
|
|
1458
|
+
* fetch paths hydrate it into an object. Which form a caller sees therefore
|
|
1459
|
+
* depends on how the row was loaded, and a caller that only accepted objects
|
|
1460
|
+
* reported half of its own data as a type error. The declared target is the
|
|
1461
|
+
* missing half: with it, an id is a relation that has not been fetched yet.
|
|
1462
|
+
*
|
|
1386
1463
|
* Returns null if the value cannot be coerced.
|
|
1387
1464
|
*/
|
|
1388
|
-
function normalizeToEntityRelation(value, propertyType) {
|
|
1465
|
+
function normalizeToEntityRelation(value, propertyType, targetPath) {
|
|
1389
1466
|
if (value instanceof EntityRelation) return value;
|
|
1467
|
+
if (targetPath && (typeof value === "string" || typeof value === "number")) {
|
|
1468
|
+
if (value === "") return null;
|
|
1469
|
+
return new EntityRelation(value, targetPath);
|
|
1470
|
+
}
|
|
1390
1471
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1391
1472
|
const obj = value;
|
|
1392
1473
|
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
@@ -1924,9 +2005,9 @@ function isKeywordAt(upper, i, keyword) {
|
|
|
1924
2005
|
*
|
|
1925
2006
|
* This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
|
|
1926
2007
|
* `AND` inside
|
|
1927
|
-
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id =
|
|
2008
|
+
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
|
|
1928
2009
|
* split the expression, and re-emitting the halves produced
|
|
1929
|
-
* `(EXISTS (...) AND m.user_id =
|
|
2010
|
+
* `(EXISTS (...) AND m.user_id = rebase.uid())`
|
|
1930
2011
|
* where `m` is no longer in scope — SQL that Postgres rejects outright with
|
|
1931
2012
|
* "missing FROM-clause entry for table". Returning null instead keeps such a
|
|
1932
2013
|
* clause as a `raw` expression, which round-trips verbatim.
|
|
@@ -2000,15 +2081,15 @@ function stripOuterParens(sql) {
|
|
|
2000
2081
|
}
|
|
2001
2082
|
}
|
|
2002
2083
|
function sqlToPolicy(sql) {
|
|
2003
|
-
const trimmed = stripOuterParens(sql.trim());
|
|
2084
|
+
const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
|
|
2004
2085
|
if (trimmed.toLowerCase() === "true") return policy.true();
|
|
2005
2086
|
if (trimmed.toLowerCase() === "false") return policy.false();
|
|
2006
|
-
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
2087
|
+
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
|
|
2007
2088
|
if (overlapMatch) {
|
|
2008
2089
|
const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
2009
2090
|
return policy.rolesOverlap(roles);
|
|
2010
2091
|
}
|
|
2011
|
-
const containMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
2092
|
+
const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
|
|
2012
2093
|
if (containMatch) {
|
|
2013
2094
|
const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
2014
2095
|
return policy.rolesContain(roles);
|
|
@@ -2024,10 +2105,10 @@ function sqlToPolicy(sql) {
|
|
|
2024
2105
|
const right = parseOperand(rightStr.trim());
|
|
2025
2106
|
if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
|
|
2026
2107
|
}
|
|
2027
|
-
return policy.raw(
|
|
2108
|
+
return policy.raw(trimmed);
|
|
2028
2109
|
}
|
|
2029
2110
|
/**
|
|
2030
|
-
* Literals from other BaaS platforms that people compare `
|
|
2111
|
+
* Literals from other BaaS platforms that people compare `rebase.uid()` against
|
|
2031
2112
|
* out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
|
|
2032
2113
|
* `pgRoles`, one surface over: the same muscle memory inside a `using:` string
|
|
2033
2114
|
* is the more dangerous spelling, because it inverts a rule instead of
|
|
@@ -2038,18 +2119,25 @@ var FOREIGN_CONVENTION_UIDS = {
|
|
|
2038
2119
|
authenticated: "Supabase",
|
|
2039
2120
|
service_role: "Supabase"
|
|
2040
2121
|
};
|
|
2041
|
-
/**
|
|
2042
|
-
|
|
2122
|
+
/**
|
|
2123
|
+
* `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
|
|
2124
|
+
*
|
|
2125
|
+
* Both schema spellings, because this runs over policy bodies read back from a
|
|
2126
|
+
* database, and one migrated by a pre-1.0 release still holds `auth.uid()`.
|
|
2127
|
+
* A security check that stops recognising a dangerous clause because the
|
|
2128
|
+
* framework renamed a function is a check that silently turns off.
|
|
2129
|
+
*/
|
|
2130
|
+
var UID_NOT_NULL = /\b(?:rebase|auth)\.uid\(\)\s+IS\s+NOT\s+NULL/i;
|
|
2043
2131
|
/**
|
|
2044
2132
|
* Find clauses that read as "signed-in users only" but admit anonymous callers.
|
|
2045
2133
|
*
|
|
2046
|
-
* Both spellings come from the same place — Supabase, where `auth.uid()`
|
|
2047
|
-
* is NULL for an anonymous request. Rebase substitutes
|
|
2134
|
+
* Both spellings come from the same place — Supabase, where its own `auth.uid()`
|
|
2135
|
+
* really is NULL for an anonymous request. Rebase substitutes
|
|
2048
2136
|
* {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
|
|
2049
2137
|
* is how the trusted *server* context is recognised), so:
|
|
2050
2138
|
*
|
|
2051
|
-
* - `
|
|
2052
|
-
* - `
|
|
2139
|
+
* - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
|
|
2140
|
+
* - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
|
|
2053
2141
|
* other. This one is not hypothetical and was not only a foreign habit:
|
|
2054
2142
|
* rebase's own request path reported `'anon'` while everything that compiled
|
|
2055
2143
|
* or checked a policy used `'anonymous'`, so whichever literal an author
|
|
@@ -2081,7 +2169,7 @@ function findAnonymousGrants(expr) {
|
|
|
2081
2169
|
if (UID_NOT_NULL.test(e.sql)) found.push({
|
|
2082
2170
|
pattern: "uid-not-null",
|
|
2083
2171
|
detail: e.sql,
|
|
2084
|
-
explanation: `\`
|
|
2172
|
+
explanation: `\`rebase.uid() IS NOT NULL\` is true for every request that came from a client, including anonymous ones — they carry '${ANONYMOUS_USER_ID}', not NULL. Use \`condition: policy.authenticated()\` to mean "signed in".`
|
|
2085
2173
|
});
|
|
2086
2174
|
return;
|
|
2087
2175
|
case "compare": {
|
|
@@ -2103,7 +2191,7 @@ function findAnonymousGrants(expr) {
|
|
|
2103
2191
|
return found;
|
|
2104
2192
|
}
|
|
2105
2193
|
function parseOperand(str) {
|
|
2106
|
-
if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /
|
|
2194
|
+
if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /rebase\.uid\(\)/i.test(str)) return policy.authUid();
|
|
2107
2195
|
const stringMatch = str.match(/^'(.+)'$/);
|
|
2108
2196
|
if (stringMatch) return policy.literal(stringMatch[1]);
|
|
2109
2197
|
if (/^\w+$/.test(str)) return policy.field(str);
|
|
@@ -2185,12 +2273,12 @@ function compile(expr, scope) {
|
|
|
2185
2273
|
const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
|
|
2186
2274
|
return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
|
|
2187
2275
|
}
|
|
2188
|
-
case "rolesOverlap": return `string_to_array(
|
|
2189
|
-
case "rolesContain": return `string_to_array(
|
|
2190
|
-
case "authenticated": return
|
|
2191
|
-
case "serverContext": return
|
|
2276
|
+
case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
|
|
2277
|
+
case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
|
|
2278
|
+
case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
|
|
2279
|
+
case "serverContext": return `${RLS_UID_SQL} IS NULL`;
|
|
2192
2280
|
case "existsIn": return compileExistsIn(expr, scope);
|
|
2193
|
-
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
2281
|
+
case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
2194
2282
|
}
|
|
2195
2283
|
}
|
|
2196
2284
|
/**
|
|
@@ -2227,8 +2315,8 @@ function operandToSql(operand, scope) {
|
|
|
2227
2315
|
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
2228
2316
|
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
2229
2317
|
case "literal": return quoteLiteral(operand.value);
|
|
2230
|
-
case "authUid": return
|
|
2231
|
-
case "authRoles": return
|
|
2318
|
+
case "authUid": return RLS_UID_SQL;
|
|
2319
|
+
case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
|
|
2232
2320
|
}
|
|
2233
2321
|
}
|
|
2234
2322
|
/**
|
|
@@ -2308,7 +2396,7 @@ var DEFAULT_GUARDED_OPS = [
|
|
|
2308
2396
|
"delete"
|
|
2309
2397
|
];
|
|
2310
2398
|
/** Whether a collection is flagged as an authentication collection. */
|
|
2311
|
-
function isAuthCollection(collection) {
|
|
2399
|
+
function isAuthCollection$1(collection) {
|
|
2312
2400
|
const auth = collection.auth;
|
|
2313
2401
|
return auth === true || typeof auth === "object" && auth?.enabled === true;
|
|
2314
2402
|
}
|
|
@@ -2341,7 +2429,7 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2341
2429
|
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2342
2430
|
check: SERVER_OR_ADMIN_EXPR$1
|
|
2343
2431
|
});
|
|
2344
|
-
if (isAuthCollection(collection)) {
|
|
2432
|
+
if (isAuthCollection$1(collection)) {
|
|
2345
2433
|
injected.push({
|
|
2346
2434
|
name: `${tableName}_default_self_read`,
|
|
2347
2435
|
operations: ["select"],
|
|
@@ -3308,6 +3396,46 @@ function resolveDataSource(collection, registry) {
|
|
|
3308
3396
|
capabilities: getDataSourceCapabilities(engine)
|
|
3309
3397
|
};
|
|
3310
3398
|
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Does a SQL toolchain own this collection's storage?
|
|
3401
|
+
*
|
|
3402
|
+
* "Owns the storage" means: something generates a table for it, pushes that
|
|
3403
|
+
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
3404
|
+
* the two disagree. That is true of a Postgres collection and false of a
|
|
3405
|
+
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
3406
|
+
* migrates — and the two were never told apart. Every stage of the SQL
|
|
3407
|
+
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
3408
|
+
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
3409
|
+
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
3410
|
+
* `db push` include list — where its name shielding a same-named real table
|
|
3411
|
+
* from Atlas's exclude list is the one that can lose data.
|
|
3412
|
+
*
|
|
3413
|
+
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
3414
|
+
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
3415
|
+
* gets the same treatment as the built-in ones.
|
|
3416
|
+
*
|
|
3417
|
+
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
3418
|
+
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
3419
|
+
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
3420
|
+
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
3421
|
+
* including a collection generates a table nothing writes to; wrongly excluding
|
|
3422
|
+
* one silently stops generating a table the app is serving from. Declare
|
|
3423
|
+
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
3424
|
+
*/
|
|
3425
|
+
function isRelationalCollection(collection, registry) {
|
|
3426
|
+
return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* The subset of `collections` a SQL toolchain owns — see
|
|
3430
|
+
* {@link isRelationalCollection}.
|
|
3431
|
+
*
|
|
3432
|
+
* Every stage that generates SQL from collections starts by calling this, so
|
|
3433
|
+
* the rule lives in one place rather than being re-decided per generator. It
|
|
3434
|
+
* keeps the input order.
|
|
3435
|
+
*/
|
|
3436
|
+
function relationalCollections(collections, registry) {
|
|
3437
|
+
return collections.filter((collection) => isRelationalCollection(collection, registry));
|
|
3438
|
+
}
|
|
3311
3439
|
//#endregion
|
|
3312
3440
|
//#region ../common/src/collections/CollectionRegistry.ts
|
|
3313
3441
|
var CollectionRegistry = class {
|
|
@@ -3602,8 +3730,24 @@ var QueryBuilder = class {
|
|
|
3602
3730
|
/**
|
|
3603
3731
|
* Set a free-text search string if supported by the backend.
|
|
3604
3732
|
*/
|
|
3605
|
-
search(searchString) {
|
|
3733
|
+
search(searchString, options) {
|
|
3606
3734
|
this.params.searchString = searchString;
|
|
3735
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
3736
|
+
return this;
|
|
3737
|
+
}
|
|
3738
|
+
/**
|
|
3739
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
3740
|
+
*
|
|
3741
|
+
* Postgres only, over a property declared as `type: "vector"`. Rows come
|
|
3742
|
+
* back with a `_distance`; `where` filters before the ordering.
|
|
3743
|
+
*/
|
|
3744
|
+
vectorSearch(property, vector, options) {
|
|
3745
|
+
this.params.vectorSearch = {
|
|
3746
|
+
property,
|
|
3747
|
+
vector,
|
|
3748
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
3749
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
3750
|
+
};
|
|
3607
3751
|
return this;
|
|
3608
3752
|
}
|
|
3609
3753
|
/**
|
|
@@ -3660,6 +3804,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
|
|
|
3660
3804
|
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
3661
3805
|
}
|
|
3662
3806
|
};
|
|
3807
|
+
/**
|
|
3808
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
3809
|
+
*
|
|
3810
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
3811
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
3812
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
3813
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
3814
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
3815
|
+
* rows are the mildest of those outcomes.
|
|
3816
|
+
*
|
|
3817
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
3818
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
3819
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
3820
|
+
* look like it is paging by offset.
|
|
3821
|
+
*/
|
|
3822
|
+
function resolveFindWindow(params) {
|
|
3823
|
+
const limit = params?.limit ?? 50;
|
|
3824
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
3825
|
+
return {
|
|
3826
|
+
limit,
|
|
3827
|
+
offset,
|
|
3828
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3663
3831
|
function normalizePageSize(raw) {
|
|
3664
3832
|
if (raw === void 0 || !Number.isFinite(raw)) return 200;
|
|
3665
3833
|
return Math.max(1, Math.floor(raw));
|
|
@@ -3909,10 +4077,12 @@ function createPrimaryKeyResolver(options) {
|
|
|
3909
4077
|
* than postgres still serve rows with one, and this keeps them working.
|
|
3910
4078
|
*/
|
|
3911
4079
|
function rowToEntity(row, slug, primaryKeys = []) {
|
|
4080
|
+
const { _matches, ...values } = row;
|
|
3912
4081
|
return {
|
|
3913
4082
|
id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
|
|
3914
4083
|
path: slug,
|
|
3915
|
-
values
|
|
4084
|
+
values,
|
|
4085
|
+
..._matches ? { searchMatches: _matches } : {}
|
|
3916
4086
|
};
|
|
3917
4087
|
}
|
|
3918
4088
|
/**
|
|
@@ -3955,21 +4125,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3955
4125
|
const accessor = {
|
|
3956
4126
|
async find(params) {
|
|
3957
4127
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3958
|
-
const limit = params
|
|
3959
|
-
const offset = params?.offset ?? 0;
|
|
4128
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3960
4129
|
const fetchService = driver.restFetchService;
|
|
3961
4130
|
const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
|
|
3962
4131
|
filter,
|
|
3963
|
-
|
|
3964
|
-
|
|
4132
|
+
logical: params?.logical,
|
|
4133
|
+
limit,
|
|
4134
|
+
offset: driverOffset,
|
|
3965
4135
|
orderBy: params?.orderBy?.[0],
|
|
3966
4136
|
order: params?.orderBy?.[1],
|
|
3967
4137
|
searchString: params?.searchString
|
|
3968
4138
|
}, params?.include) : await driver.fetchCollection({
|
|
3969
4139
|
path: slug,
|
|
3970
|
-
limit
|
|
3971
|
-
offset:
|
|
4140
|
+
limit,
|
|
4141
|
+
offset: driverOffset,
|
|
3972
4142
|
filter,
|
|
4143
|
+
logical: params?.logical,
|
|
3973
4144
|
orderBy: params?.orderBy?.[0],
|
|
3974
4145
|
order: params?.orderBy?.[1],
|
|
3975
4146
|
searchString: params?.searchString
|
|
@@ -3979,7 +4150,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3979
4150
|
if (driver.count) {
|
|
3980
4151
|
total = await driver.count({
|
|
3981
4152
|
path: slug,
|
|
3982
|
-
filter
|
|
4153
|
+
filter,
|
|
4154
|
+
logical: params?.logical,
|
|
4155
|
+
searchString: params?.searchString
|
|
3983
4156
|
});
|
|
3984
4157
|
hasMore = offset + rows.length < total;
|
|
3985
4158
|
}
|
|
@@ -4031,30 +4204,48 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4031
4204
|
values: {}
|
|
4032
4205
|
} });
|
|
4033
4206
|
},
|
|
4207
|
+
updateMany: driver.updateMany ? async (updates) => {
|
|
4208
|
+
return (await driver.updateMany({
|
|
4209
|
+
path: slug,
|
|
4210
|
+
updates: updates.map((u) => ({
|
|
4211
|
+
id: u.id,
|
|
4212
|
+
values: u.data
|
|
4213
|
+
}))
|
|
4214
|
+
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
4215
|
+
} : void 0,
|
|
4216
|
+
deleteMany: driver.deleteMany ? async (ids) => {
|
|
4217
|
+
await driver.deleteMany({
|
|
4218
|
+
path: slug,
|
|
4219
|
+
ids
|
|
4220
|
+
});
|
|
4221
|
+
} : void 0,
|
|
4034
4222
|
count: driver.count ? async (params) => {
|
|
4035
4223
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
4036
4224
|
return driver.count({
|
|
4037
4225
|
path: slug,
|
|
4038
|
-
filter
|
|
4226
|
+
filter,
|
|
4227
|
+
logical: params?.logical,
|
|
4228
|
+
searchString: params?.searchString
|
|
4039
4229
|
});
|
|
4040
4230
|
} : void 0,
|
|
4041
4231
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
4042
|
-
const limit = params
|
|
4043
|
-
const offset = params?.offset ?? 0;
|
|
4232
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
4044
4233
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
4045
4234
|
return driver.listenCollection({
|
|
4046
4235
|
path: slug,
|
|
4047
|
-
limit
|
|
4048
|
-
offset:
|
|
4236
|
+
limit,
|
|
4237
|
+
offset: driverOffset,
|
|
4049
4238
|
filter: params?.where,
|
|
4239
|
+
logical: params?.logical,
|
|
4050
4240
|
orderBy: params?.orderBy?.[0],
|
|
4051
4241
|
order: params?.orderBy?.[1],
|
|
4052
4242
|
searchString: params?.searchString,
|
|
4243
|
+
searchExplain: params?.searchExplain,
|
|
4053
4244
|
onUpdate: (entities) => {
|
|
4054
4245
|
onUpdate({
|
|
4055
4246
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
4056
4247
|
meta: {
|
|
4057
|
-
total: entities.length,
|
|
4248
|
+
total: offset + entities.length,
|
|
4058
4249
|
limit,
|
|
4059
4250
|
offset,
|
|
4060
4251
|
hasMore: entities.length >= limit
|
|
@@ -4087,8 +4278,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4087
4278
|
offset(count) {
|
|
4088
4279
|
return new QueryBuilder(accessor).offset(count);
|
|
4089
4280
|
},
|
|
4090
|
-
search(searchString) {
|
|
4091
|
-
return new QueryBuilder(accessor).search(searchString);
|
|
4281
|
+
search(searchString, options) {
|
|
4282
|
+
return new QueryBuilder(accessor).search(searchString, options);
|
|
4283
|
+
},
|
|
4284
|
+
vectorSearch(property, vector, options) {
|
|
4285
|
+
return new QueryBuilder(accessor).vectorSearch(property, vector, options);
|
|
4092
4286
|
},
|
|
4093
4287
|
include(...relations) {
|
|
4094
4288
|
return new QueryBuilder(accessor).include(...relations);
|
|
@@ -4176,8 +4370,18 @@ var SdkQueryBuilder = class {
|
|
|
4176
4370
|
this.params.offset = count;
|
|
4177
4371
|
return this;
|
|
4178
4372
|
}
|
|
4179
|
-
search(searchString) {
|
|
4373
|
+
search(searchString, options) {
|
|
4180
4374
|
this.params.searchString = searchString;
|
|
4375
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
4376
|
+
return this;
|
|
4377
|
+
}
|
|
4378
|
+
vectorSearch(property, vector, options) {
|
|
4379
|
+
this.params.vectorSearch = {
|
|
4380
|
+
property,
|
|
4381
|
+
vector,
|
|
4382
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
4383
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
4384
|
+
};
|
|
4181
4385
|
return this;
|
|
4182
4386
|
}
|
|
4183
4387
|
include(...relations) {
|
|
@@ -4231,9 +4435,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
4231
4435
|
async update(id, data) {
|
|
4232
4436
|
return entityToRow(await snap.update(id, data));
|
|
4233
4437
|
},
|
|
4438
|
+
async updateMany(updates) {
|
|
4439
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
4440
|
+
if (updates.length === 0) return [];
|
|
4441
|
+
if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
|
|
4442
|
+
return (await snap.updateMany(updates.map((u) => ({
|
|
4443
|
+
id: u.id,
|
|
4444
|
+
data: u.data
|
|
4445
|
+
})))).map(entityToRow);
|
|
4446
|
+
},
|
|
4234
4447
|
delete(id) {
|
|
4235
4448
|
return snap.delete(id);
|
|
4236
4449
|
},
|
|
4450
|
+
async deleteMany(ids) {
|
|
4451
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
4452
|
+
if (ids.length === 0) return;
|
|
4453
|
+
if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
|
|
4454
|
+
await snap.deleteMany(ids);
|
|
4455
|
+
},
|
|
4237
4456
|
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
4238
4457
|
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
4239
4458
|
data: res.data.map(entityToRow),
|
|
@@ -4249,6 +4468,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
4249
4468
|
limit: (count) => new SdkQueryBuilder(client).limit(count),
|
|
4250
4469
|
offset: (count) => new SdkQueryBuilder(client).offset(count),
|
|
4251
4470
|
search: (searchString) => new SdkQueryBuilder(client).search(searchString),
|
|
4471
|
+
vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
|
|
4252
4472
|
include: (...relations) => new SdkQueryBuilder(client).include(...relations)
|
|
4253
4473
|
};
|
|
4254
4474
|
return client;
|
|
@@ -4294,6 +4514,460 @@ function buildSdkData(driver) {
|
|
|
4294
4514
|
return wrapAsSdkData(buildRebaseData(driver));
|
|
4295
4515
|
}
|
|
4296
4516
|
//#endregion
|
|
4297
|
-
|
|
4517
|
+
//#region src/schema/search-column.ts
|
|
4518
|
+
/**
|
|
4519
|
+
* The one place a collection's `search` block becomes SQL.
|
|
4520
|
+
*
|
|
4521
|
+
* Four things describe a Postgres table in this codebase — the DDL generator,
|
|
4522
|
+
* the Drizzle schema generator, the runtime table builder for BaaS mode, and
|
|
4523
|
+
* the boot-time schema ensure — and each of them has, at some point, described
|
|
4524
|
+
* a column differently from the others. The `varchar(255)` note in
|
|
4525
|
+
* `generate-postgres-ddl-logic` is one such scar: the same property produced a
|
|
4526
|
+
* capped column down one path and an uncapped one down the other, and nothing
|
|
4527
|
+
* failed until a user hit the cap.
|
|
4528
|
+
*
|
|
4529
|
+
* So the search column is not implemented four times. It is computed once,
|
|
4530
|
+
* here, and every generator renders the same {@link SearchColumnSpec}. There is
|
|
4531
|
+
* a test asserting exactly that (`search-column-contract.test.ts`); the point of
|
|
4532
|
+
* this module is that the test has something to assert *about*.
|
|
4533
|
+
*
|
|
4534
|
+
* ## Why the expressions look the way they do
|
|
4535
|
+
*
|
|
4536
|
+
* A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
|
|
4537
|
+
* Postgres is stricter here than intuition. Verified against PostgreSQL 18:
|
|
4538
|
+
*
|
|
4539
|
+
* | expression | immutable |
|
|
4540
|
+
* |-----------------------------------------|-----------|
|
|
4541
|
+
* | `to_tsvector('spanish', col)` | yes |
|
|
4542
|
+
* | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
|
|
4543
|
+
* | `array_to_string(col, ' ')` | **no** |
|
|
4544
|
+
* | `col::text` on `text[]` | **no** |
|
|
4545
|
+
* | `to_jsonb(col)` | **no** |
|
|
4546
|
+
* | `unaccent(col)` | **no** — dictionary lookup is STABLE |
|
|
4547
|
+
* | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
|
|
4548
|
+
* | `setweight(...) || setweight(...)` | yes |
|
|
4549
|
+
*
|
|
4550
|
+
* Three of the four things a real search column needs are therefore unavailable
|
|
4551
|
+
* directly, which is why {@link searchHelperFunctions} exists: each wraps a
|
|
4552
|
+
* stable built-in in an SQL function declared IMMUTABLE. That declaration is a
|
|
4553
|
+
* promise, and it is a true one for these three — array joining, JSON string
|
|
4554
|
+
* extraction and accent folding are all deterministic for a given input; the
|
|
4555
|
+
* built-ins are marked stable only because they must account for element types
|
|
4556
|
+
* and dictionaries in general.
|
|
4557
|
+
*
|
|
4558
|
+
* The alternative was to skip `unaccent` and text arrays entirely. That is not
|
|
4559
|
+
* a real option in an accented language: Postgres stems `auditoría` to
|
|
4560
|
+
* `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
|
|
4561
|
+
* typed without accents misses every row that carries them.
|
|
4562
|
+
*/
|
|
4563
|
+
/** Schema-qualified so a collection outside `public` still resolves them. */
|
|
4564
|
+
var HELPER_SCHEMA = "public";
|
|
4565
|
+
/**
|
|
4566
|
+
* Names of the helper functions. Frozen: they are recorded in the stored
|
|
4567
|
+
* generation expression of every search column ever created, so renaming one
|
|
4568
|
+
* orphans every table that already has a search column.
|
|
4569
|
+
*/
|
|
4570
|
+
var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
|
|
4571
|
+
var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
|
|
4572
|
+
/** Raised when a `search` block names something that cannot be searched. */
|
|
4573
|
+
var SearchConfigError = class extends Error {
|
|
4574
|
+
constructor(message) {
|
|
4575
|
+
super(message);
|
|
4576
|
+
this.name = "SearchConfigError";
|
|
4577
|
+
}
|
|
4578
|
+
};
|
|
4579
|
+
/** The `search` block of a collection, or undefined when it has none. */
|
|
4580
|
+
var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
|
|
4581
|
+
/**
|
|
4582
|
+
* Refuse a `search` block on a collection this engine does not store.
|
|
4583
|
+
*
|
|
4584
|
+
* The type only permits one on a `PostgresCollectionConfig`, so TypeScript
|
|
4585
|
+
* already stops the ordinary case. This catches the rest — a JS config, a cast,
|
|
4586
|
+
* a collection whose `engine` was changed after the block was written — because
|
|
4587
|
+
* the alternative is the exact failure the block exists to prevent: a developer
|
|
4588
|
+
* who declared what to index, saw no error, and got the substring fallback.
|
|
4589
|
+
*
|
|
4590
|
+
* Called with *every* collection, before the Postgres ones are filtered out.
|
|
4591
|
+
*/
|
|
4592
|
+
var assertSearchIsPostgresOnly = (collections) => {
|
|
4593
|
+
for (const collection of collections) {
|
|
4594
|
+
if (isPostgresCollectionConfig(collection)) continue;
|
|
4595
|
+
if (!collection.search) continue;
|
|
4596
|
+
const engine = collection.engine ?? "non-postgres";
|
|
4597
|
+
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.`);
|
|
4598
|
+
}
|
|
4599
|
+
};
|
|
4600
|
+
var columnNameOf = (propName, prop) => prop && "columnName" in prop && typeof prop.columnName === "string" ? prop.columnName : toSnakeCase(propName);
|
|
4601
|
+
/**
|
|
4602
|
+
* Classify a property for search purposes.
|
|
4603
|
+
*
|
|
4604
|
+
* Deliberately narrower than `getSqlColumnType`: search only cares whether a
|
|
4605
|
+
* value reaches text, and the mapping from property to *physical* type is
|
|
4606
|
+
* asserted against `getSqlColumnType` in the contract test rather than
|
|
4607
|
+
* duplicated here.
|
|
4608
|
+
*
|
|
4609
|
+
* Returns null for anything that is not text-bearing, which the caller turns
|
|
4610
|
+
* into a boot error naming the property.
|
|
4611
|
+
*/
|
|
4612
|
+
var classify = (prop) => {
|
|
4613
|
+
switch (prop.type) {
|
|
4614
|
+
case "string": {
|
|
4615
|
+
const sp = prop;
|
|
4616
|
+
if (sp.enum) return {
|
|
4617
|
+
kind: "text",
|
|
4618
|
+
reason: "enum"
|
|
4619
|
+
};
|
|
4620
|
+
if (sp.isId === "uuid" || sp.columnType === "uuid") return {
|
|
4621
|
+
kind: "text",
|
|
4622
|
+
reason: "uuid"
|
|
4623
|
+
};
|
|
4624
|
+
return { kind: "text" };
|
|
4625
|
+
}
|
|
4626
|
+
case "map":
|
|
4627
|
+
if (prop.columnType === "json") return {
|
|
4628
|
+
kind: "jsonb",
|
|
4629
|
+
reason: "json"
|
|
4630
|
+
};
|
|
4631
|
+
return { kind: "jsonb" };
|
|
4632
|
+
case "array": {
|
|
4633
|
+
const ap = prop;
|
|
4634
|
+
let colType = ap.columnType;
|
|
4635
|
+
if (!colType && ap.of && !Array.isArray(ap.of)) {
|
|
4636
|
+
const of = ap.of;
|
|
4637
|
+
if (of.type === "string") colType = "text[]";
|
|
4638
|
+
else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
|
|
4639
|
+
else if (of.type === "boolean") colType = "boolean[]";
|
|
4640
|
+
}
|
|
4641
|
+
if (colType === "text[]") return { kind: "text_array" };
|
|
4642
|
+
if (colType === "json") return {
|
|
4643
|
+
kind: "jsonb",
|
|
4644
|
+
reason: "json"
|
|
4645
|
+
};
|
|
4646
|
+
if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
|
|
4647
|
+
kind: "text_array",
|
|
4648
|
+
reason: "non_text_array"
|
|
4649
|
+
};
|
|
4650
|
+
return { kind: "jsonb" };
|
|
4651
|
+
}
|
|
4652
|
+
default: return null;
|
|
4653
|
+
}
|
|
4654
|
+
};
|
|
4655
|
+
var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
|
|
4656
|
+
/** SQL reading one field as plain text, before normalization. */
|
|
4657
|
+
var rawTextSql = (field) => {
|
|
4658
|
+
const col = `"${field.column}"`;
|
|
4659
|
+
if (field.kind === "text") return `coalesce(${col}, '')`;
|
|
4660
|
+
if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
|
|
4661
|
+
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))`;
|
|
4662
|
+
};
|
|
4663
|
+
var quote = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
4664
|
+
/**
|
|
4665
|
+
* Resolve and validate one declared field path.
|
|
4666
|
+
*
|
|
4667
|
+
* A path that does not resolve throws. The whole point of an explicit block is
|
|
4668
|
+
* that the author knows what is indexed; a silently dropped field would make it
|
|
4669
|
+
* a guess again, and the failure — a search that returns nothing for content
|
|
4670
|
+
* that is plainly in the row — is invisible from the outside.
|
|
4671
|
+
*/
|
|
4672
|
+
var resolveField = (entry, collection, cfg) => {
|
|
4673
|
+
const path = typeof entry === "string" ? entry : entry.path;
|
|
4674
|
+
const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? "B";
|
|
4675
|
+
const where = `${collection.slug}.search`;
|
|
4676
|
+
if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
|
|
4677
|
+
const [head, ...rest] = path.split(".");
|
|
4678
|
+
const prop = collection.properties?.[head];
|
|
4679
|
+
if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
|
|
4680
|
+
const classified = classify(prop);
|
|
4681
|
+
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).`);
|
|
4682
|
+
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.`);
|
|
4683
|
+
if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
|
|
4684
|
+
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.`);
|
|
4685
|
+
if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
|
|
4686
|
+
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.`);
|
|
4687
|
+
const column = columnNameOf(head, prop);
|
|
4688
|
+
const textSql = normalize(rawTextSql({
|
|
4689
|
+
column,
|
|
4690
|
+
jsonPath: rest,
|
|
4691
|
+
kind: classified.kind
|
|
4692
|
+
}), cfg.unaccent === true);
|
|
4693
|
+
const language = cfg.language ?? "simple";
|
|
4694
|
+
return {
|
|
4695
|
+
path,
|
|
4696
|
+
column,
|
|
4697
|
+
jsonPath: rest,
|
|
4698
|
+
kind: classified.kind,
|
|
4699
|
+
weight,
|
|
4700
|
+
sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
|
|
4701
|
+
textSql
|
|
4702
|
+
};
|
|
4703
|
+
};
|
|
4704
|
+
/**
|
|
4705
|
+
* Build the full spec for a collection, or undefined when it has not opted in.
|
|
4706
|
+
*
|
|
4707
|
+
* Throws {@link SearchConfigError} on a config that cannot be honoured. Callers
|
|
4708
|
+
* at boot surface that as a startup failure — a search block that half-works is
|
|
4709
|
+
* worse than one that refuses.
|
|
4710
|
+
*/
|
|
4711
|
+
var buildSearchColumnSpec = (collection) => {
|
|
4712
|
+
const cfg = getSearchConfig(collection);
|
|
4713
|
+
if (!cfg) return void 0;
|
|
4714
|
+
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.`);
|
|
4715
|
+
const table = getTableName(collection);
|
|
4716
|
+
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
4717
|
+
const column = cfg.column ?? "search_vector";
|
|
4718
|
+
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.`);
|
|
4719
|
+
const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
|
|
4720
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4721
|
+
for (const f of fields) {
|
|
4722
|
+
if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
|
|
4723
|
+
seen.add(f.path);
|
|
4724
|
+
}
|
|
4725
|
+
const extensions = [];
|
|
4726
|
+
if (cfg.unaccent) extensions.push("unaccent");
|
|
4727
|
+
if (cfg.fuzzy) extensions.push("pg_trgm");
|
|
4728
|
+
const spec = {
|
|
4729
|
+
schema,
|
|
4730
|
+
table,
|
|
4731
|
+
column,
|
|
4732
|
+
language: cfg.language ?? "simple",
|
|
4733
|
+
unaccent: cfg.unaccent === true,
|
|
4734
|
+
fields,
|
|
4735
|
+
expression: fields.map((f) => f.sql).join(" || "),
|
|
4736
|
+
indexName: toPostgresIdentifier(`${table}_${column}_gin`),
|
|
4737
|
+
extensions
|
|
4738
|
+
};
|
|
4739
|
+
if (cfg.fuzzy) {
|
|
4740
|
+
const fuzzyColumn = `${column}_text`;
|
|
4741
|
+
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.`);
|
|
4742
|
+
spec.fuzzy = {
|
|
4743
|
+
column: fuzzyColumn,
|
|
4744
|
+
expression: fields.map((f) => f.textSql).join(" || ' ' || "),
|
|
4745
|
+
indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
|
|
4746
|
+
threshold: cfg.fuzzyThreshold ?? .3
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
return spec;
|
|
4750
|
+
};
|
|
4751
|
+
/**
|
|
4752
|
+
* The IMMUTABLE wrappers the generated expressions call.
|
|
4753
|
+
*
|
|
4754
|
+
* `CREATE OR REPLACE` so a boot against an existing database is a no-op rather
|
|
4755
|
+
* than an error, and idempotent for the same reason every other boot-time DDL
|
|
4756
|
+
* statement here is.
|
|
4757
|
+
*
|
|
4758
|
+
* The bodies are stable built-ins wrapped in an immutable promise — see the
|
|
4759
|
+
* module comment for why that promise is sound. `STRICT` matters: it makes NULL
|
|
4760
|
+
* in mean NULL out without executing the body, which is what the `coalesce` at
|
|
4761
|
+
* each call site then absorbs.
|
|
4762
|
+
*/
|
|
4763
|
+
var searchHelperFunctions = (spec) => {
|
|
4764
|
+
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 $$;`];
|
|
4765
|
+
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) $$;`);
|
|
4766
|
+
return statements;
|
|
4767
|
+
};
|
|
4768
|
+
/**
|
|
4769
|
+
* `CREATE EXTENSION` statements the spec's expressions depend on.
|
|
4770
|
+
*
|
|
4771
|
+
* `WITH SCHEMA public` is load-bearing, not tidiness. An unqualified
|
|
4772
|
+
* `CREATE EXTENSION` installs into the first schema on `search_path`, which
|
|
4773
|
+
* defaults to `"$user", public` — and the scaffold's database role is named
|
|
4774
|
+
* `rebase`, the same as the schema the generator creates one statement earlier.
|
|
4775
|
+
* So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
|
|
4776
|
+
* dictionary in `rebase`, and every reference to `public.unaccent` below fails
|
|
4777
|
+
* with "text search dictionary does not exist". Observed, not theorised.
|
|
4778
|
+
*/
|
|
4779
|
+
var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
|
|
4780
|
+
/**
|
|
4781
|
+
* Index statements for the spec.
|
|
4782
|
+
*
|
|
4783
|
+
* `CONCURRENTLY` is deliberately *not* used here. This form is emitted into a
|
|
4784
|
+
* SQL file replayed as one unit — a migration, or `search.sql` — where a
|
|
4785
|
+
* concurrent build is not allowed. The boot-time ensure path runs statement by
|
|
4786
|
+
* statement against tables that are live and populated, and uses the
|
|
4787
|
+
* concurrent form instead; see `ensureSearchColumns`.
|
|
4788
|
+
*/
|
|
4789
|
+
var searchIndexStatements = (spec) => {
|
|
4790
|
+
const statements = [`CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING GIN ("${spec.column}");`];
|
|
4791
|
+
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);`);
|
|
4792
|
+
return statements;
|
|
4793
|
+
};
|
|
4794
|
+
/**
|
|
4795
|
+
* The generated column names a collection's search block adds, if any.
|
|
4796
|
+
*
|
|
4797
|
+
* These are physical columns on the table, so `SELECT *` returns them. They are
|
|
4798
|
+
* an index in column form — a list of lexeme positions, or a concatenation of
|
|
4799
|
+
* every searchable field on the row — and nothing outside the query planner has
|
|
4800
|
+
* any use for them. Left in, every list response carries a second, larger copy
|
|
4801
|
+
* of the row's text.
|
|
4802
|
+
*/
|
|
4803
|
+
var searchColumnNames = (collection) => {
|
|
4804
|
+
let spec;
|
|
4805
|
+
try {
|
|
4806
|
+
spec = buildSearchColumnSpec(collection);
|
|
4807
|
+
} catch {
|
|
4808
|
+
return [];
|
|
4809
|
+
}
|
|
4810
|
+
if (!spec) return [];
|
|
4811
|
+
return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
|
|
4812
|
+
};
|
|
4813
|
+
/**
|
|
4814
|
+
* True for a column whose type only ever holds a search index.
|
|
4815
|
+
*
|
|
4816
|
+
* Independent of any collection config on purpose: an introspected database
|
|
4817
|
+
* (BaaS mode) can carry a `tsvector` column this framework never created —
|
|
4818
|
+
* Pagila's `film.fulltext` is the canonical one — and it should not be returned
|
|
4819
|
+
* to callers either. `isDerivedIndexColumn` already keeps such a column out of
|
|
4820
|
+
* the *properties*; this keeps it out of the *rows*.
|
|
4821
|
+
*/
|
|
4822
|
+
var isSearchIndexColumn = (column) => {
|
|
4823
|
+
const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
|
|
4824
|
+
return sqlType === "tsvector" || sqlType === "tsquery";
|
|
4825
|
+
};
|
|
4826
|
+
/**
|
|
4827
|
+
* A drizzle select projection over `table` with the search columns dropped.
|
|
4828
|
+
*
|
|
4829
|
+
* Returns undefined when nothing needs dropping, so the common case keeps using
|
|
4830
|
+
* a plain `select()` and this stays invisible in the generated SQL.
|
|
4831
|
+
*/
|
|
4832
|
+
var visibleColumnProjection = (tableColumns, collection) => {
|
|
4833
|
+
const excluded = excludedColumnNames(tableColumns, collection);
|
|
4834
|
+
if (!tableColumns || excluded.length === 0) return void 0;
|
|
4835
|
+
const projection = {};
|
|
4836
|
+
for (const [name, column] of Object.entries(tableColumns)) if (!excluded.includes(name)) projection[name] = column;
|
|
4837
|
+
return projection;
|
|
4838
|
+
};
|
|
4839
|
+
/** The same exclusion as a drizzle `db.query` `columns` denylist. */
|
|
4840
|
+
var hiddenColumnsOption = (tableColumns, collection) => {
|
|
4841
|
+
const excluded = excludedColumnNames(tableColumns, collection);
|
|
4842
|
+
if (excluded.length === 0) return void 0;
|
|
4843
|
+
return Object.fromEntries(excluded.map((name) => [name, false]));
|
|
4844
|
+
};
|
|
4845
|
+
/**
|
|
4846
|
+
* The columns to keep out of a response, by name.
|
|
4847
|
+
*
|
|
4848
|
+
* `tableColumns` is whatever `getTableColumns` returned, which is `undefined`
|
|
4849
|
+
* for anything that is not a real drizzle table — a stub in a test, a derived
|
|
4850
|
+
* or nested path with no table behind it. Nothing to exclude is the right
|
|
4851
|
+
* answer there, and it has to be an answer rather than a throw: this runs on
|
|
4852
|
+
* the read path of every collection, opted in or not.
|
|
4853
|
+
*/
|
|
4854
|
+
var excludedColumnNames = (tableColumns, collection) => {
|
|
4855
|
+
if (!tableColumns || typeof tableColumns !== "object") return [];
|
|
4856
|
+
const byName = new Set(collection ? searchColumnNames(collection) : []);
|
|
4857
|
+
return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
|
|
4858
|
+
};
|
|
4859
|
+
//#endregion
|
|
4860
|
+
//#region src/schema/auth-users-columns.ts
|
|
4861
|
+
/**
|
|
4862
|
+
* `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
|
|
4863
|
+
* — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
|
|
4864
|
+
* The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
|
|
4865
|
+
* separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
|
|
4866
|
+
* table that already holds a longer row.
|
|
4867
|
+
*/
|
|
4868
|
+
var AUTH_USERS_COLUMNS = [
|
|
4869
|
+
{
|
|
4870
|
+
column: "email",
|
|
4871
|
+
type: "TEXT",
|
|
4872
|
+
notNull: true
|
|
4873
|
+
},
|
|
4874
|
+
{
|
|
4875
|
+
column: "display_name",
|
|
4876
|
+
type: "TEXT"
|
|
4877
|
+
},
|
|
4878
|
+
{
|
|
4879
|
+
column: "photo_url",
|
|
4880
|
+
type: "TEXT"
|
|
4881
|
+
},
|
|
4882
|
+
{
|
|
4883
|
+
column: "roles",
|
|
4884
|
+
type: "TEXT[]",
|
|
4885
|
+
default: "'{}'",
|
|
4886
|
+
notNull: true
|
|
4887
|
+
},
|
|
4888
|
+
{
|
|
4889
|
+
column: "password_hash",
|
|
4890
|
+
type: "TEXT"
|
|
4891
|
+
},
|
|
4892
|
+
{
|
|
4893
|
+
column: "email_verified",
|
|
4894
|
+
type: "BOOLEAN",
|
|
4895
|
+
default: "FALSE",
|
|
4896
|
+
notNull: true
|
|
4897
|
+
},
|
|
4898
|
+
{
|
|
4899
|
+
column: "email_verification_token",
|
|
4900
|
+
type: "TEXT"
|
|
4901
|
+
},
|
|
4902
|
+
{
|
|
4903
|
+
column: "email_verification_sent_at",
|
|
4904
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
4905
|
+
},
|
|
4906
|
+
{
|
|
4907
|
+
column: "is_anonymous",
|
|
4908
|
+
type: "BOOLEAN",
|
|
4909
|
+
default: "FALSE",
|
|
4910
|
+
notNull: true
|
|
4911
|
+
},
|
|
4912
|
+
{
|
|
4913
|
+
column: "metadata",
|
|
4914
|
+
type: "JSONB",
|
|
4915
|
+
default: "'{}'",
|
|
4916
|
+
notNull: true
|
|
4917
|
+
},
|
|
4918
|
+
{
|
|
4919
|
+
column: "tokens_valid_after",
|
|
4920
|
+
type: "TIMESTAMP WITH TIME ZONE"
|
|
4921
|
+
},
|
|
4922
|
+
{
|
|
4923
|
+
column: "created_at",
|
|
4924
|
+
type: "TIMESTAMP WITH TIME ZONE",
|
|
4925
|
+
default: "NOW()",
|
|
4926
|
+
notNull: true
|
|
4927
|
+
},
|
|
4928
|
+
{
|
|
4929
|
+
column: "updated_at",
|
|
4930
|
+
type: "TIMESTAMP WITH TIME ZONE",
|
|
4931
|
+
default: "NOW()",
|
|
4932
|
+
notNull: true
|
|
4933
|
+
}
|
|
4934
|
+
];
|
|
4935
|
+
var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
|
|
4936
|
+
/** Type + inline constraints, as they appear after the column name. */
|
|
4937
|
+
function authUsersColumnSql(spec) {
|
|
4938
|
+
return [
|
|
4939
|
+
spec.type,
|
|
4940
|
+
spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
|
|
4941
|
+
spec.notNull ? "NOT NULL" : ""
|
|
4942
|
+
].filter(Boolean).join(" ");
|
|
4943
|
+
}
|
|
4944
|
+
/**
|
|
4945
|
+
* The auth-owned definition for a physical column name, or `undefined` when
|
|
4946
|
+
* auth does not own it.
|
|
4947
|
+
*
|
|
4948
|
+
* Callers pass the RESOLVED column name (after `columnName` mapping), because
|
|
4949
|
+
* that is the only name the three creators agree on: the scaffold's users
|
|
4950
|
+
* collection spells the property `displayName` and the column `display_name`.
|
|
4951
|
+
*/
|
|
4952
|
+
function authUsersColumnDefinition(column) {
|
|
4953
|
+
const spec = BY_COLUMN.get(column);
|
|
4954
|
+
return spec ? authUsersColumnSql(spec) : void 0;
|
|
4955
|
+
}
|
|
4956
|
+
/**
|
|
4957
|
+
* Whether a collection is an auth collection, i.e. whether the definitions in
|
|
4958
|
+
* this module apply to its table at all.
|
|
4959
|
+
*
|
|
4960
|
+
* Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
|
|
4961
|
+
* that one takes a `CollectionConfig`, this one is called from DDL code paths
|
|
4962
|
+
* that hold looser objects, and both spellings must accept `auth: true` as well
|
|
4963
|
+
* as `auth: { enabled: true }`.
|
|
4964
|
+
*/
|
|
4965
|
+
function isAuthCollection(collection) {
|
|
4966
|
+
const auth = collection?.auth;
|
|
4967
|
+
if (auth === true) return true;
|
|
4968
|
+
return typeof auth === "object" && auth !== null && auth.enabled === true;
|
|
4969
|
+
}
|
|
4970
|
+
//#endregion
|
|
4971
|
+
export { resolveCollectionRelations as A, legacyForeignKeyName as B, securityRuleToConditions as C, getEnumVarName as D, getColumnName as E, createRelationRef as F, camelCase as G, getPolicyNamesForRule as H, createRelationRefWithData as I, DEFAULT_ONE_OF_VALUE as J, toSnakeCase as K, normalizeToEntityRelation as L, getDeclaredPrimaryKeys as M, isAddressableId as N, getTableName as O, parseIdValues as P, Vector as Q, updateDateAutoValues as R, policyToPostgres as S, findRelation as T, isPrototypePollutingKey as U, toPostgresIdentifier as V, mergeDeep as W, hasForeignKeyOnTarget as X, resolveClientListLimit as Y, isManyToMany as Z, resolveStringColumnLength as _, SEARCH_TEXT_FN as a, resolveJunctionSpecs as b, buildSearchColumnSpec as c, searchHelperFunctions as d, searchIndexStatements as f, relationalCollections as g, CollectionRegistry as h, isAuthCollection as i, buildCompositeId as j, getTableVarName as k, hiddenColumnsOption as l, buildSdkData as m, authUsersColumnDefinition as n, SEARCH_UNACCENT_FN as o, visibleColumnProjection as p, DEFAULT_ONE_OF_TYPE as q, authUsersColumnSql as r, assertSearchIsPostgresOnly as s, AUTH_USERS_COLUMNS as t, searchExtensionStatements as u, getJunctionCollectionConfig as v, findAnonymousGrants as w, getEffectiveSecurityRules as x, getJunctionSecurityRules as y, generateForeignKeyName as z };
|
|
4298
4972
|
|
|
4299
|
-
//# sourceMappingURL=
|
|
4973
|
+
//# sourceMappingURL=auth-users-columns-Dt9g712t.js.map
|