@rebasepro/server-postgres 0.16.0 → 0.16.1-canary.g0d7af95
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/PostgresAdapter.d.ts +1 -1
- package/dist/PostgresBackendDriver.d.ts +75 -12
- package/dist/PostgresBootstrapper.d.ts +6 -6
- package/dist/auth/services.d.ts +1 -1
- package/dist/backup/backup-cron.d.ts +1 -1
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/index.d.ts +4 -4
- package/dist/{backup-service-BZoixhVl.js → backup-service-BtgHxfFm.js} +5 -4
- package/dist/{backup-service-BZoixhVl.js.map → backup-service-BtgHxfFm.js.map} +1 -1
- package/dist/cli-helpers.d.ts +41 -0
- package/dist/{auth-users-columns-CgyPWQ18.js → collection-index-DxJBvVTH.js} +486 -503
- package/dist/collection-index-DxJBvVTH.js.map +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/collections/validate-relations.d.ts +1 -1
- package/dist/{connection-BuZ97wsr.js → connection-GOKU3Hu5.js} +34 -7
- package/dist/connection-GOKU3Hu5.js.map +1 -0
- package/dist/connection.d.ts +16 -0
- package/dist/data-transformer.d.ts +1 -1
- package/dist/{ensure-collection-policies-BVFb2olB.js → ensure-collection-policies-DFpOl8SM.js} +4 -4
- package/dist/{ensure-collection-policies-BVFb2olB.js.map → ensure-collection-policies-DFpOl8SM.js.map} +1 -1
- package/dist/ensure-collection-tables-DMjOkeRy.js +1952 -0
- package/dist/ensure-collection-tables-DMjOkeRy.js.map +1 -0
- package/dist/index.d.ts +16 -16
- package/dist/index.es.js +19 -7128
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-B5Sajku6.js → rls-bootstrap-sql-DNzaWd4C.js} +3 -3
- package/dist/{rls-bootstrap-sql-B5Sajku6.js.map → rls-bootstrap-sql-DNzaWd4C.js.map} +1 -1
- package/dist/{rls-enforcement-Ch0T6OwW.js → rls-enforcement-CInuYj1-.js} +14 -4
- package/dist/rls-enforcement-CInuYj1-.js.map +1 -0
- package/dist/schema/classify-change.d.ts +82 -0
- package/dist/schema/collection-index.d.ts +182 -0
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +1 -1
- package/dist/schema/ensure-collection-tables.d.ts +93 -2
- package/dist/schema/generate-schema-commit.d.ts +136 -0
- package/dist/schema/generated-schema-staleness.d.ts +19 -0
- package/dist/schema/introspect-db-constraints.d.ts +1 -1
- package/dist/schema/introspect-db-logic.d.ts +3 -3
- package/dist/schema/introspect-db-project.d.ts +1 -1
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-structure.d.ts +2 -2
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/schema/vector-index.d.ts +88 -0
- package/dist/services/BranchService.d.ts +2 -2
- package/dist/services/FetchService.d.ts +4 -4
- package/dist/services/PersistService.d.ts +5 -5
- package/dist/services/RelationService.d.ts +3 -3
- package/dist/services/RelationWriteService.d.ts +3 -3
- package/dist/services/cdc/junction-tables.d.ts +1 -1
- package/dist/services/cdc/trigger-cdc.d.ts +1 -1
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +1 -1
- package/dist/services/channel-bus/index.d.ts +2 -2
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/dataService.d.ts +10 -10
- package/dist/services/index.d.ts +4 -4
- package/dist/services/junction-writes.d.ts +2 -2
- package/dist/services/nested-path.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +3 -3
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/services/write-denial.d.ts +1 -1
- package/dist/{src-BBFsDaeA.js → src-DiDgtX8P.js} +97 -1
- package/dist/src-DiDgtX8P.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +2 -2
- package/dist/websocket-CUnpSe8v.js +8188 -0
- package/dist/websocket-CUnpSe8v.js.map +1 -0
- package/dist/websocket.d.ts +29 -2
- package/package.json +7 -7
- package/src/PostgresBackendDriver.ts +190 -59
- package/src/backup/backup-service.ts +1 -1
- package/src/cli-helpers.ts +117 -2
- package/src/cli.ts +22 -0
- package/src/connection.ts +37 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/classify-change.ts +436 -0
- package/src/schema/collection-index.ts +427 -0
- package/src/schema/ensure-collection-tables.test.ts +168 -1
- package/src/schema/ensure-collection-tables.ts +365 -14
- package/src/schema/generate-drizzle-schema-logic.ts +23 -11
- package/src/schema/generate-drizzle-schema.ts +13 -2
- package/src/schema/generate-postgres-ddl-logic.ts +33 -6
- package/src/schema/generate-postgres-ddl.ts +13 -2
- package/src/schema/generate-schema-commit.ts +242 -0
- package/src/schema/generated-schema-staleness.ts +114 -1
- package/src/schema/vector-index.ts +278 -0
- package/src/services/collection-helpers.ts +3 -2
- package/src/websocket.ts +47 -3
- package/dist/auth-users-columns-CgyPWQ18.js.map +0 -1
- package/dist/connection-BuZ97wsr.js.map +0 -1
- package/dist/data_driver-ULAyJEi9.js +0 -193
- package/dist/data_driver-ULAyJEi9.js.map +0 -1
- package/dist/ensure-collection-tables-BY1pHRD_.js +0 -840
- package/dist/ensure-collection-tables-BY1pHRD_.js.map +0 -1
- package/dist/rls-enforcement-Ch0T6OwW.js.map +0 -1
- package/dist/src-BBFsDaeA.js.map +0 -1
- package/dist/utils/table-classification.d.ts +0 -8
- package/dist/websocket-BVgDVO-V.js +0 -614
- package/dist/websocket-BVgDVO-V.js.map +0 -1
- package/src/utils/table-classification.ts +0 -16
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { d as __require, l as __commonJSMin } from "./connection-GOKU3Hu5.js";
|
|
5
|
+
import { _ as isRelationAggregateSort, a as rewriteLegacyRlsFunctions, b as toCanonicalOp, c as isPostgresCollectionConfig, d as getDataSourceCapabilities, f as ALL_WHERE_FILTER_OPS, 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, y as sortKeyToString } from "./src-DiDgtX8P.js";
|
|
6
|
+
//#region ../types/src/errors.ts
|
|
7
|
+
/**
|
|
8
|
+
* The single error type thrown across the entire Rebase client surface —
|
|
9
|
+
* HTTP data/control-plane calls, realtime/WebSocket operations, and
|
|
10
|
+
* client-side logic errors (e.g. an unknown collection accessor). A `catch`
|
|
11
|
+
* block only ever needs to check for this one class:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { RebaseApiError } from "@rebasepro/client"; // re-exported
|
|
15
|
+
*
|
|
16
|
+
* try {
|
|
17
|
+
* await client.data.products.update(id, { price: 9 });
|
|
18
|
+
* } catch (e) {
|
|
19
|
+
* if (e instanceof RebaseApiError) {
|
|
20
|
+
* if (e.status === 404) { ... } // HTTP failures carry a status
|
|
21
|
+
* console.error(e.code, e.details);
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* `status` is present for HTTP failures and `undefined` otherwise, so its
|
|
27
|
+
* presence distinguishes transport-level errors from realtime/logic errors.
|
|
28
|
+
*
|
|
29
|
+
* @group Errors
|
|
30
|
+
*/
|
|
31
|
+
var RebaseApiError = class extends Error {
|
|
32
|
+
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
33
|
+
status;
|
|
34
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
35
|
+
code;
|
|
36
|
+
/** Structured error payload from the server, when present. */
|
|
37
|
+
details;
|
|
38
|
+
constructor(message, init = {}) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "RebaseApiError";
|
|
41
|
+
this.status = init.status;
|
|
42
|
+
this.code = init.code;
|
|
43
|
+
this.details = init.details;
|
|
44
|
+
if (init.cause !== void 0) this.cause = init.cause;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
8
48
|
//#region ../types/src/types/entities.ts
|
|
9
49
|
/**
|
|
10
50
|
* Class used to create a reference to a entity in a different path
|
|
@@ -57,6 +97,151 @@ function isManyToMany(relation) {
|
|
|
57
97
|
return relation.kind === "manyToMany";
|
|
58
98
|
}
|
|
59
99
|
//#endregion
|
|
100
|
+
//#region ../types/src/types/policy.ts
|
|
101
|
+
/**
|
|
102
|
+
* The id a request without a logged-in user reports as `rebase.uid()`.
|
|
103
|
+
*
|
|
104
|
+
* A user-context request always sets `app.uid`: blank would read back as
|
|
105
|
+
* `NULL`, and `NULL` is how the trusted server context is recognised, so an
|
|
106
|
+
* anonymous visitor would be promoted to server privileges. The driver
|
|
107
|
+
* therefore substitutes this sentinel at the single chokepoint where the GUC
|
|
108
|
+
* is set.
|
|
109
|
+
*
|
|
110
|
+
* The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a
|
|
111
|
+
* tautology on the user path** — it is true for anonymous visitors too. Use
|
|
112
|
+
* {@link policy.authenticated} to mean "signed in", and
|
|
113
|
+
* {@link policy.serverContext} to mean "the trusted server context". Do not
|
|
114
|
+
* hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one
|
|
115
|
+
* literal is not enough.
|
|
116
|
+
*
|
|
117
|
+
* @group Models
|
|
118
|
+
*/
|
|
119
|
+
var ANONYMOUS_USER_ID = "anonymous";
|
|
120
|
+
/**
|
|
121
|
+
* Every uid that has ever meant "nobody is signed in" — newest first.
|
|
122
|
+
*
|
|
123
|
+
* There are two because there were two. The types, the policy compiler, the
|
|
124
|
+
* JavaScript evaluator and the linter were all built on
|
|
125
|
+
* {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
|
|
126
|
+
* callers as `'anon'` — so `policy.authenticated()`, which compiled to
|
|
127
|
+
* `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
|
|
128
|
+
* sanctioned way to write "signed in" granted to everyone, and the linter
|
|
129
|
+
* flagged the spelling that actually worked as a foreign convention.
|
|
130
|
+
*
|
|
131
|
+
* The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here
|
|
132
|
+
* because policies outlive the server that generated them: a database still
|
|
133
|
+
* holding policies from before the fix, or a project whose server has not been
|
|
134
|
+
* upgraded yet, must not become a grant in either direction. Compile against
|
|
135
|
+
* this list, not against a single literal.
|
|
136
|
+
*
|
|
137
|
+
* No real user id is ever one of these, so a match is always "not signed in".
|
|
138
|
+
*
|
|
139
|
+
* @group Models
|
|
140
|
+
*/
|
|
141
|
+
var ANONYMOUS_USER_IDS = [ANONYMOUS_USER_ID, "anon"];
|
|
142
|
+
/** @group Models */
|
|
143
|
+
var policy = {
|
|
144
|
+
true: () => ({ kind: "true" }),
|
|
145
|
+
false: () => ({ kind: "false" }),
|
|
146
|
+
and: (...operands) => ({
|
|
147
|
+
kind: "and",
|
|
148
|
+
operands
|
|
149
|
+
}),
|
|
150
|
+
or: (...operands) => ({
|
|
151
|
+
kind: "or",
|
|
152
|
+
operands
|
|
153
|
+
}),
|
|
154
|
+
not: (operand) => ({
|
|
155
|
+
kind: "not",
|
|
156
|
+
operand
|
|
157
|
+
}),
|
|
158
|
+
compare: (left, op, right) => ({
|
|
159
|
+
kind: "compare",
|
|
160
|
+
op,
|
|
161
|
+
left,
|
|
162
|
+
right
|
|
163
|
+
}),
|
|
164
|
+
rolesOverlap: (roles) => ({
|
|
165
|
+
kind: "rolesOverlap",
|
|
166
|
+
roles
|
|
167
|
+
}),
|
|
168
|
+
rolesContain: (roles) => ({
|
|
169
|
+
kind: "rolesContain",
|
|
170
|
+
roles
|
|
171
|
+
}),
|
|
172
|
+
authenticated: () => ({ kind: "authenticated" }),
|
|
173
|
+
serverContext: () => ({ kind: "serverContext" }),
|
|
174
|
+
existsIn: (args) => ({
|
|
175
|
+
kind: "existsIn",
|
|
176
|
+
collection: args.collection,
|
|
177
|
+
where: args.where
|
|
178
|
+
}),
|
|
179
|
+
raw: (sql) => ({
|
|
180
|
+
kind: "raw",
|
|
181
|
+
sql
|
|
182
|
+
}),
|
|
183
|
+
field: (name) => ({
|
|
184
|
+
kind: "field",
|
|
185
|
+
name
|
|
186
|
+
}),
|
|
187
|
+
outerField: (name) => ({
|
|
188
|
+
kind: "outerField",
|
|
189
|
+
name
|
|
190
|
+
}),
|
|
191
|
+
literal: (value) => ({
|
|
192
|
+
kind: "literal",
|
|
193
|
+
value
|
|
194
|
+
}),
|
|
195
|
+
authUid: () => ({ kind: "authUid" }),
|
|
196
|
+
authRoles: () => ({ kind: "authRoles" })
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
|
|
200
|
+
* serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
|
|
201
|
+
* verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
|
|
202
|
+
*
|
|
203
|
+
* @group Errors
|
|
204
|
+
*/
|
|
205
|
+
var ListLimitError = class ListLimitError extends RebaseApiError {
|
|
206
|
+
/** The ceiling that was exceeded — what the caller should page by instead. */
|
|
207
|
+
maxLimit;
|
|
208
|
+
constructor(message, maxLimit) {
|
|
209
|
+
super(message, {
|
|
210
|
+
status: 400,
|
|
211
|
+
code: "INVALID_LIMIT"
|
|
212
|
+
});
|
|
213
|
+
this.name = "ListLimitError";
|
|
214
|
+
this.maxLimit = maxLimit;
|
|
215
|
+
Object.setPrototypeOf(this, ListLimitError.prototype);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
220
|
+
*
|
|
221
|
+
* - An absent / blank limit falls back to the mode default:
|
|
222
|
+
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
223
|
+
* - A limit that is present must be an integer in `[1, maxLimit]`. Anything
|
|
224
|
+
* else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
|
|
225
|
+
* {@link ListLimitError} rather than being coerced into range, because every
|
|
226
|
+
* coercion answers a question the caller did not ask with a page it cannot
|
|
227
|
+
* tell apart from the whole collection.
|
|
228
|
+
*
|
|
229
|
+
* The return is never `undefined` — no ingress that routes its client limit
|
|
230
|
+
* through this can produce an unbounded read.
|
|
231
|
+
*
|
|
232
|
+
* @throws {ListLimitError} when a present `limit` is not an integer in range.
|
|
233
|
+
*/
|
|
234
|
+
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
235
|
+
const maxLimit = opts.maxLimit ?? 1e3;
|
|
236
|
+
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
237
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : Number(String(rawLimit).trim());
|
|
238
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new ListLimitError(`Invalid \`limit\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`, maxLimit);
|
|
239
|
+
if (parsed > maxLimit) throw new ListLimitError(`\`limit\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows per read and page through the rest with \`offset\` — answering with a smaller page would be indistinguishable from there being no more rows.`, maxLimit);
|
|
240
|
+
return parsed;
|
|
241
|
+
}
|
|
242
|
+
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
60
245
|
//#region ../common/src/util/common.ts
|
|
61
246
|
var DEFAULT_ONE_OF_TYPE = "type";
|
|
62
247
|
var DEFAULT_ONE_OF_VALUE = "value";
|
|
@@ -1408,9 +1593,28 @@ function legacyForeignKeyName(name) {
|
|
|
1408
1593
|
* and `TextDecoder` are standard in both runtimes and need no ambient types.
|
|
1409
1594
|
*/
|
|
1410
1595
|
function toPostgresIdentifier(name) {
|
|
1596
|
+
return truncateToBytes(name, 63);
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* {@link toPostgresIdentifier} with the bound lifted to a parameter.
|
|
1600
|
+
*
|
|
1601
|
+
* Exists for names that end in something load-bearing. Truncating at 63 keeps
|
|
1602
|
+
* the *head* of a name and discards the tail, which is right for a descriptive
|
|
1603
|
+
* identifier and wrong for a hashed one: the hash is the part that makes it
|
|
1604
|
+
* unique, and it is at the end. A caller that appends a fingerprint truncates
|
|
1605
|
+
* the readable head to `63 - <tail>` itself and then appends, so the bound is
|
|
1606
|
+
* still 63 and the hash always survives.
|
|
1607
|
+
*
|
|
1608
|
+
* `contracts/derived-names.txt` records what the alternative costs — a foreign
|
|
1609
|
+
* key frozen as `..._corres`, its `_fkey` suffix truncated away, so a second
|
|
1610
|
+
* foreign key on that table would derive a byte-identical name.
|
|
1611
|
+
*
|
|
1612
|
+
* One truncation rule, in one function, so the two cannot drift.
|
|
1613
|
+
*/
|
|
1614
|
+
function truncateToBytes(name, maxBytes) {
|
|
1411
1615
|
const bytes = new TextEncoder().encode(name);
|
|
1412
|
-
if (bytes.byteLength <=
|
|
1413
|
-
return new TextDecoder("utf-8").decode(bytes.subarray(0,
|
|
1616
|
+
if (bytes.byteLength <= maxBytes) return name;
|
|
1617
|
+
return new TextDecoder("utf-8").decode(bytes.subarray(0, maxBytes)).replace(/�+$/, "");
|
|
1414
1618
|
}
|
|
1415
1619
|
/**
|
|
1416
1620
|
* The API name a database column is served under.
|
|
@@ -1965,8 +2169,45 @@ function getTableName(collection) {
|
|
|
1965
2169
|
if (isRelationalCollectionConfig(collection)) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
1966
2170
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
1967
2171
|
}
|
|
2172
|
+
/**
|
|
2173
|
+
* A JavaScript identifier: what a generated `export const <name> =` needs.
|
|
2174
|
+
*
|
|
2175
|
+
* Deliberately the same shape the two schema generators already define
|
|
2176
|
+
* privately — this is the third place that needed it, and the first two guard
|
|
2177
|
+
* property keys and member accesses while nothing guarded the variable name
|
|
2178
|
+
* itself.
|
|
2179
|
+
*/
|
|
2180
|
+
var JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2181
|
+
/**
|
|
2182
|
+
* The variable name a generated table is bound to.
|
|
2183
|
+
*
|
|
2184
|
+
* Camel-cases underscores, and then guarantees the result is a legal
|
|
2185
|
+
* identifier. It did only the first, so a table name that is legal in Postgres
|
|
2186
|
+
* and not in JavaScript produced a `schema.generated.ts` that does not parse:
|
|
2187
|
+
*
|
|
2188
|
+
* `2024_archive` → `export const 2024Archive = pgTable(…)`
|
|
2189
|
+
* "An identifier or keyword cannot immediately follow
|
|
2190
|
+
* a numeric literal"
|
|
2191
|
+
* `reporting.events` → `export const reporting.events = pgTable(…)`
|
|
2192
|
+
* "',' expected"
|
|
2193
|
+
*
|
|
2194
|
+
* That file is imported by the server, so the failure is not one broken
|
|
2195
|
+
* collection — `rebase build` and `db push` fail at tsc for the whole
|
|
2196
|
+
* directory. And it is reachable from a documented flow: `rebase init` against
|
|
2197
|
+
* a database holding a table called `2024_archive` writes a collection file
|
|
2198
|
+
* that parses and a schema file that does not.
|
|
2199
|
+
*
|
|
2200
|
+
* **A no-op for every name that already worked**, which is what makes changing
|
|
2201
|
+
* a derived name safe here: the only inputs whose output changes are the ones
|
|
2202
|
+
* that produced a syntax error, and nothing can be running against those.
|
|
2203
|
+
* Separators become camel case rather than disappearing, so `reporting.events`
|
|
2204
|
+
* and `reporting_events` do not collide into one variable.
|
|
2205
|
+
*/
|
|
1968
2206
|
function getTableVarName(tableName) {
|
|
1969
|
-
|
|
2207
|
+
const camel = tableName.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
|
2208
|
+
if (JS_IDENTIFIER.test(camel)) return camel;
|
|
2209
|
+
const sanitised = camel.replace(/[^A-Za-z0-9_$]+([A-Za-z0-9])?/g, (_, char) => char ? char.toUpperCase() : "").replace(/^([0-9])/, "t$1");
|
|
2210
|
+
return JS_IDENTIFIER.test(sanitised) ? sanitised : `t${sanitised}`;
|
|
1970
2211
|
}
|
|
1971
2212
|
function getEnumVarName(tableName, propName) {
|
|
1972
2213
|
return `${getTableVarName(tableName)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
|
|
@@ -2479,7 +2720,7 @@ function compile(expr, scope) {
|
|
|
2479
2720
|
}
|
|
2480
2721
|
case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
|
|
2481
2722
|
case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
|
|
2482
|
-
case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
|
|
2723
|
+
case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral$1).join(", ")})`;
|
|
2483
2724
|
case "serverContext": return `${RLS_UID_SQL} IS NULL`;
|
|
2484
2725
|
case "existsIn": return compileExistsIn(expr, scope);
|
|
2485
2726
|
case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
@@ -2518,7 +2759,7 @@ function operandToSql(operand, scope) {
|
|
|
2518
2759
|
switch (operand.kind) {
|
|
2519
2760
|
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
2520
2761
|
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
2521
|
-
case "literal": return quoteLiteral(operand.value);
|
|
2762
|
+
case "literal": return quoteLiteral$1(operand.value);
|
|
2522
2763
|
case "authUid": return RLS_UID_SQL;
|
|
2523
2764
|
case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
|
|
2524
2765
|
}
|
|
@@ -2679,7 +2920,7 @@ function quoteColumnIdentifier(name) {
|
|
|
2679
2920
|
if (BARE_IDENTIFIER.test(name) && !RESERVED_SQL_WORDS.has(name)) return name;
|
|
2680
2921
|
return `"${name.replace(/"/g, "\"\"")}"`;
|
|
2681
2922
|
}
|
|
2682
|
-
function quoteLiteral(value) {
|
|
2923
|
+
function quoteLiteral$1(value) {
|
|
2683
2924
|
if (value === null) return "NULL";
|
|
2684
2925
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
2685
2926
|
if (typeof value === "number") return String(value);
|
|
@@ -2745,7 +2986,7 @@ var DEFAULT_GUARDED_OPS = [
|
|
|
2745
2986
|
"delete"
|
|
2746
2987
|
];
|
|
2747
2988
|
/** Whether a collection is flagged as an authentication collection. */
|
|
2748
|
-
function isAuthCollection
|
|
2989
|
+
function isAuthCollection(collection) {
|
|
2749
2990
|
const auth = collection.auth;
|
|
2750
2991
|
return auth === true || typeof auth === "object" && auth?.enabled === true;
|
|
2751
2992
|
}
|
|
@@ -2783,7 +3024,7 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2783
3024
|
const explicit = [...collection.securityRules ?? []];
|
|
2784
3025
|
const tableName = getTableName(collection);
|
|
2785
3026
|
const injected = [];
|
|
2786
|
-
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return isAuthCollection
|
|
3027
|
+
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return isAuthCollection(collection) ? [...explicit, adminWriteGate(tableName)] : explicit;
|
|
2787
3028
|
injected.push({
|
|
2788
3029
|
name: `${tableName}_default_admin_read`,
|
|
2789
3030
|
operations: ["select"],
|
|
@@ -2795,7 +3036,7 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2795
3036
|
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2796
3037
|
check: SERVER_OR_ADMIN_EXPR$1
|
|
2797
3038
|
});
|
|
2798
|
-
if (isAuthCollection
|
|
3039
|
+
if (isAuthCollection(collection)) {
|
|
2799
3040
|
injected.push({
|
|
2800
3041
|
name: `${tableName}_default_self_read`,
|
|
2801
3042
|
operations: ["select"],
|
|
@@ -2805,6 +3046,22 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2805
3046
|
}
|
|
2806
3047
|
return [...explicit, ...injected];
|
|
2807
3048
|
}
|
|
3049
|
+
/**
|
|
3050
|
+
* The framework defaults that {@link getEffectiveSecurityRules} would add to a
|
|
3051
|
+
* collection, without the author's own rules.
|
|
3052
|
+
*
|
|
3053
|
+
* These policies appear in the database under names the author never wrote, and
|
|
3054
|
+
* a permissive policy ORs with every other permissive policy — so someone
|
|
3055
|
+
* reading their `securityRules` and then the real ACL sees more access than they
|
|
3056
|
+
* declared. Dropping them by hand does nothing either: `db push` is declarative,
|
|
3057
|
+
* so the next push asserts them again. Callers use this to say, in the generated
|
|
3058
|
+
* DDL, which policies are injected and how to take them off.
|
|
3059
|
+
*/
|
|
3060
|
+
function getInjectedSecurityRules(collection) {
|
|
3061
|
+
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return isAuthCollection(collection) ? [adminWriteGate(getTableName(collection))] : [];
|
|
3062
|
+
const explicitCount = (collection.securityRules ?? []).length;
|
|
3063
|
+
return getEffectiveSecurityRules(collection).slice(explicitCount);
|
|
3064
|
+
}
|
|
2808
3065
|
//#endregion
|
|
2809
3066
|
//#region ../common/src/util/junction-policies.ts
|
|
2810
3067
|
var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
@@ -3281,25 +3538,8 @@ function getJunctionSecurityRules(spec) {
|
|
|
3281
3538
|
});
|
|
3282
3539
|
})))();
|
|
3283
3540
|
/**
|
|
3284
|
-
*
|
|
3285
|
-
*
|
|
3286
|
-
* One definition, three call sites, because they used to disagree. For the same
|
|
3287
|
-
* `columnType: "varchar"` property the DDL generator emitted `VARCHAR(255)`
|
|
3288
|
-
* while the Drizzle generator emitted a bare `varchar("col")` — which Postgres
|
|
3289
|
-
* reads as *unbounded* — so which of the two you ran decided whether the column
|
|
3290
|
-
* had a limit at all. Introspection then dropped the length entirely, so reading
|
|
3291
|
-
* an existing `character varying(500)` column back and regenerating it produced
|
|
3292
|
-
* a `VARCHAR(255)`: a silent narrowing of a column with data already in it.
|
|
3293
|
-
*
|
|
3294
|
-
* `validation.max` is the property's own statement about how long the value may
|
|
3295
|
-
* be, so it is the only sensible source for the column's width — and it keeps
|
|
3296
|
-
* the constraint the database enforces in step with the one the app enforces,
|
|
3297
|
-
* rather than inventing a second, different limit underneath it.
|
|
3541
|
+
* Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
|
|
3298
3542
|
*/
|
|
3299
|
-
function resolveStringColumnLength(prop) {
|
|
3300
|
-
const max = prop.validation?.max;
|
|
3301
|
-
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : 255;
|
|
3302
|
-
}
|
|
3303
3543
|
//#endregion
|
|
3304
3544
|
//#region ../../node_modules/.pnpm/fast-equals@6.0.2/node_modules/fast-equals/dist/es/index.mjs
|
|
3305
3545
|
var { getOwnPropertyNames, getOwnPropertySymbols } = Object;
|
|
@@ -4931,6 +5171,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4931
5171
|
orderBy: normalizeOrderBy(params?.orderBy),
|
|
4932
5172
|
searchString: params?.searchString,
|
|
4933
5173
|
searchExplain: params?.searchExplain,
|
|
5174
|
+
vectorSearch: params?.vectorSearch,
|
|
4934
5175
|
onUpdate: (entities) => {
|
|
4935
5176
|
onUpdate({
|
|
4936
5177
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
@@ -5206,502 +5447,244 @@ function buildSdkData(driver) {
|
|
|
5206
5447
|
return wrapAsSdkData(buildRebaseData(driver));
|
|
5207
5448
|
}
|
|
5208
5449
|
//#endregion
|
|
5209
|
-
//#region src/schema/
|
|
5210
|
-
/**
|
|
5211
|
-
*
|
|
5212
|
-
*
|
|
5213
|
-
*
|
|
5214
|
-
* the
|
|
5215
|
-
*
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
*
|
|
5226
|
-
* ## Why the expressions look the way they do
|
|
5227
|
-
*
|
|
5228
|
-
* A `GENERATED ALWAYS AS … STORED` expression must be strictly IMMUTABLE, and
|
|
5229
|
-
* Postgres is stricter here than intuition. Verified against PostgreSQL 18:
|
|
5230
|
-
*
|
|
5231
|
-
* | expression | immutable |
|
|
5232
|
-
* |-----------------------------------------|-----------|
|
|
5233
|
-
* | `to_tsvector('spanish', col)` | yes |
|
|
5234
|
-
* | `to_tsvector(col)` (1-arg) | **no** — depends on `default_text_search_config` |
|
|
5235
|
-
* | `array_to_string(col, ' ')` | **no** |
|
|
5236
|
-
* | `col::text` on `text[]` | **no** |
|
|
5237
|
-
* | `to_jsonb(col)` | **no** |
|
|
5238
|
-
* | `unaccent(col)` | **no** — dictionary lookup is STABLE |
|
|
5239
|
-
* | `jsonb_to_tsvector('spanish', j, '["string"]')` | yes |
|
|
5240
|
-
* | `setweight(...) || setweight(...)` | yes |
|
|
5241
|
-
*
|
|
5242
|
-
* Three of the four things a real search column needs are therefore unavailable
|
|
5243
|
-
* directly, which is why {@link searchHelperFunctions} exists: each wraps a
|
|
5244
|
-
* stable built-in in an SQL function declared IMMUTABLE. That declaration is a
|
|
5245
|
-
* promise, and it is a true one for these three — array joining, JSON string
|
|
5246
|
-
* extraction and accent folding are all deterministic for a given input; the
|
|
5247
|
-
* built-ins are marked stable only because they must account for element types
|
|
5248
|
-
* and dictionaries in general.
|
|
5249
|
-
*
|
|
5250
|
-
* The alternative was to skip `unaccent` and text arrays entirely. That is not
|
|
5251
|
-
* a real option in an accented language: Postgres stems `auditoría` to
|
|
5252
|
-
* `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
|
|
5253
|
-
* typed without accents misses every row that carries them.
|
|
5254
|
-
*/
|
|
5255
|
-
/** Schema-qualified so a collection outside `public` still resolves them. */
|
|
5256
|
-
var HELPER_SCHEMA = "public";
|
|
5257
|
-
/**
|
|
5258
|
-
* Names of the helper functions. Frozen: they are recorded in the stored
|
|
5259
|
-
* generation expression of every search column ever created, so renaming one
|
|
5260
|
-
* orphans every table that already has a search column.
|
|
5261
|
-
*/
|
|
5262
|
-
var SEARCH_TEXT_FN = `${HELPER_SCHEMA}.rebase_search_text`;
|
|
5263
|
-
var SEARCH_UNACCENT_FN = `${HELPER_SCHEMA}.rebase_search_unaccent`;
|
|
5264
|
-
/** Raised when a `search` block names something that cannot be searched. */
|
|
5265
|
-
var SearchConfigError = class extends Error {
|
|
5266
|
-
constructor(message) {
|
|
5267
|
-
super(message);
|
|
5268
|
-
this.name = "SearchConfigError";
|
|
5450
|
+
//#region src/schema/collection-index.ts
|
|
5451
|
+
/**
|
|
5452
|
+
* A declaration that cannot become an index.
|
|
5453
|
+
*
|
|
5454
|
+
* Thrown at build time, naming the collection and the array position, because
|
|
5455
|
+
* the alternative is a `CREATE INDEX` that fails during a push with a Postgres
|
|
5456
|
+
* error mentioning a column the author never wrote.
|
|
5457
|
+
*/
|
|
5458
|
+
var CollectionIndexConfigError = class extends Error {
|
|
5459
|
+
collectionSlug;
|
|
5460
|
+
position;
|
|
5461
|
+
constructor(collectionSlug, position, message) {
|
|
5462
|
+
super(`${collectionSlug}.indexes[${position}]: ${message}`);
|
|
5463
|
+
this.name = "CollectionIndexConfigError";
|
|
5464
|
+
this.collectionSlug = collectionSlug;
|
|
5465
|
+
this.position = position;
|
|
5269
5466
|
}
|
|
5270
5467
|
};
|
|
5271
|
-
/** The `search` block of a collection, or undefined when it has none. */
|
|
5272
|
-
var getSearchConfig = (collection) => isPostgresCollectionConfig(collection) ? collection.search : void 0;
|
|
5273
5468
|
/**
|
|
5274
|
-
*
|
|
5275
|
-
*
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
*
|
|
5281
|
-
*
|
|
5282
|
-
*
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5469
|
+
* `_ix`/`_ux` plus `_` plus 7 hex — the part of the name that must always
|
|
5470
|
+
* survive truncation, and therefore is never inside the truncated portion.
|
|
5471
|
+
*/
|
|
5472
|
+
var NAME_SUFFIX_BYTES = 11;
|
|
5473
|
+
var isOrderedMethod = (method) => method === "btree";
|
|
5474
|
+
/**
|
|
5475
|
+
* The parts of an index that decide what it *is*.
|
|
5476
|
+
*
|
|
5477
|
+
* A semantic projection, not the rendered statement — the same arrangement as
|
|
5478
|
+
* `getPolicyNameHash`, and for the same reason. A change to how this file
|
|
5479
|
+
* formats SQL (eliding a default `USING btree`, quoting differently, emitting
|
|
5480
|
+
* `NULLS LAST` explicitly) must not silently rename every index in every
|
|
5481
|
+
* deployed database. Hashing generator output would make every cosmetic edit a
|
|
5482
|
+
* fleet-wide DROP + CREATE.
|
|
5483
|
+
*
|
|
5484
|
+
* `reason` is deliberately absent: rewording a comment must not rebuild an
|
|
5485
|
+
* index. `nulls` is the *effective* placement, so writing Postgres's own
|
|
5486
|
+
* default down is a no-op rather than a redefinition.
|
|
5487
|
+
*
|
|
5488
|
+
* `v` is the only escape hatch, and it is expensive on purpose: bumping it
|
|
5489
|
+
* renames every index in the field.
|
|
5490
|
+
*/
|
|
5491
|
+
var indexFingerprint = (spec) => sha1Hex(JSON.stringify({
|
|
5492
|
+
v: 1,
|
|
5493
|
+
s: spec.schema,
|
|
5494
|
+
t: spec.table,
|
|
5495
|
+
m: spec.method,
|
|
5496
|
+
u: spec.unique,
|
|
5497
|
+
k: spec.keys.map((k) => [
|
|
5498
|
+
k.column,
|
|
5499
|
+
k.direction,
|
|
5500
|
+
k.nulls
|
|
5501
|
+
]),
|
|
5502
|
+
i: spec.include,
|
|
5503
|
+
w: spec.predicate
|
|
5504
|
+
})).substring(0, 7);
|
|
5505
|
+
/**
|
|
5506
|
+
* `<table>_<columns>_ix_<hash>`, or `_ux_` when unique.
|
|
5507
|
+
*
|
|
5508
|
+
* Truncation eats the readable head and never the hash. `toPostgresIdentifier`
|
|
5509
|
+
* truncates the whole string at 63 bytes, which on a hashed name would cut off
|
|
5510
|
+
* the one part that makes it unique — the failure already frozen into
|
|
5511
|
+
* `contracts/derived-names.txt`, where a foreign key is recorded with its
|
|
5512
|
+
* `_fkey` suffix truncated away, so a second foreign key on that table would
|
|
5513
|
+
* derive a byte-identical name.
|
|
5514
|
+
*/
|
|
5515
|
+
var deriveIndexName = (spec) => {
|
|
5516
|
+
const suffix = `_${spec.unique ? "ux" : "ix"}_${indexFingerprint(spec)}`;
|
|
5517
|
+
return `${truncateToBytes(`${spec.table}_${spec.keys.map((k) => k.column).join("_")}`, 63 - NAME_SUFFIX_BYTES)}${suffix}`;
|
|
5291
5518
|
};
|
|
5292
|
-
var
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
* Deliberately narrower than `getSqlColumnType`: search only cares whether a
|
|
5297
|
-
* value reaches text, and the mapping from property to *physical* type is
|
|
5298
|
-
* asserted against `getSqlColumnType` in the contract test rather than
|
|
5299
|
-
* duplicated here.
|
|
5300
|
-
*
|
|
5301
|
-
* Returns null for anything that is not text-bearing, which the caller turns
|
|
5302
|
-
* into a boot error naming the property.
|
|
5303
|
-
*/
|
|
5304
|
-
var classify = (prop) => {
|
|
5305
|
-
switch (prop.type) {
|
|
5306
|
-
case "string": {
|
|
5307
|
-
const sp = prop;
|
|
5308
|
-
if (sp.enum) return {
|
|
5309
|
-
kind: "text",
|
|
5310
|
-
reason: "enum"
|
|
5311
|
-
};
|
|
5312
|
-
if (sp.isId === "uuid" || sp.columnType === "uuid") return {
|
|
5313
|
-
kind: "text",
|
|
5314
|
-
reason: "uuid"
|
|
5315
|
-
};
|
|
5316
|
-
return { kind: "text" };
|
|
5317
|
-
}
|
|
5318
|
-
case "map":
|
|
5319
|
-
if (prop.columnType === "json") return {
|
|
5320
|
-
kind: "jsonb",
|
|
5321
|
-
reason: "json"
|
|
5322
|
-
};
|
|
5323
|
-
return { kind: "jsonb" };
|
|
5324
|
-
case "array": {
|
|
5325
|
-
const ap = prop;
|
|
5326
|
-
let colType = ap.columnType;
|
|
5327
|
-
if (!colType && ap.of && !Array.isArray(ap.of)) {
|
|
5328
|
-
const of = ap.of;
|
|
5329
|
-
if (of.type === "string") colType = "text[]";
|
|
5330
|
-
else if (of.type === "number") colType = of.validation?.integer ? "integer[]" : "numeric[]";
|
|
5331
|
-
else if (of.type === "boolean") colType = "boolean[]";
|
|
5332
|
-
}
|
|
5333
|
-
if (colType === "text[]") return { kind: "text_array" };
|
|
5334
|
-
if (colType === "json") return {
|
|
5335
|
-
kind: "jsonb",
|
|
5336
|
-
reason: "json"
|
|
5337
|
-
};
|
|
5338
|
-
if (colType === "integer[]" || colType === "boolean[]" || colType === "numeric[]") return {
|
|
5339
|
-
kind: "text_array",
|
|
5340
|
-
reason: "non_text_array"
|
|
5341
|
-
};
|
|
5342
|
-
return { kind: "jsonb" };
|
|
5343
|
-
}
|
|
5344
|
-
default: return null;
|
|
5345
|
-
}
|
|
5346
|
-
};
|
|
5347
|
-
var normalize = (inner, unaccent) => unaccent ? `${SEARCH_UNACCENT_FN}(${inner})` : inner;
|
|
5348
|
-
/** SQL reading one field as plain text, before normalization. */
|
|
5349
|
-
var rawTextSql = (field) => {
|
|
5350
|
-
const col = `"${field.column}"`;
|
|
5351
|
-
if (field.kind === "text") return `coalesce(${col}, '')`;
|
|
5352
|
-
if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
|
|
5353
|
-
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))`;
|
|
5354
|
-
};
|
|
5355
|
-
var quote = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
5356
|
-
/**
|
|
5357
|
-
* Resolve and validate one declared field path.
|
|
5358
|
-
*
|
|
5359
|
-
* A path that does not resolve throws. The whole point of an explicit block is
|
|
5360
|
-
* that the author knows what is indexed; a silently dropped field would make it
|
|
5361
|
-
* a guess again, and the failure — a search that returns nothing for content
|
|
5362
|
-
* that is plainly in the row — is invisible from the outside.
|
|
5363
|
-
*/
|
|
5364
|
-
var resolveField = (entry, collection, cfg) => {
|
|
5365
|
-
const path = typeof entry === "string" ? entry : entry.path;
|
|
5366
|
-
const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? "B";
|
|
5367
|
-
const where = `${collection.slug}.search`;
|
|
5368
|
-
if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
|
|
5369
|
-
const [head, ...rest] = path.split(".");
|
|
5370
|
-
const prop = collection.properties?.[head];
|
|
5371
|
-
if (!prop) throw new SearchConfigError(`${where}: "${path}" starts at property "${head}", which this collection does not declare. Known properties: ${Object.keys(collection.properties ?? {}).join(", ")}.`);
|
|
5372
|
-
const classified = classify(prop);
|
|
5373
|
-
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).`);
|
|
5374
|
-
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.`);
|
|
5375
|
-
if (classified.reason === "uuid") throw new SearchConfigError(`${where}: "${path}" is a UUID column. Look it up by id rather than searching it.`);
|
|
5376
|
-
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.`);
|
|
5377
|
-
if (classified.reason === "non_text_array") throw new SearchConfigError(`${where}: "${path}" is an array of numbers or booleans. Only \`string[]\` carries text to search.`);
|
|
5378
|
-
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.`);
|
|
5379
|
-
const column = columnNameOf(head, prop);
|
|
5380
|
-
const textSql = normalize(rawTextSql({
|
|
5381
|
-
column,
|
|
5382
|
-
jsonPath: rest,
|
|
5383
|
-
kind: classified.kind
|
|
5384
|
-
}), cfg.unaccent === true);
|
|
5385
|
-
const language = cfg.language ?? "simple";
|
|
5386
|
-
return {
|
|
5387
|
-
path,
|
|
5388
|
-
column,
|
|
5389
|
-
jsonPath: rest,
|
|
5390
|
-
kind: classified.kind,
|
|
5391
|
-
weight,
|
|
5392
|
-
sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
|
|
5393
|
-
textSql
|
|
5394
|
-
};
|
|
5519
|
+
var quoteLiteral = (value) => {
|
|
5520
|
+
if (typeof value === "number") return String(value);
|
|
5521
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
5522
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
5395
5523
|
};
|
|
5396
|
-
/**
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
const cfg = getSearchConfig(collection);
|
|
5405
|
-
if (!cfg) return void 0;
|
|
5406
|
-
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.`);
|
|
5407
|
-
const table = getTableName(collection);
|
|
5408
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
5409
|
-
const column = cfg.column ?? "search_vector";
|
|
5410
|
-
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.`);
|
|
5411
|
-
const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
|
|
5412
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5413
|
-
for (const f of fields) {
|
|
5414
|
-
if (seen.has(f.path)) throw new SearchConfigError(`${collection.slug}.search: "${f.path}" is listed twice.`);
|
|
5415
|
-
seen.add(f.path);
|
|
5524
|
+
/** Render a resolved predicate as the body of a `WHERE` clause. */
|
|
5525
|
+
var renderPredicate = (predicate) => {
|
|
5526
|
+
if ("and" in predicate) return predicate.and.map(renderPredicate).join(" AND ");
|
|
5527
|
+
switch (predicate.op) {
|
|
5528
|
+
case "is null":
|
|
5529
|
+
case "is not null": return `"${predicate.column}" ${predicate.op.toUpperCase()}`;
|
|
5530
|
+
case "in": return `"${predicate.column}" IN (${predicate.value.map(quoteLiteral).join(", ")})`;
|
|
5531
|
+
default: return `"${predicate.column}" ${predicate.op} ${quoteLiteral(predicate.value)}`;
|
|
5416
5532
|
}
|
|
5417
|
-
const extensions = [];
|
|
5418
|
-
if (cfg.unaccent) extensions.push("unaccent");
|
|
5419
|
-
if (cfg.fuzzy) extensions.push("pg_trgm");
|
|
5420
|
-
const spec = {
|
|
5421
|
-
schema,
|
|
5422
|
-
table,
|
|
5423
|
-
column,
|
|
5424
|
-
language: cfg.language ?? "simple",
|
|
5425
|
-
unaccent: cfg.unaccent === true,
|
|
5426
|
-
fields,
|
|
5427
|
-
expression: fields.map((f) => f.sql).join(" || "),
|
|
5428
|
-
indexName: toPostgresIdentifier(`${table}_${column}_gin`),
|
|
5429
|
-
extensions
|
|
5430
|
-
};
|
|
5431
|
-
if (cfg.fuzzy) {
|
|
5432
|
-
const fuzzyColumn = `${column}_text`;
|
|
5433
|
-
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.`);
|
|
5434
|
-
spec.fuzzy = {
|
|
5435
|
-
column: fuzzyColumn,
|
|
5436
|
-
expression: fields.map((f) => f.textSql).join(" || ' ' || "),
|
|
5437
|
-
indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
|
|
5438
|
-
threshold: cfg.fuzzyThreshold ?? .3
|
|
5439
|
-
};
|
|
5440
|
-
}
|
|
5441
|
-
return spec;
|
|
5442
5533
|
};
|
|
5443
5534
|
/**
|
|
5444
|
-
* The
|
|
5445
|
-
*
|
|
5446
|
-
* `
|
|
5447
|
-
*
|
|
5448
|
-
*
|
|
5449
|
-
*
|
|
5450
|
-
*
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
const
|
|
5457
|
-
|
|
5458
|
-
|
|
5535
|
+
* The `CREATE INDEX` for one spec.
|
|
5536
|
+
*
|
|
5537
|
+
* `concurrently` is a parameter rather than a string replacement on the way
|
|
5538
|
+
* out. `search-column.ts` and `vector-index.ts` both reach for
|
|
5539
|
+
* `.replace("CREATE INDEX IF NOT EXISTS", …)` instead, which silently does
|
|
5540
|
+
* nothing for a UNIQUE index — the rendered text is `CREATE UNIQUE INDEX …`
|
|
5541
|
+
* and the pattern never matches.
|
|
5542
|
+
*/
|
|
5543
|
+
var collectionIndexStatement = (spec, options = {}) => {
|
|
5544
|
+
const unique = spec.unique ? "UNIQUE " : "";
|
|
5545
|
+
const concurrently = options.concurrently ? "CONCURRENTLY " : "";
|
|
5546
|
+
const ifNotExists = options.ifNotExists ? "IF NOT EXISTS " : "";
|
|
5547
|
+
const using = spec.method === "btree" ? "" : ` USING ${spec.method}`;
|
|
5548
|
+
const keys = spec.keys.map((k) => {
|
|
5549
|
+
if (!isOrderedMethod(spec.method)) return `"${k.column}"`;
|
|
5550
|
+
const direction = k.direction === "desc" ? " DESC" : "";
|
|
5551
|
+
const impliedNulls = k.direction === "desc" ? "first" : "last";
|
|
5552
|
+
const nulls = k.nulls === impliedNulls ? "" : ` NULLS ${k.nulls.toUpperCase()}`;
|
|
5553
|
+
return `"${k.column}"${direction}${nulls}`;
|
|
5554
|
+
}).join(", ");
|
|
5555
|
+
const include = spec.include.length > 0 ? ` INCLUDE (${spec.include.map((c) => `"${c}"`).join(", ")})` : "";
|
|
5556
|
+
const where = spec.predicate ? ` WHERE ${renderPredicate(spec.predicate)}` : "";
|
|
5557
|
+
return `CREATE ${unique}INDEX ${concurrently}${ifNotExists}"${spec.indexName}" ON "${spec.schema}"."${spec.table}"${using} (${keys})${include}${where};`;
|
|
5459
5558
|
};
|
|
5559
|
+
var collectionIndexStatements = (specs, options = {}) => specs.map((spec) => collectionIndexStatement(spec, options));
|
|
5560
|
+
var relationOf = (collection, propKey) => resolveCollectionRelations(collection)[propKey];
|
|
5460
5561
|
/**
|
|
5461
|
-
*
|
|
5562
|
+
* The column a property key indexes.
|
|
5462
5563
|
*
|
|
5463
|
-
* `
|
|
5464
|
-
* `
|
|
5465
|
-
*
|
|
5466
|
-
*
|
|
5467
|
-
* So the moment that schema exists, `CREATE EXTENSION unaccent` puts the
|
|
5468
|
-
* dictionary in `rebase`, and every reference to `public.unaccent` below fails
|
|
5469
|
-
* with "text search dictionary does not exist". Observed, not theorised.
|
|
5470
|
-
*/
|
|
5471
|
-
var searchExtensionStatements = (spec) => spec.extensions.map((e) => `CREATE EXTENSION IF NOT EXISTS ${e} WITH SCHEMA ${HELPER_SCHEMA};`);
|
|
5472
|
-
/**
|
|
5473
|
-
* Index statements for the spec.
|
|
5564
|
+
* A `belongsTo` resolves to its `localKey` — `primaryCategory` becomes
|
|
5565
|
+
* `primary_category_id` — which is the case an index is most often wanted for
|
|
5566
|
+
* and the case where the property key and the column differ. Everything else
|
|
5567
|
+
* goes through `resolveColumnName`.
|
|
5474
5568
|
*
|
|
5475
|
-
*
|
|
5476
|
-
*
|
|
5477
|
-
*
|
|
5478
|
-
* statement against tables that are live and populated, and uses the
|
|
5479
|
-
* concurrent form instead; see `ensureSearchColumns`.
|
|
5569
|
+
* The other relation kinds have no local column at all: the foreign key lives
|
|
5570
|
+
* on the target's table, or in a junction. Indexing them here is refused
|
|
5571
|
+
* rather than resolved to a column that does not exist.
|
|
5480
5572
|
*/
|
|
5481
|
-
var
|
|
5482
|
-
const
|
|
5483
|
-
if (
|
|
5484
|
-
|
|
5573
|
+
var resolveIndexableColumn = (collection, propKey, resolveColumnName, fail) => {
|
|
5574
|
+
const relation = relationOf(collection, propKey);
|
|
5575
|
+
if (relation) {
|
|
5576
|
+
if (relation.kind === "belongsTo") return relation.localKey;
|
|
5577
|
+
fail(`"${propKey}" is a ${relation.kind} relation, which has no column on this table — the foreign key lives on "${relation.targetSlug}". Declare the index there.`);
|
|
5578
|
+
}
|
|
5579
|
+
const property = collection.properties?.[propKey];
|
|
5580
|
+
if (!property) fail(`"${propKey}" is not a property of this collection.`);
|
|
5581
|
+
return resolveColumnName(propKey, property);
|
|
5485
5582
|
};
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
*/
|
|
5493
|
-
var SEARCH_STAMP_PREFIX = "rebase:search:v1:";
|
|
5494
|
-
/**
|
|
5495
|
-
* A stable fingerprint of one generated column's expression.
|
|
5496
|
-
*
|
|
5497
|
-
* Why a stamp rather than reading the expression back: Postgres stores a
|
|
5498
|
-
* generated column's expression *parsed*, and hands it back deparsed — casts
|
|
5499
|
-
* made explicit, identifiers requoted, schema qualifications added or dropped
|
|
5500
|
-
* according to `search_path`. Comparing that text to the text we generated
|
|
5501
|
-
* would report drift on wording, and this comparison decides whether a boot
|
|
5502
|
-
* refuses, so a false positive is an outage. The stamp is written by the same
|
|
5503
|
-
* code that writes the column, so equality means what it says.
|
|
5504
|
-
*/
|
|
5505
|
-
var searchExpressionFingerprint = (expression) => `${SEARCH_STAMP_PREFIX}${createHash("sha256").update(expression).digest("hex").slice(0, 16)}`;
|
|
5506
|
-
/**
|
|
5507
|
-
* The stamps for a spec's generated columns — one per column, never shared.
|
|
5508
|
-
*
|
|
5509
|
-
* Per column on purpose: turning `fuzzy` on adds a second column and changes
|
|
5510
|
-
* nothing about the first, and a spec-wide fingerprint would report the
|
|
5511
|
-
* untouched `tsvector` column as drifted and refuse a boot over a change that
|
|
5512
|
-
* is purely additive.
|
|
5513
|
-
*/
|
|
5514
|
-
var searchColumnStamps = (spec) => {
|
|
5515
|
-
const stamp = (column, expression) => {
|
|
5516
|
-
const fingerprint = searchExpressionFingerprint(expression);
|
|
5517
|
-
return {
|
|
5583
|
+
var resolvePredicate = (collection, predicate, resolveColumnName, fail) => {
|
|
5584
|
+
if ("and" in predicate) return { and: predicate.and.map((p) => resolvePredicate(collection, p, resolveColumnName, fail)) };
|
|
5585
|
+
const column = resolveIndexableColumn(collection, predicate.prop, resolveColumnName, fail);
|
|
5586
|
+
switch (predicate.op) {
|
|
5587
|
+
case "is null":
|
|
5588
|
+
case "is not null": return {
|
|
5518
5589
|
column,
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5590
|
+
op: predicate.op
|
|
5591
|
+
};
|
|
5592
|
+
case "in":
|
|
5593
|
+
if (new Set(predicate.value).size !== predicate.value.length) fail(`the \`in\` list for "${predicate.prop}" repeats a value, which changes nothing.`);
|
|
5594
|
+
return {
|
|
5595
|
+
column,
|
|
5596
|
+
op: "in",
|
|
5597
|
+
value: [...predicate.value]
|
|
5598
|
+
};
|
|
5599
|
+
default: return {
|
|
5600
|
+
column,
|
|
5601
|
+
op: predicate.op,
|
|
5602
|
+
value: predicate.value
|
|
5522
5603
|
};
|
|
5523
|
-
};
|
|
5524
|
-
const stamps = [stamp(spec.column, spec.expression)];
|
|
5525
|
-
if (spec.fuzzy) stamps.push(stamp(spec.fuzzy.column, spec.fuzzy.expression));
|
|
5526
|
-
return stamps;
|
|
5527
|
-
};
|
|
5528
|
-
/**
|
|
5529
|
-
* The generated column names a collection's search block adds, if any.
|
|
5530
|
-
*
|
|
5531
|
-
* These are physical columns on the table, so `SELECT *` returns them. They are
|
|
5532
|
-
* an index in column form — a list of lexeme positions, or a concatenation of
|
|
5533
|
-
* every searchable field on the row — and nothing outside the query planner has
|
|
5534
|
-
* any use for them. Left in, every list response carries a second, larger copy
|
|
5535
|
-
* of the row's text.
|
|
5536
|
-
*/
|
|
5537
|
-
var searchColumnNames = (collection) => {
|
|
5538
|
-
let spec;
|
|
5539
|
-
try {
|
|
5540
|
-
spec = buildSearchColumnSpec(collection);
|
|
5541
|
-
} catch {
|
|
5542
|
-
return [];
|
|
5543
5604
|
}
|
|
5544
|
-
if (!spec) return [];
|
|
5545
|
-
return spec.fuzzy ? [spec.column, spec.fuzzy.column] : [spec.column];
|
|
5546
|
-
};
|
|
5547
|
-
/**
|
|
5548
|
-
* True for a column whose type only ever holds a search index.
|
|
5549
|
-
*
|
|
5550
|
-
* Independent of any collection config on purpose: an introspected database
|
|
5551
|
-
* (BaaS mode) can carry a `tsvector` column this framework never created —
|
|
5552
|
-
* Pagila's `film.fulltext` is the canonical one — and it should not be returned
|
|
5553
|
-
* to callers either. `isDerivedIndexColumn` already keeps such a column out of
|
|
5554
|
-
* the *properties*; this keeps it out of the *rows*.
|
|
5555
|
-
*/
|
|
5556
|
-
var isSearchIndexColumn = (column) => {
|
|
5557
|
-
const sqlType = typeof column?.getSQLType === "function" ? column.getSQLType().toLowerCase() : "";
|
|
5558
|
-
return sqlType === "tsvector" || sqlType === "tsquery";
|
|
5559
5605
|
};
|
|
5606
|
+
/** The primary key columns of a collection, for the "you already have this" refusal. */
|
|
5607
|
+
var primaryKeyColumns = (collection, resolveColumnName) => Object.entries(collection.properties ?? {}).filter(([, prop]) => prop && typeof prop === "object" && "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => resolveColumnName(key, prop));
|
|
5560
5608
|
/**
|
|
5561
|
-
*
|
|
5609
|
+
* Every index one collection declares, resolved and named.
|
|
5562
5610
|
*
|
|
5563
|
-
*
|
|
5564
|
-
*
|
|
5611
|
+
* Throws {@link CollectionIndexConfigError} rather than dropping a bad entry:
|
|
5612
|
+
* an index that silently does not exist is the failure mode this whole feature
|
|
5613
|
+
* is here to remove.
|
|
5565
5614
|
*/
|
|
5566
|
-
var
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
const
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5615
|
+
var buildCollectionIndexSpecs = (collection, resolveColumnName) => {
|
|
5616
|
+
if (!isPostgresCollectionConfig(collection)) return [];
|
|
5617
|
+
const declared = collection.indexes;
|
|
5618
|
+
if (!declared || declared.length === 0) return [];
|
|
5619
|
+
const slug = collection.slug ?? getTableName(collection);
|
|
5620
|
+
const schema = collection.schema ?? "public";
|
|
5621
|
+
const table = getTableName(collection);
|
|
5622
|
+
const pk = primaryKeyColumns(collection, resolveColumnName).sort().join(",");
|
|
5623
|
+
const specs = [];
|
|
5624
|
+
const byName = /* @__PURE__ */ new Map();
|
|
5625
|
+
declared.forEach((index, position) => {
|
|
5626
|
+
const fail = (message) => {
|
|
5627
|
+
throw new CollectionIndexConfigError(slug, position, message);
|
|
5628
|
+
};
|
|
5629
|
+
if (typeof index.reason !== "string" || index.reason.trim() === "") fail("`reason` is required — see the doc comment. An index nobody can justify is one nobody can delete.");
|
|
5630
|
+
if (!Array.isArray(index.on) || index.on.length === 0) fail("`on` must name at least one property.");
|
|
5631
|
+
if (index.on.length > 5) fail(`\`on\` has ${index.on.length} keys; the limit is 5. Payload columns belong in \`include\`.`);
|
|
5632
|
+
const method = index.using ?? "btree";
|
|
5633
|
+
const unique = method === "btree" && Boolean(index.unique);
|
|
5634
|
+
if (!isOrderedMethod(method)) {
|
|
5635
|
+
for (const key of index.on) if (typeof key !== "string" && ("direction" in key || "nulls" in key)) fail(`access method "${method}" does not support ASC/DESC or NULLS options.`);
|
|
5636
|
+
}
|
|
5637
|
+
const keys = index.on.map((key) => {
|
|
5638
|
+
const column = resolveIndexableColumn(collection, typeof key === "string" ? key : key.prop, resolveColumnName, fail);
|
|
5639
|
+
const direction = (typeof key === "string" ? void 0 : key.direction) ?? "asc";
|
|
5640
|
+
return {
|
|
5641
|
+
column,
|
|
5642
|
+
direction,
|
|
5643
|
+
nulls: (typeof key === "string" ? void 0 : key.nulls) ?? (direction === "desc" ? "first" : "last")
|
|
5644
|
+
};
|
|
5645
|
+
});
|
|
5646
|
+
const duplicateKey = keys.map((k) => k.column).find((c, i, all) => all.indexOf(c) !== i);
|
|
5647
|
+
if (duplicateKey) fail(`"${duplicateKey}" appears twice in \`on\`.`);
|
|
5648
|
+
if (keys.map((k) => k.column).sort().join(",") === pk && pk !== "") fail(`this is the primary key — "${table}_pkey" already indexes exactly these columns.`);
|
|
5649
|
+
const include = (index.include ?? []).map((propKey) => resolveIndexableColumn(collection, propKey, resolveColumnName, fail));
|
|
5650
|
+
const overlap = include.find((c) => keys.some((k) => k.column === c));
|
|
5651
|
+
if (overlap) fail(`"${overlap}" is in both \`on\` and \`include\`; Postgres rejects the overlap.`);
|
|
5652
|
+
if (unique && keys.length === 1) {
|
|
5653
|
+
const propKey = typeof index.on[0] === "string" ? index.on[0] : index.on[0].prop;
|
|
5654
|
+
if ((collection.properties?.[propKey])?.validation?.unique) fail(`"${propKey}" already declares \`validation.unique\`, which compiles to an inline UNIQUE. Two declarations of one guarantee — remove one.`);
|
|
5655
|
+
}
|
|
5656
|
+
const predicate = index.where ? resolvePredicate(collection, index.where, resolveColumnName, fail) : null;
|
|
5657
|
+
const withoutName = {
|
|
5658
|
+
schema,
|
|
5659
|
+
table,
|
|
5660
|
+
method,
|
|
5661
|
+
unique,
|
|
5662
|
+
keys,
|
|
5663
|
+
include,
|
|
5664
|
+
predicate,
|
|
5665
|
+
reason: index.reason
|
|
5666
|
+
};
|
|
5667
|
+
const indexName = deriveIndexName(withoutName);
|
|
5668
|
+
const clash = byName.get(indexName);
|
|
5669
|
+
if (clash !== void 0) fail(`derives the same name as indexes[${clash}] — they are the same index declared twice.`);
|
|
5670
|
+
byName.set(indexName, position);
|
|
5671
|
+
specs.push({
|
|
5672
|
+
...withoutName,
|
|
5673
|
+
indexName
|
|
5674
|
+
});
|
|
5675
|
+
});
|
|
5676
|
+
return specs;
|
|
5592
5677
|
};
|
|
5593
|
-
//#endregion
|
|
5594
|
-
//#region src/schema/auth-users-columns.ts
|
|
5595
|
-
/**
|
|
5596
|
-
* `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
|
|
5597
|
-
* — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
|
|
5598
|
-
* The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
|
|
5599
|
-
* separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
|
|
5600
|
-
* table that already holds a longer row.
|
|
5601
|
-
*/
|
|
5602
|
-
var AUTH_USERS_COLUMNS = [
|
|
5603
|
-
{
|
|
5604
|
-
column: "email",
|
|
5605
|
-
type: "TEXT",
|
|
5606
|
-
notNull: true
|
|
5607
|
-
},
|
|
5608
|
-
{
|
|
5609
|
-
column: "display_name",
|
|
5610
|
-
type: "TEXT"
|
|
5611
|
-
},
|
|
5612
|
-
{
|
|
5613
|
-
column: "photo_url",
|
|
5614
|
-
type: "TEXT"
|
|
5615
|
-
},
|
|
5616
|
-
{
|
|
5617
|
-
column: "roles",
|
|
5618
|
-
type: "TEXT[]",
|
|
5619
|
-
default: "'{}'",
|
|
5620
|
-
notNull: true
|
|
5621
|
-
},
|
|
5622
|
-
{
|
|
5623
|
-
column: "password_hash",
|
|
5624
|
-
type: "TEXT"
|
|
5625
|
-
},
|
|
5626
|
-
{
|
|
5627
|
-
column: "email_verified",
|
|
5628
|
-
type: "BOOLEAN",
|
|
5629
|
-
default: "FALSE",
|
|
5630
|
-
notNull: true
|
|
5631
|
-
},
|
|
5632
|
-
{
|
|
5633
|
-
column: "email_verification_token",
|
|
5634
|
-
type: "TEXT"
|
|
5635
|
-
},
|
|
5636
|
-
{
|
|
5637
|
-
column: "email_verification_sent_at",
|
|
5638
|
-
type: "TIMESTAMP WITH TIME ZONE"
|
|
5639
|
-
},
|
|
5640
|
-
{
|
|
5641
|
-
column: "is_anonymous",
|
|
5642
|
-
type: "BOOLEAN",
|
|
5643
|
-
default: "FALSE",
|
|
5644
|
-
notNull: true
|
|
5645
|
-
},
|
|
5646
|
-
{
|
|
5647
|
-
column: "metadata",
|
|
5648
|
-
type: "JSONB",
|
|
5649
|
-
default: "'{}'",
|
|
5650
|
-
notNull: true
|
|
5651
|
-
},
|
|
5652
|
-
{
|
|
5653
|
-
column: "tokens_valid_after",
|
|
5654
|
-
type: "TIMESTAMP WITH TIME ZONE"
|
|
5655
|
-
},
|
|
5656
|
-
{
|
|
5657
|
-
column: "created_at",
|
|
5658
|
-
type: "TIMESTAMP WITH TIME ZONE",
|
|
5659
|
-
default: "NOW()",
|
|
5660
|
-
notNull: true
|
|
5661
|
-
},
|
|
5662
|
-
{
|
|
5663
|
-
column: "updated_at",
|
|
5664
|
-
type: "TIMESTAMP WITH TIME ZONE",
|
|
5665
|
-
default: "NOW()",
|
|
5666
|
-
notNull: true
|
|
5667
|
-
}
|
|
5668
|
-
];
|
|
5669
|
-
var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
|
|
5670
|
-
/** Type + inline constraints, as they appear after the column name. */
|
|
5671
|
-
function authUsersColumnSql(spec) {
|
|
5672
|
-
return [
|
|
5673
|
-
spec.type,
|
|
5674
|
-
spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
|
|
5675
|
-
spec.notNull ? "NOT NULL" : ""
|
|
5676
|
-
].filter(Boolean).join(" ");
|
|
5677
|
-
}
|
|
5678
5678
|
/**
|
|
5679
|
-
*
|
|
5680
|
-
* auth does not own it.
|
|
5679
|
+
* Every declared index across a set of collections, in a stable order.
|
|
5681
5680
|
*
|
|
5682
|
-
*
|
|
5683
|
-
*
|
|
5684
|
-
*
|
|
5681
|
+
* Sorted because the result reaches `schema.sql`, which `doctor` string-
|
|
5682
|
+
* compares against a regenerated copy — `generatePostgresDdl` does not sort its
|
|
5683
|
+
* collections, so leaving this in declaration order would make the artifact
|
|
5684
|
+
* depend on the order files happened to load in.
|
|
5685
5685
|
*/
|
|
5686
|
-
|
|
5687
|
-
const spec = BY_COLUMN.get(column);
|
|
5688
|
-
return spec ? authUsersColumnSql(spec) : void 0;
|
|
5689
|
-
}
|
|
5690
|
-
/**
|
|
5691
|
-
* Whether a collection is an auth collection, i.e. whether the definitions in
|
|
5692
|
-
* this module apply to its table at all.
|
|
5693
|
-
*
|
|
5694
|
-
* Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
|
|
5695
|
-
* that one takes a `CollectionConfig`, this one is called from DDL code paths
|
|
5696
|
-
* that hold looser objects, and both spellings must accept `auth: true` as well
|
|
5697
|
-
* as `auth: { enabled: true }`.
|
|
5698
|
-
*/
|
|
5699
|
-
function isAuthCollection(collection) {
|
|
5700
|
-
const auth = collection?.auth;
|
|
5701
|
-
if (auth === true) return true;
|
|
5702
|
-
return typeof auth === "object" && auth !== null && auth.enabled === true;
|
|
5703
|
-
}
|
|
5686
|
+
var buildCollectionIndexPlan = (collections, resolveColumnName) => collections.flatMap((collection) => buildCollectionIndexSpecs(collection, resolveColumnName)).sort((a, b) => a.schema.localeCompare(b.schema) || a.table.localeCompare(b.table) || a.indexName.localeCompare(b.indexName));
|
|
5704
5687
|
//#endregion
|
|
5705
|
-
export {
|
|
5688
|
+
export { sortCollectionsBySlug as A, getPolicyNamesForRule as B, getTableName as C, getDeclaredPrimaryKeys as D, buildCompositeId as E, firstFreeKey as F, DEFAULT_ONE_OF_TYPE as G, mergeDeep as H, generateForeignKeyName as I, resolveClientListLimit as J, DEFAULT_ONE_OF_VALUE as K, legacyForeignKeyName as L, createRelationRefWithData as M, normalizeToEntityRelation as N, isAddressableId as O, updateDateAutoValues as P, Vector as Q, toPostgresIdentifier as R, getEnumVarName as S, resolveCollectionRelations as T, camelCase as U, isPrototypePollutingKey as V, toSnakeCase as W, hasForeignKeyOnTarget as X, ANONYMOUS_USER_ID as Y, isManyToMany as Z, securityRuleToConditions as _, buildSdkData as a, findRelation as b, parseOrderBySpecStrict as c, getJunctionCollectionConfig as d, getJunctionSecurityRules as f, policyToPostgres as g, getInjectedSecurityRules as h, collectionIndexStatements as i, createRelationRef as j, parseIdValues as k, CollectionRegistry as l, getEffectiveSecurityRules as m, buildCollectionIndexSpecs as n, OrderBySpecError as o, resolveJunctionSpecs as p, ListLimitError as q, collectionIndexStatement as r, normalizeDriverOrderBy as s, buildCollectionIndexPlan as t, relationalCollections as u, findAnonymousGrants as v, getTableVarName as w, getColumnName as x, fieldKeyForColumn as y, toWireKey as z };
|
|
5706
5689
|
|
|
5707
|
-
//# sourceMappingURL=
|
|
5690
|
+
//# sourceMappingURL=collection-index-DxJBvVTH.js.map
|