@rebasepro/server-postgres 0.13.1-canary.gc77922a → 0.13.1-canary.gca63148
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-BQttUZE2.js} +717 -46
- package/dist/auth-users-columns-BQttUZE2.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 +1 -1
- package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-C8H7ig-m.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-C8H7ig-m.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-CyMBc7wo.js} +86 -15
- package/dist/ensure-collection-tables-CyMBc7wo.js.map +1 -0
- package/dist/index.es.js +674 -160
- 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 +5 -5
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +192 -0
- package/dist/security/rls-enforcement.d.ts +53 -2
- package/dist/services/FetchService.d.ts +23 -0
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/realtimeService.d.ts +25 -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-B2LsrINK.js.map → websocket-D0TBU3ia.js.map} +1 -1
- 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 +22 -9
- package/src/cli.ts +96 -30
- package/src/collections/validate-relations.ts +17 -1
- 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 +144 -14
- package/src/schema/generated-schema-staleness.ts +7 -2
- 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 +549 -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 +146 -15
- 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 +2 -0
- package/src/services/realtimeService.ts +36 -33
- 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/src/schema/auth-bootstrap-sql.ts +0 -47
package/dist/index.es.js
CHANGED
|
@@ -2,11 +2,12 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, o as pinSearchPath, r as createPostgresDatabaseConnection } from "./connection-BuZ97wsr.js";
|
|
5
|
-
import { A as
|
|
6
|
-
import {
|
|
7
|
-
import { t as ANONYMOUS_USER_ID } from "./policy-
|
|
8
|
-
import { t as createPostgresWebSocket } from "./websocket-
|
|
9
|
-
import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as listBackups, d as resolvePgBinary, f as restoreDump, g as selectBackupsToPrune, h as require_source, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as preflight, m as validateDump, n as applyGlobals, o as ensureDatabaseExists, p as uploadBackup, s as getServerVersionMajor, t as BackupToolError, u as pruneBackups, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-
|
|
5
|
+
import { A as resolveCollectionRelations, B as legacyForeignKeyName, C as securityRuleToConditions, D as getEnumVarName, E as getColumnName, F as createRelationRef, G as camelCase, H as getPolicyNamesForRule, I as createRelationRefWithData, K as toSnakeCase, L as normalizeToEntityRelation, M as getDeclaredPrimaryKeys, N as isAddressableId, O as getTableName$1, P as parseIdValues, Q as Vector, R as updateDateAutoValues, S as policyToPostgres, T as findRelation, U as isPrototypePollutingKey, W as mergeDeep, X as hasForeignKeyOnTarget, Y as resolveClientListLimit, Z as isManyToMany, _ as resolveStringColumnLength, b as resolveJunctionSpecs, c as buildSearchColumnSpec, g as relationalCollections, h as CollectionRegistry, j as buildCompositeId, k as getTableVarName, l as hiddenColumnsOption, m as buildSdkData, o as SEARCH_UNACCENT_FN, p as visibleColumnProjection, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS, v as getJunctionCollectionConfig, w as findAnonymousGrants, x as getEffectiveSecurityRules, y as getJunctionSecurityRules, z as generateForeignKeyName } from "./auth-users-columns-BQttUZE2.js";
|
|
6
|
+
import { c as isPostgresCollectionConfig, l as isRelationalCollectionConfig, n as REBASE_SCHEMA, o as usesLegacyRlsFunctions } from "./src-C_wvdMnl.js";
|
|
7
|
+
import { t as ANONYMOUS_USER_ID } from "./policy-CPkCqVTz.js";
|
|
8
|
+
import { t as createPostgresWebSocket } from "./websocket-D0TBU3ia.js";
|
|
9
|
+
import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as listBackups, d as resolvePgBinary, f as restoreDump, g as selectBackupsToPrune, h as require_source, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as preflight, m as validateDump, n as applyGlobals, o as ensureDatabaseExists, p as uploadBackup, s as getServerVersionMajor, t as BackupToolError, u as pruneBackups, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-Bww-Lg0s.js";
|
|
10
|
+
import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-Bpv3nUZo.js";
|
|
10
11
|
import { Client, Pool } from "pg";
|
|
11
12
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
12
13
|
import { ApiError, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
|
|
@@ -30,25 +31,6 @@ import { randomUUID } from "crypto";
|
|
|
30
31
|
function isChannelBusInstance(setting) {
|
|
31
32
|
return typeof setting?.publish === "function";
|
|
32
33
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
35
|
-
*
|
|
36
|
-
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
37
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
38
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
39
|
-
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
40
|
-
*
|
|
41
|
-
* The return is never `undefined` — no ingress that routes its client limit
|
|
42
|
-
* through this can produce an unbounded read.
|
|
43
|
-
*/
|
|
44
|
-
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
45
|
-
const maxLimit = opts.maxLimit ?? 1e3;
|
|
46
|
-
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
47
|
-
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
48
|
-
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
49
|
-
}
|
|
50
|
-
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
51
|
-
}
|
|
52
34
|
//#endregion
|
|
53
35
|
//#region ../common/src/util/email.ts
|
|
54
36
|
/**
|
|
@@ -152,6 +134,142 @@ var buildPropertyCallbacks = (properties) => {
|
|
|
152
134
|
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
153
135
|
};
|
|
154
136
|
//#endregion
|
|
137
|
+
//#region ../common/src/util/internal-tables.ts
|
|
138
|
+
/**
|
|
139
|
+
* The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
|
|
140
|
+
* end-user role away from them.
|
|
141
|
+
*
|
|
142
|
+
* ## Why this exists
|
|
143
|
+
*
|
|
144
|
+
* Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
|
|
145
|
+
* provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
|
|
146
|
+
* in the schemas a project uses — including `rebase`, because a project's own
|
|
147
|
+
* collections are allowed to live there (the scaffold puts `users` there). It
|
|
148
|
+
* also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
|
|
149
|
+
* migrating role inherits the same grant.
|
|
150
|
+
*
|
|
151
|
+
* Every framework-internal table is created later: auth's tables come up during
|
|
152
|
+
* `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
|
|
153
|
+
* job registers, `idempotency_keys` on the first request that carries a key. So
|
|
154
|
+
* they all inherited full DML for the end-user role — and none of them enables
|
|
155
|
+
* row-level security, because none of them is a collection with
|
|
156
|
+
* `securityRules`. Measured on a freshly provisioned database, `SET ROLE
|
|
157
|
+
* rebase_user` could read `rebase.refresh_tokens` (session token hashes),
|
|
158
|
+
* `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
|
|
159
|
+
* `rebase.api_keys` (including its `admin` flag), and insert into
|
|
160
|
+
* `rebase.app_config`.
|
|
161
|
+
*
|
|
162
|
+
* Nothing routes a user-context query at those tables today, so this was not
|
|
163
|
+
* reachable over the API. That is the wrong thing to depend on: the documented
|
|
164
|
+
* model is that RLS is the authorization boundary, and these tables sat outside
|
|
165
|
+
* it. The boundary is now a privilege boundary instead — the role simply cannot
|
|
166
|
+
* address them.
|
|
167
|
+
*
|
|
168
|
+
* ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
|
|
169
|
+
*
|
|
170
|
+
* RLS with no policy denies every row, which is the same outcome, but it is the
|
|
171
|
+
* *weaker* statement: it leaves the grant in place, so a later policy — or a
|
|
172
|
+
* `FORCE` flag cleared by some future migration — reopens the table. There is no
|
|
173
|
+
* row of `refresh_tokens` any end user should ever reach, so the honest encoding
|
|
174
|
+
* is "this role has no privilege here at all". It also keeps the owner
|
|
175
|
+
* connection (which auth actually runs on) completely unaffected.
|
|
176
|
+
*
|
|
177
|
+
* ## Keeping it true
|
|
178
|
+
*
|
|
179
|
+
* `packages/rls-check` scans the `rebase` schema — it used to skip it as a
|
|
180
|
+
* "platform" schema — and its `rls-disabled` check fires on exactly the
|
|
181
|
+
* condition this module removes: RLS off *and* a DML grant to a reachable role.
|
|
182
|
+
* So a table added here without a revoke is caught by `pnpm rls:check`, not by
|
|
183
|
+
* someone re-reading this file.
|
|
184
|
+
*/
|
|
185
|
+
/**
|
|
186
|
+
* The Postgres role authenticated requests run as.
|
|
187
|
+
*
|
|
188
|
+
* Defined here rather than in the Postgres driver because both the driver (which
|
|
189
|
+
* provisions the role) and this module (which revokes on its behalf) need it,
|
|
190
|
+
* and a second spelling of a role name is a silent no-op waiting to happen.
|
|
191
|
+
*/
|
|
192
|
+
var REBASE_USER_ROLE = "rebase_user";
|
|
193
|
+
/**
|
|
194
|
+
* Framework-internal table names, unqualified.
|
|
195
|
+
*
|
|
196
|
+
* Deliberately NOT including `users`: the auth user table is also a collection,
|
|
197
|
+
* with `securityRules`, RLS enabled and policies applied. Users read their own
|
|
198
|
+
* row through it — revoking there would break sign-in.
|
|
199
|
+
*
|
|
200
|
+
* `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
|
|
201
|
+
* because `db migrate apply` passes `--revisions-schema rebase`.
|
|
202
|
+
*/
|
|
203
|
+
var REBASE_INTERNAL_TABLES = [
|
|
204
|
+
"user_identities",
|
|
205
|
+
"refresh_tokens",
|
|
206
|
+
"password_reset_tokens",
|
|
207
|
+
"magic_link_tokens",
|
|
208
|
+
"mfa_factors",
|
|
209
|
+
"mfa_challenges",
|
|
210
|
+
"recovery_codes",
|
|
211
|
+
"app_config",
|
|
212
|
+
"schema_meta",
|
|
213
|
+
"api_keys",
|
|
214
|
+
"cron_logs",
|
|
215
|
+
"cron_claims",
|
|
216
|
+
"idempotency_keys",
|
|
217
|
+
"entity_history",
|
|
218
|
+
"branches",
|
|
219
|
+
"channel_messages",
|
|
220
|
+
"channel_cursors",
|
|
221
|
+
"channel_presence",
|
|
222
|
+
"atlas_schema_revisions"
|
|
223
|
+
];
|
|
224
|
+
/** Postgres identifiers this module is willing to interpolate. */
|
|
225
|
+
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
226
|
+
/**
|
|
227
|
+
* A single statement that takes every privilege on `schema.table` away from the
|
|
228
|
+
* end-user role.
|
|
229
|
+
*
|
|
230
|
+
* Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
|
|
231
|
+
* happen in practice:
|
|
232
|
+
*
|
|
233
|
+
* - the role does not exist when the connection is unprivileged (Rebase then
|
|
234
|
+
* relies on native RLS rather than a role switch), and a bare `REVOKE` on a
|
|
235
|
+
* missing role is an error, not a no-op;
|
|
236
|
+
* - the table may not exist yet — `cron_logs` never appears in a project with
|
|
237
|
+
* no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
|
|
238
|
+
*
|
|
239
|
+
* One command, so it is safe on handles that speak the extended query protocol
|
|
240
|
+
* and reject multi-statement strings.
|
|
241
|
+
*/
|
|
242
|
+
function revokeInternalTableSql(schema, table) {
|
|
243
|
+
if (!SAFE_IDENTIFIER.test(schema)) throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
|
|
244
|
+
if (!SAFE_IDENTIFIER.test(table)) throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
|
|
245
|
+
const qualified = `"${schema}"."${table}"`;
|
|
246
|
+
return `
|
|
247
|
+
DO $rebase_revoke$
|
|
248
|
+
BEGIN
|
|
249
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
|
|
250
|
+
AND to_regclass('${qualified}') IS NOT NULL THEN
|
|
251
|
+
EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
|
|
252
|
+
END IF;
|
|
253
|
+
END
|
|
254
|
+
$rebase_revoke$;
|
|
255
|
+
`.trim();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Revoke on every internal table in `schema`, one statement at a time.
|
|
259
|
+
*
|
|
260
|
+
* Best-effort per table: a connection that does not own one of them (a
|
|
261
|
+
* pre-provisioned database, a platform-managed ledger) cannot revoke on it, and
|
|
262
|
+
* that must not take down a boot. The caller decides how loud to be — `onError`
|
|
263
|
+
* exists so the driver can warn without this module importing a logger.
|
|
264
|
+
*/
|
|
265
|
+
async function revokeInternalTableAccess(execute, schema, options) {
|
|
266
|
+
for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) try {
|
|
267
|
+
await execute(revokeInternalTableSql(schema, table));
|
|
268
|
+
} catch (error) {
|
|
269
|
+
options?.onError?.(table, error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
155
273
|
//#region ../common/src/table-classification.ts
|
|
156
274
|
/** Schemas that are always considered Rebase-internal. */
|
|
157
275
|
var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
|
|
@@ -436,6 +554,12 @@ function deriveRowAddress(row, collection, registry) {
|
|
|
436
554
|
//#endregion
|
|
437
555
|
//#region src/utils/drizzle-conditions.ts
|
|
438
556
|
/**
|
|
557
|
+
* Postgres's own default for `pg_trgm.word_similarity_threshold`. Named here
|
|
558
|
+
* because the fuzzy predicate has to know when the index-backed operator agrees
|
|
559
|
+
* with the collection's declared threshold and when it would narrow too far.
|
|
560
|
+
*/
|
|
561
|
+
var PG_TRGM_WORD_SIMILARITY_DEFAULT = .6;
|
|
562
|
+
/**
|
|
439
563
|
* Process-wide default, set once when the driver is constructed.
|
|
440
564
|
*
|
|
441
565
|
* The condition builder is a set of *static* methods reached from a dozen
|
|
@@ -493,7 +617,7 @@ function toMembershipList(value) {
|
|
|
493
617
|
* @example
|
|
494
618
|
* const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;
|
|
495
619
|
*/
|
|
496
|
-
var DrizzleConditionBuilder = class {
|
|
620
|
+
var DrizzleConditionBuilder = class DrizzleConditionBuilder {
|
|
497
621
|
/**
|
|
498
622
|
* Express "reachable from this parent through this relation" as a plain
|
|
499
623
|
* `WHERE` condition on the target table.
|
|
@@ -1119,10 +1243,28 @@ var DrizzleConditionBuilder = class {
|
|
|
1119
1243
|
return or(...conditions);
|
|
1120
1244
|
}
|
|
1121
1245
|
/**
|
|
1122
|
-
* Build search conditions for text fields
|
|
1246
|
+
* Build search conditions for text fields.
|
|
1247
|
+
*
|
|
1248
|
+
* Two shapes, chosen by whether the collection declared a `search` block:
|
|
1249
|
+
*
|
|
1250
|
+
* - **Declared** — one `@@ websearch_to_tsquery` against the generated
|
|
1251
|
+
* `tsvector` column. Stems, drops stopwords, AND-es the terms, reaches
|
|
1252
|
+
* inside JSONB and arrays, and uses the GIN index.
|
|
1253
|
+
* - **Not declared** — the original `ILIKE '%term%'` OR-ed across top-level
|
|
1254
|
+
* string properties, unchanged.
|
|
1255
|
+
*
|
|
1256
|
+
* The second is the default and stays the default. A collection that has
|
|
1257
|
+
* not opted in compiles to exactly the SQL it compiled to before this
|
|
1258
|
+
* branch existed, which is the only reason it is safe to have added it.
|
|
1259
|
+
*
|
|
1260
|
+
* `collection` is optional so that the callers which genuinely have no
|
|
1261
|
+
* collection in hand — nested paths, derived views — keep working; without
|
|
1262
|
+
* one there is no `search` block to read and the ILIKE path is correct.
|
|
1123
1263
|
*/
|
|
1124
|
-
static buildSearchConditions(searchString, properties, table) {
|
|
1264
|
+
static buildSearchConditions(searchString, properties, table, collection) {
|
|
1125
1265
|
const searchConditions = [];
|
|
1266
|
+
const ftsCondition = collection ? DrizzleConditionBuilder.buildFullTextCondition(searchString, table, collection) : void 0;
|
|
1267
|
+
if (ftsCondition) return [ftsCondition];
|
|
1126
1268
|
for (const [key, prop] of Object.entries(properties)) {
|
|
1127
1269
|
const p = prop;
|
|
1128
1270
|
if (p.type === "string" && !p.enum && p.isId !== "uuid") {
|
|
@@ -1135,6 +1277,117 @@ var DrizzleConditionBuilder = class {
|
|
|
1135
1277
|
return searchConditions;
|
|
1136
1278
|
}
|
|
1137
1279
|
/**
|
|
1280
|
+
* The `@@` predicate for a collection that declared a `search` block, or
|
|
1281
|
+
* undefined for one that did not.
|
|
1282
|
+
*
|
|
1283
|
+
* The query is normalized exactly as the indexed content was — same text
|
|
1284
|
+
* search configuration, same accent folding. Skipping that on the query
|
|
1285
|
+
* side is the subtle way to get a search that matches nothing: the column
|
|
1286
|
+
* would hold `gestion` while the query asked for `gestión`.
|
|
1287
|
+
*
|
|
1288
|
+
* `websearch_to_tsquery` rather than `plainto_tsquery` because it is the
|
|
1289
|
+
* one that behaves the way a search box looks like it should — quoted
|
|
1290
|
+
* phrases, `or`, and a leading `-` to exclude — and because it never throws
|
|
1291
|
+
* on user input, which `to_tsquery` does on so much as a stray parenthesis.
|
|
1292
|
+
*/
|
|
1293
|
+
static buildFullTextCondition(searchString, table, collection) {
|
|
1294
|
+
let spec;
|
|
1295
|
+
try {
|
|
1296
|
+
spec = buildSearchColumnSpec(collection);
|
|
1297
|
+
} catch {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (!spec) return void 0;
|
|
1301
|
+
const column = table[spec.column];
|
|
1302
|
+
if (!column) return;
|
|
1303
|
+
const exact = sql`${column} @@ ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)}`;
|
|
1304
|
+
if (!spec.fuzzy) return exact;
|
|
1305
|
+
const fuzzyColumn = table[spec.fuzzy.column];
|
|
1306
|
+
if (!fuzzyColumn) return exact;
|
|
1307
|
+
const needle = spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`;
|
|
1308
|
+
const similar = sql`public.word_similarity(${needle}, ${fuzzyColumn}) >= ${spec.fuzzy.threshold}`;
|
|
1309
|
+
return sql`(${exact} OR ${spec.fuzzy.threshold > PG_TRGM_WORD_SIMILARITY_DEFAULT ? sql`(${needle} OPERATOR(public.<%) ${fuzzyColumn} AND ${similar})` : similar})`;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* `websearch_to_tsquery(<config>, <normalized search string>)`.
|
|
1313
|
+
*
|
|
1314
|
+
* Split out because the ranking expression needs the identical query — a
|
|
1315
|
+
* row ranked against a different tsquery than it was matched against is a
|
|
1316
|
+
* ranking of something else.
|
|
1317
|
+
*/
|
|
1318
|
+
static normalizedTsQuery(searchString, spec) {
|
|
1319
|
+
const normalized = spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`;
|
|
1320
|
+
return sql`websearch_to_tsquery(${spec.language}, ${normalized})`;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* A JSONB array of `{ field, snippet }` naming which declared fields matched
|
|
1324
|
+
* and showing the text around each hit — what backs `_matches`.
|
|
1325
|
+
*
|
|
1326
|
+
* A ranked list answers "which rows", never "why this row". For a talent
|
|
1327
|
+
* pool that difference is the product: a candidate surfacing for
|
|
1328
|
+
* "iso 14001" on a *certification* is a different candidate from one whose
|
|
1329
|
+
* bio happens to mention the standard, and the score cannot tell them apart.
|
|
1330
|
+
*
|
|
1331
|
+
* Built as a correlated subquery over a `VALUES` list of the declared
|
|
1332
|
+
* fields, rather than one `CASE` per field, so the shape does not change
|
|
1333
|
+
* with the number of fields and the empty result is a plain `[]`.
|
|
1334
|
+
*
|
|
1335
|
+
* `ts_headline` runs over the same normalized text that was indexed. Over
|
|
1336
|
+
* the *original* text it would find nothing to mark whenever `unaccent` is
|
|
1337
|
+
* on — the query's lexemes are folded and the document's are not — and
|
|
1338
|
+
* would return the text silently unhighlighted. Folded-but-marked beats
|
|
1339
|
+
* pretty-but-inert.
|
|
1340
|
+
*
|
|
1341
|
+
* Undefined when the collection has not opted in, when the column is not on
|
|
1342
|
+
* the table yet, or when the caller did not ask: this costs a `ts_headline`
|
|
1343
|
+
* per field per row and `ts_headline` re-parses the document.
|
|
1344
|
+
*/
|
|
1345
|
+
static buildSearchMatchesExpression(searchString, table, collection) {
|
|
1346
|
+
let spec;
|
|
1347
|
+
try {
|
|
1348
|
+
spec = buildSearchColumnSpec(collection);
|
|
1349
|
+
} catch {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (!spec || spec.fields.length === 0) return void 0;
|
|
1353
|
+
if (!table[spec.column]) return void 0;
|
|
1354
|
+
const query = DrizzleConditionBuilder.normalizedTsQuery(searchString, spec);
|
|
1355
|
+
const config = sql`${spec.language}`;
|
|
1356
|
+
const rows = spec.fields.map((f, i) => sql`(${i}, ${f.path}, ${sql.raw(f.textSql)})`);
|
|
1357
|
+
return sql`(
|
|
1358
|
+
SELECT coalesce(jsonb_agg(s.m ORDER BY f.ord), '[]'::jsonb)
|
|
1359
|
+
FROM (VALUES ${sql.join(rows, sql`, `)}) AS f(ord, path, txt)
|
|
1360
|
+
CROSS JOIN LATERAL (
|
|
1361
|
+
SELECT jsonb_build_object(
|
|
1362
|
+
'field', f.path,
|
|
1363
|
+
'snippet', ts_headline(${config}::regconfig, f.txt, ${query},
|
|
1364
|
+
'StartSel=<mark>,StopSel=</mark>,MaxWords=14,MinWords=1,MaxFragments=1,FragmentDelimiter= … ')
|
|
1365
|
+
) AS m
|
|
1366
|
+
WHERE to_tsvector(${config}::regconfig, f.txt) @@ ${query}
|
|
1367
|
+
) s
|
|
1368
|
+
)`;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* `ts_rank(<column>, <query>)` for the collection, or undefined when it has
|
|
1372
|
+
* not opted in. This is what backs `orderBy: ["_score", "desc"]`.
|
|
1373
|
+
*/
|
|
1374
|
+
static buildSearchRankExpression(searchString, table, collection) {
|
|
1375
|
+
let spec;
|
|
1376
|
+
try {
|
|
1377
|
+
spec = buildSearchColumnSpec(collection);
|
|
1378
|
+
} catch {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (!spec) return void 0;
|
|
1382
|
+
const column = table[spec.column];
|
|
1383
|
+
if (!column) return void 0;
|
|
1384
|
+
const rank = sql`ts_rank(${column}, ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)})`;
|
|
1385
|
+
if (!spec.fuzzy) return rank;
|
|
1386
|
+
const fuzzyColumn = table[spec.fuzzy.column];
|
|
1387
|
+
if (!fuzzyColumn) return rank;
|
|
1388
|
+
return sql`(${rank} + public.word_similarity(${spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`}, ${fuzzyColumn}))`;
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1138
1391
|
* Build a unique field check condition
|
|
1139
1392
|
*/
|
|
1140
1393
|
static buildUniqueFieldCondition(fieldColumn, value, idColumn, excludeId) {
|
|
@@ -1276,7 +1529,11 @@ function sanitizeAndConvertDates(obj) {
|
|
|
1276
1529
|
if (obj instanceof Date) return obj.toISOString();
|
|
1277
1530
|
if (typeof obj === "object") {
|
|
1278
1531
|
const newObj = {};
|
|
1279
|
-
for (const key in obj)
|
|
1532
|
+
for (const key in obj) {
|
|
1533
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
1534
|
+
if (isPrototypePollutingKey(key)) continue;
|
|
1535
|
+
newObj[key] = sanitizeAndConvertDates(obj[key]);
|
|
1536
|
+
}
|
|
1280
1537
|
return newObj;
|
|
1281
1538
|
}
|
|
1282
1539
|
if (typeof obj === "string") {
|
|
@@ -1305,6 +1562,7 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
1305
1562
|
if (relation.kind === "belongsTo") foreignKeys.add(relation.localKey);
|
|
1306
1563
|
});
|
|
1307
1564
|
for (const [key, value] of Object.entries(row)) {
|
|
1565
|
+
if (isPrototypePollutingKey(key)) continue;
|
|
1308
1566
|
const property = properties[key];
|
|
1309
1567
|
const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
|
|
1310
1568
|
if (!property) {
|
|
@@ -1670,6 +1928,18 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
|
|
|
1670
1928
|
* from the result (used by `normalizeDbValues` where
|
|
1671
1929
|
* Drizzle's relational API already hydrates them).
|
|
1672
1930
|
*/
|
|
1931
|
+
/**
|
|
1932
|
+
* Keys a query computes and attaches to a row, rather than reads from a column.
|
|
1933
|
+
*
|
|
1934
|
+
* An explicit set rather than a `_` prefix rule: a user's column may perfectly
|
|
1935
|
+
* well be called `_internal`, and passing it through here would put a value on
|
|
1936
|
+
* the row that no property describes and nothing downstream knows how to type.
|
|
1937
|
+
*/
|
|
1938
|
+
var QUERY_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
1939
|
+
"_score",
|
|
1940
|
+
"_distance",
|
|
1941
|
+
"_matches"
|
|
1942
|
+
]);
|
|
1673
1943
|
function normalizeScalarValues(data, properties, collection, resolvedRelations, options) {
|
|
1674
1944
|
const result = {};
|
|
1675
1945
|
const internalFKColumns = /* @__PURE__ */ new Set();
|
|
@@ -1681,6 +1951,10 @@ function normalizeScalarValues(data, properties, collection, resolvedRelations,
|
|
|
1681
1951
|
result[key] = value === null ? null : typeof value === "number" ? value : String(value);
|
|
1682
1952
|
continue;
|
|
1683
1953
|
}
|
|
1954
|
+
if (QUERY_METADATA_KEYS.has(key)) {
|
|
1955
|
+
result[key] = value;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1684
1958
|
const property = properties[key];
|
|
1685
1959
|
if (!property) continue;
|
|
1686
1960
|
if (options.skipRelations && property.type === "relation") continue;
|
|
@@ -1933,7 +2207,7 @@ var RelationService = class {
|
|
|
1933
2207
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
1934
2208
|
const additionalFilters = [];
|
|
1935
2209
|
if (options.searchString) {
|
|
1936
|
-
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, targetCollection.properties, targetTable);
|
|
2210
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, targetCollection.properties, targetTable, targetCollection);
|
|
1937
2211
|
if (searchConditions.length === 0) return [];
|
|
1938
2212
|
const searchCombined = DrizzleConditionBuilder.combineConditionsWithOr(searchConditions);
|
|
1939
2213
|
if (searchCombined) additionalFilters.push(searchCombined);
|
|
@@ -3039,7 +3313,7 @@ function sanitizeErrorForClient(error, context) {
|
|
|
3039
3313
|
* Service for handling all row read operations.
|
|
3040
3314
|
* Handles fetching, searching, counting, and filtering rows.
|
|
3041
3315
|
*/
|
|
3042
|
-
var FetchService = class {
|
|
3316
|
+
var FetchService = class FetchService {
|
|
3043
3317
|
db;
|
|
3044
3318
|
registry;
|
|
3045
3319
|
relationService;
|
|
@@ -3112,6 +3386,24 @@ var FetchService = class {
|
|
|
3112
3386
|
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
3113
3387
|
* caller that hands over no collection to resolve against.
|
|
3114
3388
|
*/
|
|
3389
|
+
/**
|
|
3390
|
+
* The ORDER BY target, which may be relevance rather than a column.
|
|
3391
|
+
*
|
|
3392
|
+
* `_score` is only meaningful for a collection that declared a `search`
|
|
3393
|
+
* block *and* for a request that carried a search string — ranking rows
|
|
3394
|
+
* against no query ranks them all at zero. Outside those two conditions it
|
|
3395
|
+
* is an unknown field and gets the same 400 as any other typo, which is the
|
|
3396
|
+
* behaviour that matters: a sort that is silently dropped returns 200 with
|
|
3397
|
+
* rows in arbitrary order, and paging over that repeats and skips rows.
|
|
3398
|
+
*/
|
|
3399
|
+
static SCORE_FIELD = "_score";
|
|
3400
|
+
resolveOrderTarget(table, orderBy, collection, searchString) {
|
|
3401
|
+
if (orderBy === FetchService.SCORE_FIELD && collection && searchString) {
|
|
3402
|
+
const rank = DrizzleConditionBuilder.buildSearchRankExpression(searchString, table, collection);
|
|
3403
|
+
if (rank) return rank;
|
|
3404
|
+
}
|
|
3405
|
+
return this.resolveOrderByField(table, orderBy, collection);
|
|
3406
|
+
}
|
|
3115
3407
|
resolveOrderByField(table, orderBy, collection) {
|
|
3116
3408
|
const columnAt = (key) => (key in table ? table[key] : void 0) || void 0;
|
|
3117
3409
|
const direct = columnAt(orderBy);
|
|
@@ -3239,12 +3531,14 @@ var FetchService = class {
|
|
|
3239
3531
|
*/
|
|
3240
3532
|
buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig, scopeCondition) {
|
|
3241
3533
|
const queryOpts = {};
|
|
3534
|
+
const hidden = hiddenColumnsOption(getTableColumns(table), this.registry.getCollectionByPath(collectionPath) ?? void 0);
|
|
3535
|
+
if (hidden) queryOpts.columns = hidden;
|
|
3242
3536
|
if (withConfig) queryOpts.with = withConfig;
|
|
3243
3537
|
const allConditions = [];
|
|
3244
3538
|
if (scopeCondition) allConditions.push(scopeCondition);
|
|
3245
3539
|
if (options.searchString) {
|
|
3246
3540
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
3247
|
-
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
3541
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
|
|
3248
3542
|
if (searchConditions.length === 0) {
|
|
3249
3543
|
queryOpts.where = and(eq(idField, -99999999));
|
|
3250
3544
|
return queryOpts;
|
|
@@ -3267,7 +3561,7 @@ var FetchService = class {
|
|
|
3267
3561
|
const orderExpressions = [];
|
|
3268
3562
|
if (options.orderBy) {
|
|
3269
3563
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
3270
|
-
const orderByField = this.
|
|
3564
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
3271
3565
|
if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
3272
3566
|
}
|
|
3273
3567
|
orderExpressions.push(desc(idField));
|
|
@@ -3284,6 +3578,7 @@ var FetchService = class {
|
|
|
3284
3578
|
if (!options.startAfter) return [];
|
|
3285
3579
|
const cursor = options.startAfter;
|
|
3286
3580
|
if (options.orderBy) {
|
|
3581
|
+
if (options.orderBy === FetchService.SCORE_FIELD) throw ApiError.badRequest("Cursor pagination (`startAfter`) cannot be combined with `orderBy: \"_score\"`. Relevance is computed per query rather than stored, so it cannot key a cursor. Use `limit`/`offset` for relevance-ordered pages, or order by a column.", "SCORE_CURSOR_UNSUPPORTED", { field: FetchService.SCORE_FIELD });
|
|
3287
3582
|
const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
|
|
3288
3583
|
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
3289
3584
|
if (orderByField) {
|
|
@@ -3354,9 +3649,11 @@ var FetchService = class {
|
|
|
3354
3649
|
const qb = this.getQueryBuilder(tableName);
|
|
3355
3650
|
if (qb) try {
|
|
3356
3651
|
const withConfig = this.buildWithConfig(collection);
|
|
3652
|
+
const hidden = hiddenColumnsOption(getTableColumns(table), collection);
|
|
3357
3653
|
const row = await qb.findFirst({
|
|
3358
3654
|
where: eq(idField, parsedId),
|
|
3359
|
-
with: withConfig
|
|
3655
|
+
with: withConfig,
|
|
3656
|
+
...hidden ? { columns: hidden } : {}
|
|
3360
3657
|
});
|
|
3361
3658
|
if (!row) return void 0;
|
|
3362
3659
|
const flatRow = toFlatRow(row, collection, this.registry);
|
|
@@ -3370,7 +3667,8 @@ var FetchService = class {
|
|
|
3370
3667
|
if (reachedDatabase(e)) throw e;
|
|
3371
3668
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
3372
3669
|
}
|
|
3373
|
-
const
|
|
3670
|
+
const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
|
|
3671
|
+
const result = await this.db.select(visibleOne).from(table).where(eq(idField, parsedId)).limit(1);
|
|
3374
3672
|
if (result.length === 0) return void 0;
|
|
3375
3673
|
const raw = result[0];
|
|
3376
3674
|
const values = await parseDataFromServer(raw, collection, this.db, this.registry);
|
|
@@ -3426,14 +3724,21 @@ var FetchService = class {
|
|
|
3426
3724
|
}
|
|
3427
3725
|
let vectorMeta;
|
|
3428
3726
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
3727
|
+
const visible = visibleColumnProjection(getTableColumns(table), collection);
|
|
3728
|
+
const rankSelect = options.searchString ? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection) : void 0;
|
|
3729
|
+
const matchesSelect = options.searchString && options.searchExplain ? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection) : void 0;
|
|
3429
3730
|
let query = vectorMeta ? this.db.select({
|
|
3430
|
-
table_row: table,
|
|
3731
|
+
table_row: visible ?? table,
|
|
3431
3732
|
_distance: vectorMeta.distanceSelect
|
|
3432
|
-
}).from(table).$dynamic() : this.db.select(
|
|
3733
|
+
}).from(table).$dynamic() : rankSelect ? this.db.select({
|
|
3734
|
+
table_row: visible ?? table,
|
|
3735
|
+
_score: rankSelect,
|
|
3736
|
+
...matchesSelect ? { _matches: matchesSelect } : {}
|
|
3737
|
+
}).from(table).$dynamic() : visible ? this.db.select(visible).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
3433
3738
|
const allConditions = [];
|
|
3434
3739
|
if (scopeCondition) allConditions.push(scopeCondition);
|
|
3435
3740
|
if (options.searchString) {
|
|
3436
|
-
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
3741
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
|
|
3437
3742
|
if (searchConditions.length === 0) return [];
|
|
3438
3743
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
3439
3744
|
}
|
|
@@ -3453,7 +3758,7 @@ var FetchService = class {
|
|
|
3453
3758
|
const orderExpressions = [];
|
|
3454
3759
|
if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
|
|
3455
3760
|
else if (options.orderBy) {
|
|
3456
|
-
const orderByField = this.
|
|
3761
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
3457
3762
|
if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
3458
3763
|
}
|
|
3459
3764
|
orderExpressions.push(desc(idField));
|
|
@@ -3473,6 +3778,10 @@ var FetchService = class {
|
|
|
3473
3778
|
const results = vectorMeta ? rawResults.map((r) => ({
|
|
3474
3779
|
...r.table_row,
|
|
3475
3780
|
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
3781
|
+
})) : rankSelect ? rawResults.map((r) => ({
|
|
3782
|
+
...r.table_row,
|
|
3783
|
+
_score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
|
|
3784
|
+
...matchesSelect ? { _matches: r._matches ?? [] } : {}
|
|
3476
3785
|
})) : rawResults;
|
|
3477
3786
|
return this.processRowResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
|
|
3478
3787
|
}
|
|
@@ -3564,7 +3873,7 @@ var FetchService = class {
|
|
|
3564
3873
|
const allConditions = [];
|
|
3565
3874
|
if (hop) allConditions.push(this.buildRelationScope(hop));
|
|
3566
3875
|
if (options.searchString) {
|
|
3567
|
-
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
3876
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
|
|
3568
3877
|
if (searchConditions.length === 0) return 0;
|
|
3569
3878
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
3570
3879
|
}
|
|
@@ -3709,7 +4018,8 @@ var FetchService = class {
|
|
|
3709
4018
|
if (reachedDatabase(e)) throw e;
|
|
3710
4019
|
logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
3711
4020
|
}
|
|
3712
|
-
const
|
|
4021
|
+
const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
|
|
4022
|
+
const result = await this.db.select(visibleOne).from(table).where(eq(idField, parsedId)).limit(1);
|
|
3713
4023
|
if (result.length === 0) return null;
|
|
3714
4024
|
const flatEntity = { ...result[0] };
|
|
3715
4025
|
if (!include || include.length === 0) return flatEntity;
|
|
@@ -3747,14 +4057,21 @@ var FetchService = class {
|
|
|
3747
4057
|
const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
|
|
3748
4058
|
let vectorMeta;
|
|
3749
4059
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
4060
|
+
const visible = visibleColumnProjection(getTableColumns(table), collection);
|
|
4061
|
+
const rankSelect = options.searchString ? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection) : void 0;
|
|
4062
|
+
const matchesSelect = options.searchString && options.searchExplain ? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection) : void 0;
|
|
3750
4063
|
let query = vectorMeta ? this.db.select({
|
|
3751
|
-
table_row: table,
|
|
4064
|
+
table_row: visible ?? table,
|
|
3752
4065
|
_distance: vectorMeta.distanceSelect
|
|
3753
|
-
}).from(table).$dynamic() : this.db.select(
|
|
4066
|
+
}).from(table).$dynamic() : rankSelect ? this.db.select({
|
|
4067
|
+
table_row: visible ?? table,
|
|
4068
|
+
_score: rankSelect,
|
|
4069
|
+
...matchesSelect ? { _matches: matchesSelect } : {}
|
|
4070
|
+
}).from(table).$dynamic() : visible ? this.db.select(visible).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
3754
4071
|
const allConditions = [];
|
|
3755
4072
|
if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
|
|
3756
4073
|
if (options.searchString) {
|
|
3757
|
-
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
4074
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
|
|
3758
4075
|
if (searchConditions.length === 0) return [];
|
|
3759
4076
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
3760
4077
|
}
|
|
@@ -3770,7 +4087,7 @@ var FetchService = class {
|
|
|
3770
4087
|
const orderExpressions = [];
|
|
3771
4088
|
if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
|
|
3772
4089
|
else if (options.orderBy) {
|
|
3773
|
-
const orderByField = this.
|
|
4090
|
+
const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
|
|
3774
4091
|
if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
3775
4092
|
}
|
|
3776
4093
|
orderExpressions.push(desc(idField));
|
|
@@ -3783,6 +4100,11 @@ var FetchService = class {
|
|
|
3783
4100
|
...r.table_row,
|
|
3784
4101
|
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
3785
4102
|
}));
|
|
4103
|
+
if (rankSelect) return rawResults.map((r) => ({
|
|
4104
|
+
...r.table_row,
|
|
4105
|
+
_score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
|
|
4106
|
+
...matchesSelect ? { _matches: r._matches ?? [] } : {}
|
|
4107
|
+
}));
|
|
3786
4108
|
return rawResults;
|
|
3787
4109
|
}
|
|
3788
4110
|
/**
|
|
@@ -4046,7 +4368,8 @@ var PersistService = class {
|
|
|
4046
4368
|
if (error instanceof ApiError || error?.name === "ApiError") return error;
|
|
4047
4369
|
const pgError = extractPgError(error);
|
|
4048
4370
|
if (pgError) {
|
|
4049
|
-
const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
4371
|
+
const { message, code } = pgErrorToFriendlyMessage(pgError, collectionSlug);
|
|
4372
|
+
if (/^2[23]/.test(code)) return code === "23505" ? ApiError.conflict(message, `PG_${code}`) : ApiError.badRequest(message, `PG_${code}`);
|
|
4050
4373
|
return new Error(message);
|
|
4051
4374
|
}
|
|
4052
4375
|
const causeMessage = extractCauseMessage(error);
|
|
@@ -4271,6 +4594,7 @@ var BranchService = class {
|
|
|
4271
4594
|
metadata JSONB DEFAULT '{}'
|
|
4272
4595
|
);
|
|
4273
4596
|
`));
|
|
4597
|
+
await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "branches")));
|
|
4274
4598
|
}
|
|
4275
4599
|
/**
|
|
4276
4600
|
* Create a new branch database by templating the source database.
|
|
@@ -4378,43 +4702,49 @@ var BranchService = class {
|
|
|
4378
4702
|
};
|
|
4379
4703
|
//#endregion
|
|
4380
4704
|
//#region src/security/rls-enforcement.ts
|
|
4705
|
+
var quoteIdent$1 = (name) => `"${name.replace(/"/g, "\"\"")}"`;
|
|
4706
|
+
/** DML the user role holds on managed tables (RLS still filters per row). */
|
|
4707
|
+
var USER_TABLE_PRIVILEGES = "SELECT, INSERT, UPDATE, DELETE";
|
|
4381
4708
|
/**
|
|
4382
|
-
*
|
|
4383
|
-
*
|
|
4384
|
-
* Every operation runs in one of two contexts:
|
|
4709
|
+
* Warn when the connection role shares its name with an existing schema.
|
|
4385
4710
|
*
|
|
4386
|
-
*
|
|
4387
|
-
*
|
|
4388
|
-
*
|
|
4389
|
-
*
|
|
4390
|
-
* `securityRules` are the whole authorization model; app-layer callbacks
|
|
4391
|
-
* are validation/side-effects, not a security boundary.
|
|
4711
|
+
* Postgres resolves unqualified names through `search_path`, which defaults to
|
|
4712
|
+
* `"$user", public` — and `$user` is the connection ROLE. When a schema of that
|
|
4713
|
+
* name exists it sits ahead of `public`, so every unqualified statement
|
|
4714
|
+
* silently operates on it instead:
|
|
4392
4715
|
*
|
|
4393
|
-
*
|
|
4394
|
-
* background jobs, and the explicit `rebase.dataAsAdmin` accessor. As table
|
|
4395
|
-
* owner it bypasses RLS. This is the trusted plane, equivalent to
|
|
4396
|
-
* Supabase's `service_role`.
|
|
4716
|
+
* CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts
|
|
4397
4717
|
*
|
|
4398
|
-
*
|
|
4718
|
+
* Nothing errors. You get a second table of the same name in the wrong schema,
|
|
4719
|
+
* and reads that pin `public` cannot see it — which reads as "missing table" and
|
|
4720
|
+
* sends people to re-run a push that creates a *third* copy. The bootstrapper
|
|
4721
|
+
* has a whole branch dedicated to recognising the symptom after the fact.
|
|
4399
4722
|
*
|
|
4400
|
-
*
|
|
4401
|
-
*
|
|
4402
|
-
*
|
|
4403
|
-
*
|
|
4404
|
-
*
|
|
4405
|
-
*
|
|
4406
|
-
*
|
|
4407
|
-
* RLS binds. Transaction-scoped, so it composes with poolers.
|
|
4723
|
+
* Rebase shipped straight into this: it creates a schema named `rebase` while
|
|
4724
|
+
* every template named the database role `rebase` too. The scaffold uses
|
|
4725
|
+
* `rebase_app` now, and every pool Rebase opens pins `search_path=public`
|
|
4726
|
+
* (`pinSearchPath`), which covers the paths the framework controls. This covers
|
|
4727
|
+
* the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,
|
|
4728
|
+
* a hand-written migration — because the hazard is a property of the two NAMES,
|
|
4729
|
+
* not of any one connection.
|
|
4408
4730
|
*
|
|
4409
|
-
*
|
|
4410
|
-
*
|
|
4411
|
-
*
|
|
4731
|
+
* A warning rather than a boot failure: the database works, the framework's own
|
|
4732
|
+
* traffic is pinned, and refusing to start over a naming choice a user may have
|
|
4733
|
+
* inherited would be worse than the risk.
|
|
4412
4734
|
*/
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4735
|
+
async function warnOnRoleSchemaCollision(run) {
|
|
4736
|
+
try {
|
|
4737
|
+
const rows = await run(`
|
|
4738
|
+
SELECT current_user AS role,
|
|
4739
|
+
EXISTS (
|
|
4740
|
+
SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user
|
|
4741
|
+
) AS collides
|
|
4742
|
+
`);
|
|
4743
|
+
if (rows[0]?.collides !== true) return;
|
|
4744
|
+
const role = String(rows[0]?.role ?? "the connection role");
|
|
4745
|
+
logger.warn(`⚠️ The database role "${role}" has the same name as a schema. Postgres resolves unqualified names through \`search_path\`, which defaults to \`"$user", public\` — so "${role}" is searched BEFORE public, and any unqualified \`CREATE TABLE\`/\`SELECT\` from a tool that does not pin the path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in "${role}" instead. Rebase's own connections pin \`search_path=public\`, so the server is unaffected. To remove the hazard entirely, connect as a role whose name is not also a schema — the scaffold uses "rebase_app".`);
|
|
4746
|
+
} catch {}
|
|
4747
|
+
}
|
|
4418
4748
|
async function detectConnectionPosture(run) {
|
|
4419
4749
|
const row = (await run(`
|
|
4420
4750
|
SELECT current_user AS role,
|
|
@@ -4490,6 +4820,9 @@ async function ensureAppRole(run, schemas) {
|
|
|
4490
4820
|
await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);
|
|
4491
4821
|
await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);
|
|
4492
4822
|
await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);
|
|
4823
|
+
await revokeInternalTableAccess(async (text) => {
|
|
4824
|
+
await run(text);
|
|
4825
|
+
}, schema, { onError: (table, error) => logger.warn(`🔐 [rls] Could not revoke "${REBASE_USER_ROLE}" access to "${schema}"."${table}" — it stays reachable by authenticated requests: ` + (error instanceof Error ? error.message : String(error))) });
|
|
4493
4826
|
}
|
|
4494
4827
|
logger.info(`🔐 [rls] User role "${REBASE_USER_ROLE}" provisioned (schemas: ${uniqueSchemas.join(", ")})`);
|
|
4495
4828
|
}
|
|
@@ -4575,6 +4908,40 @@ function warnOnAnonymousGrants(collections) {
|
|
|
4575
4908
|
logger.warn(`Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are true for everyone:\n\n` + problems.join("\n\n") + "\n");
|
|
4576
4909
|
}
|
|
4577
4910
|
/**
|
|
4911
|
+
* Name the collections whose raw policy SQL still calls the pre-1.0 helpers.
|
|
4912
|
+
*
|
|
4913
|
+
* The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the
|
|
4914
|
+
* database, so nothing is broken and no policy is wrong — which is exactly why
|
|
4915
|
+
* this has to be said out loud. A silent rewrite that works forever is not a
|
|
4916
|
+
* migration, it is a second supported spelling nobody wrote down, and the next
|
|
4917
|
+
* person to read those rules will copy the old one.
|
|
4918
|
+
*
|
|
4919
|
+
* Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,
|
|
4920
|
+
* `policy.rolesOverlap(...)`) compile from the model and were never affected.
|
|
4921
|
+
*/
|
|
4922
|
+
function warnOnLegacyRlsFunctions(collections) {
|
|
4923
|
+
const sites = [];
|
|
4924
|
+
for (const collection of collections) for (const rule of collection.securityRules ?? []) {
|
|
4925
|
+
const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
|
|
4926
|
+
if (![usingExpr, withCheckExpr].filter((e) => e !== null).some(containsLegacyRlsCall)) continue;
|
|
4927
|
+
const site = `${collection.slug ?? "(unnamed)"} → "${rule.name ?? "(unnamed rule)"}"`;
|
|
4928
|
+
if (!sites.includes(site)) sites.push(site);
|
|
4929
|
+
}
|
|
4930
|
+
if (sites.length === 0) return;
|
|
4931
|
+
logger.warn(`These security rules call the pre-1.0 RLS helpers (\`auth.uid()\`, \`auth.roles()\`, \`auth.jwt()\`). They still work — the compiler rewrites them — but the functions now live in the \`rebase\` schema, and the \`auth\` one is Supabase's. Update the raw SQL in these rules to \`${REBASE_SCHEMA}.uid()\` and friends, or switch them to the structured helpers (\`policy.authUid()\`, \`policy.rolesOverlap()\`), which never had to be spelled by hand:\n\n` + sites.map((s) => ` • ${s}`).join("\n") + "\n");
|
|
4932
|
+
}
|
|
4933
|
+
/** Whether any `raw` expression in the tree calls a pre-1.0 helper. */
|
|
4934
|
+
function containsLegacyRlsCall(expr) {
|
|
4935
|
+
switch (expr.kind) {
|
|
4936
|
+
case "raw": return usesLegacyRlsFunctions(expr.sql);
|
|
4937
|
+
case "and":
|
|
4938
|
+
case "or": return expr.operands.some(containsLegacyRlsCall);
|
|
4939
|
+
case "not": return containsLegacyRlsCall(expr.operand);
|
|
4940
|
+
case "existsIn": return containsLegacyRlsCall(expr.where);
|
|
4941
|
+
default: return false;
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
/**
|
|
4578
4945
|
* Reject `pgRoles` that this server can never satisfy.
|
|
4579
4946
|
*
|
|
4580
4947
|
* `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a
|
|
@@ -4704,6 +5071,25 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
4704
5071
|
}
|
|
4705
5072
|
};
|
|
4706
5073
|
}
|
|
5074
|
+
/**
|
|
5075
|
+
* Build the context handed to every collection callback.
|
|
5076
|
+
*
|
|
5077
|
+
* Note `data: this.data` — `this` is whichever driver is running the
|
|
5078
|
+
* operation, so the callback's data plane inherits that driver's privilege.
|
|
5079
|
+
* On a user request `AuthenticatedPostgresBackendDriver.withTransaction`
|
|
5080
|
+
* constructs a fresh base driver bound to the RLS-scoped transaction and
|
|
5081
|
+
* runs the operation on it, so `this.data` speaks through that connection
|
|
5082
|
+
* and policies apply. On server-context work `this` is the base driver on
|
|
5083
|
+
* the owner connection, and they do not. Pinned by the
|
|
5084
|
+
* `"scopes context.data to the caller"` case in the `rls-enforcement` e2e
|
|
5085
|
+
* suite, because it is the kind of property that is easy to break from a
|
|
5086
|
+
* distance and impossible to notice.
|
|
5087
|
+
*
|
|
5088
|
+
* Previously returned through `as unknown as RebaseCallContext`, which
|
|
5089
|
+
* disabled checking for the whole object and let `driver` — documented in
|
|
5090
|
+
* the callbacks guide — sit on the runtime context while absent from the
|
|
5091
|
+
* contract. Both are declared now, so this is a plain typed return.
|
|
5092
|
+
*/
|
|
4707
5093
|
buildCallContext() {
|
|
4708
5094
|
return {
|
|
4709
5095
|
user: this.user,
|
|
@@ -5159,6 +5545,106 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
5159
5545
|
return saved;
|
|
5160
5546
|
});
|
|
5161
5547
|
}
|
|
5548
|
+
/**
|
|
5549
|
+
* Update many rows through the same pipeline as {@link save}, in one
|
|
5550
|
+
* transaction.
|
|
5551
|
+
*
|
|
5552
|
+
* Structurally the mirror of {@link saveMany} — same tx-bound sub-driver,
|
|
5553
|
+
* same deferred notifications, same per-row error labelling — but it calls
|
|
5554
|
+
* `save` with an explicit `id` and `status: "existing"`, which is precisely
|
|
5555
|
+
* what `saveMany` cannot do: that one passes `status: "new"` and keeps the
|
|
5556
|
+
* key inside `values`, so it inserts or upserts and can never target a
|
|
5557
|
+
* particular row.
|
|
5558
|
+
*
|
|
5559
|
+
* All-or-nothing, so an id matching no row aborts the batch. A partial
|
|
5560
|
+
* update is the outcome with no good recovery: the caller cannot tell which
|
|
5561
|
+
* half landed without re-reading everything.
|
|
5562
|
+
*/
|
|
5563
|
+
async updateMany({ path, updates, collection }) {
|
|
5564
|
+
return this.db.transaction(async (tx) => {
|
|
5565
|
+
const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
|
|
5566
|
+
txDriver.dataService = new DataService(tx, this.registry);
|
|
5567
|
+
txDriver.client = this.client;
|
|
5568
|
+
txDriver._deferNotifications = this._deferNotifications;
|
|
5569
|
+
txDriver._pendingNotifications = this._pendingNotifications;
|
|
5570
|
+
const saved = [];
|
|
5571
|
+
for (let i = 0; i < updates.length; i++) {
|
|
5572
|
+
const { id, values } = updates[i];
|
|
5573
|
+
try {
|
|
5574
|
+
if (!await txDriver.fetchOne({
|
|
5575
|
+
path,
|
|
5576
|
+
id: String(id),
|
|
5577
|
+
collection
|
|
5578
|
+
})) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
|
|
5579
|
+
statusCode: 404,
|
|
5580
|
+
code: "NOT_FOUND"
|
|
5581
|
+
});
|
|
5582
|
+
saved.push(await txDriver.save({
|
|
5583
|
+
path,
|
|
5584
|
+
id: String(id),
|
|
5585
|
+
values,
|
|
5586
|
+
collection,
|
|
5587
|
+
status: "existing"
|
|
5588
|
+
}));
|
|
5589
|
+
} catch (error) {
|
|
5590
|
+
throw Object.assign(new Error(`Update ${i} of ${updates.length} (id ${JSON.stringify(id)}) failed: ${error?.message ?? error}`, { cause: error }), {
|
|
5591
|
+
statusCode: error?.statusCode,
|
|
5592
|
+
code: error?.code,
|
|
5593
|
+
name: error?.name
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
return saved;
|
|
5598
|
+
});
|
|
5599
|
+
}
|
|
5600
|
+
/**
|
|
5601
|
+
* Delete many rows in one transaction, running the full delete pipeline —
|
|
5602
|
+
* `beforeDelete`, the delete, `afterDelete` — for each.
|
|
5603
|
+
*
|
|
5604
|
+
* Looping the single-row {@link delete} rather than emitting one
|
|
5605
|
+
* `DELETE ... WHERE id = ANY($1)` is the deliberate choice: a single
|
|
5606
|
+
* statement would be faster and would skip every callback, so a collection
|
|
5607
|
+
* relying on `beforeDelete` to veto or on `afterDelete` to clean up
|
|
5608
|
+
* dependents would behave differently depending on how many rows the caller
|
|
5609
|
+
* happened to delete at once. Same pipeline, one transaction.
|
|
5610
|
+
*/
|
|
5611
|
+
async deleteMany({ path, ids, collection }) {
|
|
5612
|
+
await this.db.transaction(async (tx) => {
|
|
5613
|
+
const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
|
|
5614
|
+
txDriver.dataService = new DataService(tx, this.registry);
|
|
5615
|
+
txDriver.client = this.client;
|
|
5616
|
+
txDriver._deferNotifications = this._deferNotifications;
|
|
5617
|
+
txDriver._pendingNotifications = this._pendingNotifications;
|
|
5618
|
+
for (let i = 0; i < ids.length; i++) {
|
|
5619
|
+
const id = ids[i];
|
|
5620
|
+
try {
|
|
5621
|
+
const existing = await txDriver.fetchOne({
|
|
5622
|
+
path,
|
|
5623
|
+
id: String(id),
|
|
5624
|
+
collection
|
|
5625
|
+
});
|
|
5626
|
+
if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
|
|
5627
|
+
statusCode: 404,
|
|
5628
|
+
code: "NOT_FOUND"
|
|
5629
|
+
});
|
|
5630
|
+
await txDriver.delete({
|
|
5631
|
+
row: {
|
|
5632
|
+
id: String(id),
|
|
5633
|
+
path,
|
|
5634
|
+
values: existing
|
|
5635
|
+
},
|
|
5636
|
+
collection
|
|
5637
|
+
});
|
|
5638
|
+
} catch (error) {
|
|
5639
|
+
throw Object.assign(new Error(`Delete ${i} of ${ids.length} (id ${JSON.stringify(id)}) failed: ${error?.message ?? error}`, { cause: error }), {
|
|
5640
|
+
statusCode: error?.statusCode,
|
|
5641
|
+
code: error?.code,
|
|
5642
|
+
name: error?.name
|
|
5643
|
+
});
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
});
|
|
5647
|
+
}
|
|
5162
5648
|
async delete({ row, collection }) {
|
|
5163
5649
|
const targetPath = row.path;
|
|
5164
5650
|
const targetRow = { ...row.values ?? {} };
|
|
@@ -6123,11 +6609,13 @@ var computeSharedRelationName = (rel, sourceCollection, _collections) => {
|
|
|
6123
6609
|
}
|
|
6124
6610
|
return fallback;
|
|
6125
6611
|
};
|
|
6126
|
-
var generateSchema = async (
|
|
6612
|
+
var generateSchema = async (allCollections, stripPolicies = false) => {
|
|
6613
|
+
const collections = relationalCollections(allCollections);
|
|
6127
6614
|
let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
|
|
6128
6615
|
const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
|
|
6129
6616
|
const hasVector = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "vector"));
|
|
6130
6617
|
const hasBinary = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "binary"));
|
|
6618
|
+
const hasSearch = collections.some((c) => buildSearchColumnSpec(c) !== void 0);
|
|
6131
6619
|
const pgCoreImports = [
|
|
6132
6620
|
"primaryKey",
|
|
6133
6621
|
"pgTable",
|
|
@@ -6152,7 +6640,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
6152
6640
|
];
|
|
6153
6641
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
6154
6642
|
if (hasVector) pgCoreImports.push("vector");
|
|
6155
|
-
if (hasBinary) pgCoreImports.push("customType");
|
|
6643
|
+
if (hasBinary || hasSearch) pgCoreImports.push("customType");
|
|
6156
6644
|
const uniqueSchemas = Array.from(new Set(collections.map((c) => isPostgresCollectionConfig(c) ? c.schema : void 0).filter(Boolean)));
|
|
6157
6645
|
if (uniqueSchemas.length > 0) pgCoreImports.push("pgSchema");
|
|
6158
6646
|
schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
|
|
@@ -6234,6 +6722,11 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
6234
6722
|
const columnString = getDrizzleColumn(propName, prop, collection, collections);
|
|
6235
6723
|
if (columnString) columns.add(columnString);
|
|
6236
6724
|
});
|
|
6725
|
+
const searchSpec = buildSearchColumnSpec(collection);
|
|
6726
|
+
if (searchSpec) {
|
|
6727
|
+
columns.add(` ${searchSpec.column}: customType({ dataType() { return 'tsvector'; } })("${searchSpec.column}").generatedAlwaysAs(sql\`${searchSpec.expression}\`)`);
|
|
6728
|
+
if (searchSpec.fuzzy) columns.add(` ${searchSpec.fuzzy.column}: text("${searchSpec.fuzzy.column}").generatedAlwaysAs(sql\`${searchSpec.fuzzy.expression}\`)`);
|
|
6729
|
+
}
|
|
6237
6730
|
if (!Array.from(columns).some((col) => col.includes(".primaryKey()"))) columns.add(" id: text(\"id\").primaryKey()");
|
|
6238
6731
|
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
6239
6732
|
const securityRules = getEffectiveSecurityRules(collection);
|
|
@@ -6937,6 +7430,8 @@ var ChannelHistoryStore = class {
|
|
|
6937
7430
|
last_seq BIGINT NOT NULL
|
|
6938
7431
|
)
|
|
6939
7432
|
`);
|
|
7433
|
+
await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_messages")));
|
|
7434
|
+
await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_cursors")));
|
|
6940
7435
|
this.tablesReady = true;
|
|
6941
7436
|
logger.info(`✅ [ChannelHistory] Retained channels ready (${this.rules.length} rule(s)).`);
|
|
6942
7437
|
}
|
|
@@ -7120,6 +7615,7 @@ var ChannelPresenceStore = class {
|
|
|
7120
7615
|
CREATE INDEX IF NOT EXISTS idx_channel_presence_last_seen
|
|
7121
7616
|
ON rebase.channel_presence (last_seen)
|
|
7122
7617
|
`);
|
|
7618
|
+
await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence")));
|
|
7123
7619
|
this.tablesReady = true;
|
|
7124
7620
|
}
|
|
7125
7621
|
/** Record (or refresh) a client's presence. */
|
|
@@ -7771,23 +8267,18 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
7771
8267
|
path: request.path,
|
|
7772
8268
|
collectionRequest: {
|
|
7773
8269
|
filter: request.filter,
|
|
8270
|
+
logical: request.logical,
|
|
7774
8271
|
orderBy: request.orderBy,
|
|
7775
8272
|
order: request.order,
|
|
7776
8273
|
limit: boundedLimit,
|
|
8274
|
+
offset: request.offset,
|
|
7777
8275
|
startAfter: request.startAfter,
|
|
7778
8276
|
databaseId: request.collection?.databaseId,
|
|
7779
8277
|
searchString: request.searchString
|
|
7780
8278
|
},
|
|
7781
8279
|
authContext
|
|
7782
8280
|
});
|
|
7783
|
-
const rows = await this.fetchCollectionWithAuth(request.path,
|
|
7784
|
-
filter: request.filter,
|
|
7785
|
-
orderBy: request.orderBy,
|
|
7786
|
-
order: request.order,
|
|
7787
|
-
limit: boundedLimit,
|
|
7788
|
-
startAfter: request.startAfter,
|
|
7789
|
-
searchString: request.searchString
|
|
7790
|
-
}, authContext);
|
|
8281
|
+
const rows = await this.fetchCollectionWithAuth(request.path, this._subscriptions.get(subscriptionId).collectionRequest, authContext);
|
|
7791
8282
|
this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
|
|
7792
8283
|
} catch (error) {
|
|
7793
8284
|
const sanitized = sanitizeErrorForClient(error, request.path);
|
|
@@ -7971,6 +8462,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
7971
8462
|
});
|
|
7972
8463
|
else fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
|
|
7973
8464
|
filter: collectionRequest.filter,
|
|
8465
|
+
logical: collectionRequest.logical,
|
|
7974
8466
|
orderBy: collectionRequest.orderBy,
|
|
7975
8467
|
order: collectionRequest.order,
|
|
7976
8468
|
limit: collectionRequest.limit,
|
|
@@ -9086,7 +9578,7 @@ function createBackupCron(config) {
|
|
|
9086
9578
|
enabled: config.enabled ?? true,
|
|
9087
9579
|
timeoutSeconds: 3600,
|
|
9088
9580
|
async handler({ log }) {
|
|
9089
|
-
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-
|
|
9581
|
+
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-Bww-Lg0s.js").then((n) => n.r);
|
|
9090
9582
|
const { destination } = config;
|
|
9091
9583
|
if (destination.kind !== "local" && !config.storage) throw new Error(`Backup destination is ${destination.kind} but no storage controller was provided. Pass the backend's configured StorageController to createBackupCron({ storage }).`);
|
|
9092
9584
|
log(`Starting backup of "${dbName}"…`);
|
|
@@ -9381,7 +9873,16 @@ function assertRelationsResolve(collections, registry) {
|
|
|
9381
9873
|
const defects = findRelationDefects(collections, registry);
|
|
9382
9874
|
if (defects.length === 0) return;
|
|
9383
9875
|
const lines = defects.map((d) => ` • ${d.collection}.${d.relationName} (${d.kind})\n ${d.problem}\n fix: ${d.fix}`);
|
|
9384
|
-
throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against
|
|
9876
|
+
throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against \`backend/src/schema.generated.ts\`.
|
|
9877
|
+
|
|
9878
|
+
Each of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
|
|
9879
|
+
|
|
9880
|
+
If the database was migrated recently — an upgrade, a \`db push\`, a restore — this file is
|
|
9881
|
+
probably older than the schema it describes. Regenerate it before changing anything else:
|
|
9882
|
+
|
|
9883
|
+
rebase schema generate
|
|
9884
|
+
|
|
9885
|
+
If it is already current, then the collection is what disagrees with it:
|
|
9385
9886
|
|
|
9386
9887
|
` + lines.join("\n\n") + "\n");
|
|
9387
9888
|
}
|
|
@@ -9592,7 +10093,7 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
9592
10093
|
if (idProp) {
|
|
9593
10094
|
const isId = "isId" in idProp ? idProp.isId : void 0;
|
|
9594
10095
|
if (isId === "uuid") userIdType = "UUID";
|
|
9595
|
-
else if (isId === "
|
|
10096
|
+
else if (isId === "increment") userIdType = "INTEGER";
|
|
9596
10097
|
}
|
|
9597
10098
|
}
|
|
9598
10099
|
try {
|
|
@@ -9625,43 +10126,42 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
9625
10126
|
const emailLengthConstraint = `"${authIdentifier("email_length_check")}"`;
|
|
9626
10127
|
const emailLowerUniqueIndex = authIdentifier("email_lower_key");
|
|
9627
10128
|
const verificationTokenIndex = authIdentifier("email_verification_token_idx");
|
|
10129
|
+
const usersColumnDdl = AUTH_USERS_COLUMNS.map((spec) => spec.column === "email" ? `${spec.column} ${authUsersColumnSql(spec)} CONSTRAINT ${emailLengthConstraint} CHECK (length(email) <= 320)` : `${spec.column} ${authUsersColumnSql(spec)}`).join(",\n ");
|
|
9628
10130
|
await db.execute(sql`
|
|
9629
10131
|
CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (
|
|
9630
10132
|
id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},
|
|
9631
|
-
|
|
9632
|
-
display_name TEXT,
|
|
9633
|
-
photo_url TEXT,
|
|
9634
|
-
roles TEXT[] DEFAULT '{}' NOT NULL,
|
|
9635
|
-
password_hash TEXT,
|
|
9636
|
-
email_verified BOOLEAN DEFAULT FALSE NOT NULL,
|
|
9637
|
-
email_verification_token TEXT,
|
|
9638
|
-
email_verification_sent_at TIMESTAMP WITH TIME ZONE,
|
|
9639
|
-
is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
|
|
9640
|
-
metadata JSONB DEFAULT '{}' NOT NULL,
|
|
9641
|
-
tokens_valid_after TIMESTAMP WITH TIME ZONE,
|
|
9642
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
9643
|
-
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
10133
|
+
${sql.raw(usersColumnDdl)}
|
|
9644
10134
|
)
|
|
9645
10135
|
`);
|
|
9646
|
-
|
|
9647
|
-
CREATE OR REPLACE FUNCTION ${sql.raw(`"${authSchema}"`)}.sync_uid_user_id() RETURNS trigger AS $$
|
|
9648
|
-
BEGIN
|
|
9649
|
-
IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN
|
|
9650
|
-
NEW.uid := NEW.user_id;
|
|
9651
|
-
ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN
|
|
9652
|
-
NEW.user_id := NEW.uid;
|
|
9653
|
-
END IF;
|
|
9654
|
-
RETURN NEW;
|
|
9655
|
-
END $$ LANGUAGE plpgsql
|
|
9656
|
-
`);
|
|
9657
|
-
for (const authTable of [
|
|
10136
|
+
const legacyFkTables = [
|
|
9658
10137
|
"user_identities",
|
|
9659
10138
|
"refresh_tokens",
|
|
9660
10139
|
"password_reset_tokens",
|
|
9661
10140
|
"magic_link_tokens",
|
|
9662
10141
|
"mfa_factors",
|
|
9663
10142
|
"recovery_codes"
|
|
9664
|
-
]
|
|
10143
|
+
];
|
|
10144
|
+
const legacyFkTableList = legacyFkTables.map((t) => `'${t}'`).join(", ");
|
|
10145
|
+
const legacyUserIdPresent = await db.execute(sql`
|
|
10146
|
+
SELECT 1
|
|
10147
|
+
FROM information_schema.columns
|
|
10148
|
+
WHERE table_schema = ${authSchema}
|
|
10149
|
+
AND table_name IN (${sql.raw(legacyFkTableList)})
|
|
10150
|
+
AND column_name = 'user_id'
|
|
10151
|
+
LIMIT 1
|
|
10152
|
+
`);
|
|
10153
|
+
if (legacyUserIdPresent.rows.length > 0) await db.execute(sql`
|
|
10154
|
+
CREATE OR REPLACE FUNCTION ${sql.raw(`"${authSchema}"`)}.sync_uid_user_id() RETURNS trigger AS $$
|
|
10155
|
+
BEGIN
|
|
10156
|
+
IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN
|
|
10157
|
+
NEW.uid := NEW.user_id;
|
|
10158
|
+
ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN
|
|
10159
|
+
NEW.user_id := NEW.uid;
|
|
10160
|
+
END IF;
|
|
10161
|
+
RETURN NEW;
|
|
10162
|
+
END $$ LANGUAGE plpgsql
|
|
10163
|
+
`);
|
|
10164
|
+
for (const authTable of legacyUserIdPresent.rows.length > 0 ? legacyFkTables : []) {
|
|
9665
10165
|
const qualified = `"${authSchema}"."${authTable}"`;
|
|
9666
10166
|
await db.execute(sql`
|
|
9667
10167
|
DO $$
|
|
@@ -9782,54 +10282,50 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
9782
10282
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
9783
10283
|
)
|
|
9784
10284
|
`);
|
|
9785
|
-
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
|
|
9786
10285
|
await db.transaction(async (tx) => {
|
|
9787
10286
|
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
|
|
9788
|
-
await tx.execute(sql
|
|
9789
|
-
CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
|
|
9790
|
-
SELECT COALESCE(
|
|
9791
|
-
NULLIF(current_setting('app.uid', true), ''),
|
|
9792
|
-
NULLIF(current_setting('app.user_id', true), '')
|
|
9793
|
-
);
|
|
9794
|
-
$$ LANGUAGE sql STABLE
|
|
9795
|
-
`);
|
|
9796
|
-
await tx.execute(sql`
|
|
9797
|
-
CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb AS $$
|
|
9798
|
-
SELECT COALESCE(
|
|
9799
|
-
NULLIF(current_setting('app.jwt', true), ''),
|
|
9800
|
-
'{}'
|
|
9801
|
-
)::jsonb;
|
|
9802
|
-
$$ LANGUAGE sql STABLE
|
|
9803
|
-
`);
|
|
9804
|
-
await tx.execute(sql`
|
|
9805
|
-
CREATE OR REPLACE FUNCTION auth.roles() RETURNS text AS $$
|
|
9806
|
-
SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
|
|
9807
|
-
$$ LANGUAGE sql STABLE
|
|
9808
|
-
`);
|
|
10287
|
+
for (const statement of RLS_BOOTSTRAP_STATEMENTS) await tx.execute(sql.raw(statement));
|
|
9809
10288
|
});
|
|
9810
|
-
for (const
|
|
9811
|
-
"
|
|
9812
|
-
|
|
9813
|
-
"roles TEXT[] DEFAULT '{}' NOT NULL",
|
|
9814
|
-
"password_hash TEXT",
|
|
9815
|
-
"email_verified BOOLEAN DEFAULT FALSE NOT NULL",
|
|
9816
|
-
"email_verification_token TEXT",
|
|
9817
|
-
"email_verification_sent_at TIMESTAMP WITH TIME ZONE",
|
|
9818
|
-
"is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
|
|
9819
|
-
"metadata JSONB DEFAULT '{}' NOT NULL",
|
|
9820
|
-
"tokens_valid_after TIMESTAMP WITH TIME ZONE",
|
|
9821
|
-
"created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
|
|
9822
|
-
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
|
|
9823
|
-
]) await db.execute(sql`
|
|
10289
|
+
for (const spec of AUTH_USERS_COLUMNS) {
|
|
10290
|
+
if (spec.column === "email") continue;
|
|
10291
|
+
await db.execute(sql`
|
|
9824
10292
|
ALTER TABLE ${sql.raw(usersTableName)}
|
|
9825
|
-
ADD COLUMN IF NOT EXISTS ${sql.raw(
|
|
10293
|
+
ADD COLUMN IF NOT EXISTS ${sql.raw(`${spec.column} ${authUsersColumnSql(spec)}`)}
|
|
9826
10294
|
`);
|
|
9827
|
-
|
|
9828
|
-
|
|
10295
|
+
}
|
|
10296
|
+
const usersColumnRows = (await db.execute(sql`
|
|
10297
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
9829
10298
|
FROM information_schema.columns
|
|
9830
10299
|
WHERE table_schema = ${usersSchema} AND table_name = ${resolvedTable}
|
|
9831
|
-
`);
|
|
9832
|
-
const usersColumnTypes = new Map(
|
|
10300
|
+
`)).rows;
|
|
10301
|
+
const usersColumnTypes = new Map(usersColumnRows.map((row) => [row.column_name, row.data_type]));
|
|
10302
|
+
const usersColumnState = new Map(usersColumnRows.map((row) => [row.column_name, row]));
|
|
10303
|
+
for (const spec of AUTH_USERS_COLUMNS) {
|
|
10304
|
+
const state = usersColumnState.get(spec.column);
|
|
10305
|
+
if (!state) continue;
|
|
10306
|
+
if (spec.default !== void 0 && state.column_default === null) {
|
|
10307
|
+
await db.execute(sql`
|
|
10308
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
10309
|
+
ALTER COLUMN ${sql.raw(`"${spec.column}"`)} SET DEFAULT ${sql.raw(spec.default)}
|
|
10310
|
+
`);
|
|
10311
|
+
logger.info(`🔧 Restored the default on ${usersTableName}.${spec.column}`);
|
|
10312
|
+
}
|
|
10313
|
+
if (!spec.notNull || state.is_nullable !== "YES") continue;
|
|
10314
|
+
if (spec.default !== void 0) await db.execute(sql`
|
|
10315
|
+
UPDATE ${sql.raw(usersTableName)}
|
|
10316
|
+
SET ${sql.raw(`"${spec.column}"`)} = ${sql.raw(spec.default)}
|
|
10317
|
+
WHERE ${sql.raw(`"${spec.column}"`)} IS NULL
|
|
10318
|
+
`);
|
|
10319
|
+
try {
|
|
10320
|
+
await db.execute(sql`
|
|
10321
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
10322
|
+
ALTER COLUMN ${sql.raw(`"${spec.column}"`)} SET NOT NULL
|
|
10323
|
+
`);
|
|
10324
|
+
logger.info(`🔧 Restored NOT NULL on ${usersTableName}.${spec.column}`);
|
|
10325
|
+
} catch (err) {
|
|
10326
|
+
logger.warn(`⚠️ ${usersTableName}.${spec.column} should be NOT NULL but still holds NULLs, so the constraint was not applied. Fill or remove those rows and restart: ` + (err instanceof Error ? err.message : String(err)));
|
|
10327
|
+
}
|
|
10328
|
+
}
|
|
9833
10329
|
for (const column of [
|
|
9834
10330
|
"email",
|
|
9835
10331
|
"display_name",
|
|
@@ -10011,10 +10507,12 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
10011
10507
|
[authSchema, "user_identities"],
|
|
10012
10508
|
[authSchema, "refresh_tokens"],
|
|
10013
10509
|
[authSchema, "password_reset_tokens"],
|
|
10510
|
+
[authSchema, "magic_link_tokens"],
|
|
10014
10511
|
[authSchema, "app_config"],
|
|
10015
10512
|
[authSchema, "mfa_factors"],
|
|
10016
10513
|
[authSchema, "mfa_challenges"],
|
|
10017
|
-
[authSchema, "recovery_codes"]
|
|
10514
|
+
[authSchema, "recovery_codes"],
|
|
10515
|
+
[authSchema, "schema_meta"]
|
|
10018
10516
|
];
|
|
10019
10517
|
for (const [schemaName, tableName] of authTablePairs) if ((await db.execute(sql`
|
|
10020
10518
|
SELECT 1
|
|
@@ -10034,6 +10532,9 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
10034
10532
|
logger.warn(`⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`);
|
|
10035
10533
|
}
|
|
10036
10534
|
await stampAuthSchemaVersion(db, authSchema);
|
|
10535
|
+
await revokeInternalTableAccess(async (text) => {
|
|
10536
|
+
await db.execute(sql.raw(text));
|
|
10537
|
+
}, authSchema, { onError: (table, err) => logger.warn(`🔐 Could not revoke authenticated-role access to "${authSchema}"."${table}": ` + (err instanceof Error ? err.message : String(err))) });
|
|
10037
10538
|
logger.info("✅ Auth tables ready");
|
|
10038
10539
|
} catch (error) {
|
|
10039
10540
|
if (error instanceof AuthSchemaVersionError) throw error;
|
|
@@ -11362,6 +11863,7 @@ async function ensureHistoryTableExists(db) {
|
|
|
11362
11863
|
CREATE INDEX IF NOT EXISTS idx_history_time
|
|
11363
11864
|
ON rebase.entity_history(table_name, entity_id, updated_at DESC)
|
|
11364
11865
|
`);
|
|
11866
|
+
await db.execute(sql.raw(revokeInternalTableSql("rebase", "entity_history")));
|
|
11365
11867
|
logger.info("✅ Entity history table ready");
|
|
11366
11868
|
} catch (error) {
|
|
11367
11869
|
logger.error("❌ Failed to create row history table", { error });
|
|
@@ -12083,12 +12585,12 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12083
12585
|
const runSql = async (text) => {
|
|
12084
12586
|
return (await schemaAwareDb.execute(sql.raw(text))).rows ?? [];
|
|
12085
12587
|
};
|
|
12588
|
+
await warnOnRoleSchemaCollision(runSql);
|
|
12086
12589
|
const posture = await detectConnectionPosture(runSql);
|
|
12087
12590
|
if (posture.privileged) {
|
|
12088
12591
|
await ensureAppRole(runSql, [
|
|
12089
12592
|
"public",
|
|
12090
12593
|
"rebase",
|
|
12091
|
-
"auth",
|
|
12092
12594
|
...registry.getCollections().map((c) => c.schema).filter((s) => typeof s === "string")
|
|
12093
12595
|
]);
|
|
12094
12596
|
driver.rlsUserRole = REBASE_USER_ROLE;
|
|
@@ -12098,6 +12600,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12098
12600
|
} else logger.info(`🔐 RLS enforcement: connection role "${posture.role}" is subject to RLS natively; no role switch needed.`);
|
|
12099
12601
|
await validatePolicyPgRoles(runSql, registry.getCollections(), driver.rlsUserRole ?? posture.role);
|
|
12100
12602
|
warnOnAnonymousGrants(registry.getCollections());
|
|
12603
|
+
warnOnLegacyRlsFunctions(registry.getCollections());
|
|
12101
12604
|
}
|
|
12102
12605
|
if (driver.branchService) try {
|
|
12103
12606
|
await driver.branchService.ensureBranchMetadataTable();
|
|
@@ -12313,7 +12816,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12313
12816
|
*/
|
|
12314
12817
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
12315
12818
|
const internals = driverResult.internals;
|
|
12316
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
12819
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-CyMBc7wo.js");
|
|
12317
12820
|
const plan = await ensureCollectionTables({ async query(text) {
|
|
12318
12821
|
const result = await internals.db.execute(sql.raw(text));
|
|
12319
12822
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
@@ -12338,13 +12841,24 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12338
12841
|
*/
|
|
12339
12842
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
12340
12843
|
const internals = driverResult.internals;
|
|
12341
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
12844
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-C8H7ig-m.js");
|
|
12342
12845
|
const outcome = await ensureCollectionPolicies({ async query(text) {
|
|
12343
12846
|
const result = await internals.db.execute(sql.raw(text));
|
|
12344
12847
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
12345
12848
|
} }, collections, log);
|
|
12346
12849
|
for (const skip of outcome.skipped) logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
|
|
12347
12850
|
for (const failure of outcome.failures) logger.warn(`🔐 [rls] Could not fully apply policies to "${failure.table}" — it stays locked (denies) until this is resolved: ${failure.error}`);
|
|
12851
|
+
try {
|
|
12852
|
+
const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-Bpv3nUZo.js").then((n) => n.n);
|
|
12853
|
+
await dropLegacyAuthSchema(async (text) => {
|
|
12854
|
+
return (await internals.db.execute(sql.raw(text))).rows ?? [];
|
|
12855
|
+
}, {
|
|
12856
|
+
info: (m) => logger.info(m),
|
|
12857
|
+
warn: (m) => logger.warn(m)
|
|
12858
|
+
});
|
|
12859
|
+
} catch (err) {
|
|
12860
|
+
logger.info("Left the legacy `auth` schema in place: " + (err instanceof Error ? err.message : String(err)));
|
|
12861
|
+
}
|
|
12348
12862
|
return { applied: outcome.policiesApplied };
|
|
12349
12863
|
},
|
|
12350
12864
|
getAdmin(driverResult) {
|
|
@@ -12352,7 +12866,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12352
12866
|
},
|
|
12353
12867
|
mountRoutes(app, basePath, driverResult) {},
|
|
12354
12868
|
async initializeWebsockets(server, realtimeService, driver, config, adapter) {
|
|
12355
|
-
const { createPostgresWebSocket } = await import("./websocket-
|
|
12869
|
+
const { createPostgresWebSocket } = await import("./websocket-D0TBU3ia.js").then((n) => n.n);
|
|
12356
12870
|
createPostgresWebSocket(server, realtimeService, driver, config, adapter);
|
|
12357
12871
|
}
|
|
12358
12872
|
};
|