@rebasepro/server-postgres 0.25.1 → 0.26.0
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/{BranchService-ClklxyAQ.js → BranchService-CWBnDhcv.js} +25 -9
- package/dist/BranchService-CWBnDhcv.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +13 -0
- package/dist/PostgresBootstrapper.d.ts +7 -0
- package/dist/auth/services.d.ts +8 -1
- package/dist/auth/verify-auth-tables.d.ts +21 -0
- package/dist/{backup-cli-gijeXe0u.js → backup-cli-PoRG-zEq.js} +3 -3
- package/dist/{backup-cli-gijeXe0u.js.map → backup-cli-PoRG-zEq.js.map} +1 -1
- package/dist/{boot-ddl-DYH95Qeg.js → boot-ddl-EsgqDkx0.js} +2 -2
- package/dist/{boot-ddl-DYH95Qeg.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
- package/dist/branch-diff-CxE4VlxT.js +1396 -0
- package/dist/branch-diff-CxE4VlxT.js.map +1 -0
- package/dist/branch-diff-report-DpvVf7hR.js +48 -0
- package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
- package/dist/branching/branch-diff-report.d.ts +7 -0
- package/dist/branching/branch-diff.d.ts +115 -0
- package/dist/{cli-console-CQrQ0lAY.js → cli-console-f0k6_TsJ.js} +38 -5
- package/dist/cli-console-f0k6_TsJ.js.map +1 -0
- package/dist/cli-console.d.ts +17 -1
- package/dist/{cli-errors-DiA0XFIS.js → cli-errors-DsYyNYzd.js} +53 -2
- package/dist/cli-errors-DsYyNYzd.js.map +1 -0
- package/dist/cli.js +102 -122
- package/dist/cli.js.map +1 -1
- package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
- package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
- package/dist/{deferred-index-build-BE0I6sYU.js → deferred-index-build-CqaOv1Xx.js} +3 -3
- package/dist/{deferred-index-build-BE0I6sYU.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
- package/dist/{doctor-BT--1kq3.js → doctor-DW_mlTzz.js} +81 -33
- package/dist/doctor-DW_mlTzz.js.map +1 -0
- package/dist/{ensure-collection-policies-BnGOURpG.js → ensure-collection-policies-DLWgabQ2.js} +5 -5
- package/dist/{ensure-collection-policies-BnGOURpG.js.map → ensure-collection-policies-DLWgabQ2.js.map} +1 -1
- package/dist/{ensure-collection-tables-Cet4650m.js → ensure-collection-tables-CFIRq8ni.js} +5 -5
- package/dist/{ensure-collection-tables-Cet4650m.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
- package/dist/{ensure-tables-l_OgFQjN.js → ensure-tables-4nA-tfwi.js} +4 -4
- package/dist/{ensure-tables-l_OgFQjN.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
- package/dist/{enum-labels-kept-nheku1pB.js → enum-labels-kept-PcPW65xy.js} +2 -2
- package/dist/{enum-labels-kept-nheku1pB.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +7 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.es.js +226 -8
- package/dist/index.es.js.map +1 -1
- package/dist/{db-subcommands-BPoHCxJR.js → migrate-baseline-B-2Ef4Mv.js} +266 -41
- package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
- package/dist/{migrate-policy-stamps-3eqfhes6.js → migrate-policy-stamps-BQtFHApx.js} +5 -5
- package/dist/{migrate-policy-stamps-3eqfhes6.js.map → migrate-policy-stamps-BQtFHApx.js.map} +1 -1
- package/dist/{open-rebase-EmqVmS14.js → open-rebase-C8jQ6s5a.js} +292 -46
- package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
- package/dist/open-rebase.d.ts +9 -1
- package/dist/{pg-error-utils-CFEnm2m9.js → pg-error-utils-DhiUTlVx.js} +2 -2
- package/dist/{pg-error-utils-CFEnm2m9.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
- package/dist/{push-index-prebuild-COd7i6wm.js → push-index-prebuild-UTvB5pqH.js} +3 -3
- package/dist/{push-index-prebuild-COd7i6wm.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
- package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
- package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
- package/dist/{push-legacy-renames-CLgYYwPE.js → push-legacy-renames-BwaKi8xF.js} +3 -3
- package/dist/{push-legacy-renames-CLgYYwPE.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
- package/dist/{push-policy-plan-D2gpQsdv.js → push-policy-plan-DQ2tWceD.js} +2 -2
- package/dist/{push-policy-plan-D2gpQsdv.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
- package/dist/{rls-enforcement-DDyNoOSy.js → rls-enforcement-BOHuHVlc.js} +42 -3
- package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
- package/dist/schema/destructive-sql.d.ts +16 -0
- package/dist/schema/doctor-cli.js +3 -3
- package/dist/schema/introspect-db.js +1 -1
- package/dist/schema/migration-freshness.d.ts +69 -0
- package/dist/schema/push-kept-defaults.d.ts +57 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
- package/dist/security/rls-enforcement.d.ts +18 -0
- package/dist/services/FetchService.d.ts +12 -0
- package/dist/services/channel-presence.d.ts +3 -1
- package/dist/services/nested-path.d.ts +16 -0
- package/dist/services/realtimeService.d.ts +18 -1
- package/dist/{websocket-BKZwsjmf.js → websocket-CKYc3V4N.js} +41 -12
- package/dist/websocket-CKYc3V4N.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-ClklxyAQ.js.map +0 -1
- package/dist/cli-console-CQrQ0lAY.js.map +0 -1
- package/dist/cli-errors-DiA0XFIS.js.map +0 -1
- package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
- package/dist/db-subcommands-BPoHCxJR.js.map +0 -1
- package/dist/doctor-BT--1kq3.js.map +0 -1
- package/dist/open-rebase-EmqVmS14.js.map +0 -1
- package/dist/rls-enforcement-DDyNoOSy.js.map +0 -1
- package/dist/websocket-BKZwsjmf.js.map +0 -1
|
@@ -2,26 +2,28 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { i as createPostgresDatabaseConnection } from "./connection-czvI7TZL.js";
|
|
4
4
|
import { _ as visibleColumnProjection, a as buildSearchColumnSpec, r as SEARCH_UNACCENT_FN, s as hiddenColumnsOption } from "./search-column-BM-GV6vH.js";
|
|
5
|
-
import { a as diagnoseDbError, c as isEconnrefused, n as closedDatabaseName,
|
|
6
|
-
import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-
|
|
7
|
-
import { t as BranchService } from "./BranchService-
|
|
8
|
-
import { a as ensureAppRoleGrants, d as
|
|
5
|
+
import { a as diagnoseDbError, c as isEconnrefused, n as closedDatabaseName, r as deepestErrorMessage, rt as extractCauseMessage, u as parseHostInfo } from "./cli-errors-DsYyNYzd.js";
|
|
6
|
+
import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-DhiUTlVx.js";
|
|
7
|
+
import { t as BranchService } from "./BranchService-CWBnDhcv.js";
|
|
8
|
+
import { a as ensureAppRoleGrants, c as revokeUserRoleSql, d as warnOnAnonymousGrants, f as warnOnLegacyRlsFunctions, i as ensureAppRole, n as applyAuthContext, o as findExposedInternalTables, p as warnOnRoleSchemaCollision, r as detectConnectionPosture, t as REBASE_USER_ROLE$1, u as validatePolicyPgRoles } from "./rls-enforcement-BOHuHVlc.js";
|
|
9
9
|
import { u as resolveColumnName } from "./column-plan-helpers-IG6uFb1s.js";
|
|
10
|
+
import { t as AUTH_USERS_COLUMNS } from "./auth-users-columns-C72EMoDJ.js";
|
|
10
11
|
import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
|
|
11
12
|
import { t as generateSchema } from "./generate-drizzle-schema-logic-CPzCRD-8.js";
|
|
12
13
|
import { r as renderPgType } from "./render-ddl-4v88y-Gv.js";
|
|
13
|
-
import { u as typesAgree } from "./deferred-index-build-
|
|
14
|
-
import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-
|
|
15
|
-
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-
|
|
14
|
+
import { u as typesAgree } from "./deferred-index-build-CqaOv1Xx.js";
|
|
15
|
+
import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-EsgqDkx0.js";
|
|
16
|
+
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-CFIRq8ni.js";
|
|
17
|
+
import { a as mergeDatabaseBranch, i as diffDatabaseBranch } from "./branch-diff-CxE4VlxT.js";
|
|
16
18
|
import { t as DatabasePoolManager } from "./databasePoolManager-3stMNFL-.js";
|
|
17
|
-
import { a as resolveAuthSchema, i as probeAuthSchema, r as assertAuthSchemaCompatible, t as ensureAuthTablesExist } from "./ensure-tables-
|
|
19
|
+
import { a as resolveAuthSchema, i as probeAuthSchema, r as assertAuthSchemaCompatible, t as ensureAuthTablesExist } from "./ensure-tables-4nA-tfwi.js";
|
|
18
20
|
import { a as buildBaasSchema, c as introspectSchema, i as warnOnGeneratedSchemaDrift, l as readRlsStatus, n as diffGeneratedSchemaAgainstCatalogue, o as readCatalogueSchema, s as buildCollectionsFromSchema } from "./generated-schema-diff-eZCklRZQ.js";
|
|
19
21
|
import { Client, DatabaseError, Pool } from "pg";
|
|
20
22
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
21
23
|
import { ApiError, assertQueryFieldsReadable, createDdlBootstrapper, createEmailService, logger, rawQueryLoggingEnabled, resolveBatchRefs, rowETag, splitFieldOps } from "@rebasepro/server";
|
|
22
24
|
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, is, isNotNull, isNull, isTable, lt, lte, not, notInArray, or, relations, sql } from "drizzle-orm";
|
|
23
25
|
import { PgArray, PgNumeric, PgTable, PgTimestamp, PgTimestampString, PgTransaction, bigint, boolean, getTableConfig, index, integer, jsonb, pgSchema, pgTable, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
|
|
24
|
-
import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, INCLUDE_COUNT_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit, resourceEnvSuffix, resourceKeyOf } from "@rebasepro/types";
|
|
26
|
+
import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, INCLUDE_COUNT_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit, resourceEnvSuffix, resourceKeyOf, roleNameProblem } from "@rebasepro/types";
|
|
25
27
|
import { COMPOSITE_ID_SEPARATOR, COMPOSITE_ID_SEPARATOR as COMPOSITE_ID_SEPARATOR$1, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, OrderBySpecError, REBASE_USER_ROLE, UnknownFilterOperatorError, apiHiddenFieldNames, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createAccessorSlugResolver, createRelationRef, createRelationRefWithData, deserializeFilter, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys, getJunctionConfigForRelation, getTableName as getTableName$1, getTenantConfig, hasFieldAccessRules, includableRelationNames, isAddressableId, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeToEntityRelation, parseIdValues, parseIdValues as parseIdValues$1, parseOrderBySpecStrict, queryableFieldNames, requireCallbackClient, requireCallbackCollection, resolveCollectionRelations, resolveTenantWrite, restoresSoftDeletedRow, revokeInternalTableSql, softDeleteFieldOf, sqlRows, tenantBypassRoles, toCallbackError, toFilterTuples, unknownAggregateField, unknownFilterField, unknownIncludeRelation, unknownOrderByField, unknownProjectionField, unknownRelationTargetField, unknownVectorProperty, updateDateAutoValues, updateUserAutoValues, withRelationReferencesOnly } from "@rebasepro/common";
|
|
26
28
|
import { camelCase, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, splitSearchTerms, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
|
|
27
29
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -4403,6 +4405,38 @@ function isJunctionBackedRelation(relation) {
|
|
|
4403
4405
|
return relation.shared;
|
|
4404
4406
|
}
|
|
4405
4407
|
/**
|
|
4408
|
+
* Reject a create or update whose body names a relation Rebase does not write
|
|
4409
|
+
* — a multi-hop `via` (`writable: false`) — before anything is read or written.
|
|
4410
|
+
*
|
|
4411
|
+
* A single-hop `via` is written through this door as it shipped (to-one: the
|
|
4412
|
+
* target's key; to-many: one junction's rows). A longer chain used to get as
|
|
4413
|
+
* far as the writer: a to-many one failed inside the transaction as a 500
|
|
4414
|
+
* RELATION_MISCONFIGURED telling the caller to regenerate a schema that was
|
|
4415
|
+
* fine, and a to-one one re-pointed the target at the first step's column.
|
|
4416
|
+
*
|
|
4417
|
+
* Matched the way the write path matches: a key that is a relation's own name
|
|
4418
|
+
* (the to-many split), or a `relation` property that resolves to one (the
|
|
4419
|
+
* to-one transformer). An `undefined` value is no value — the transformer skips
|
|
4420
|
+
* it — so it is not a write; `null` is, since it would unlink.
|
|
4421
|
+
*/
|
|
4422
|
+
function assertNoReadOnlyRelationValues(values, collection) {
|
|
4423
|
+
const properties = collection.properties ?? {};
|
|
4424
|
+
let relations;
|
|
4425
|
+
const named = [];
|
|
4426
|
+
for (const [key, value] of Object.entries(values)) {
|
|
4427
|
+
if (value === void 0) continue;
|
|
4428
|
+
relations ??= resolveCollectionRelations(collection);
|
|
4429
|
+
const relation = relations[key] ?? (properties[key]?.type === "relation" ? findRelation(relations, key) : void 0);
|
|
4430
|
+
if (relation && !relation.writable) named.push(key);
|
|
4431
|
+
}
|
|
4432
|
+
if (named.length === 0) return;
|
|
4433
|
+
const list = named.map((key) => `'${key}'`).join(", ");
|
|
4434
|
+
throw ApiError.badRequest(`${list} on '${collection.slug}' ${named.length > 1 ? "are multi-hop `via` relations" : "is a multi-hop `via` relation"}, which is read-only: only single-hop via relations are writable — a to-one chain of one step, or a to-many chain through one junction table. Write the rows the chain joins directly.`, "RELATION_NOT_WRITABLE", {
|
|
4435
|
+
collection: collection.slug,
|
|
4436
|
+
fields: named
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
/**
|
|
4406
4440
|
* Reject a nested write whose final segment is a to-one relation.
|
|
4407
4441
|
*
|
|
4408
4442
|
* There is no column on the target row that records a to-one parent — the
|
|
@@ -4414,8 +4448,8 @@ function isJunctionBackedRelation(relation) {
|
|
|
4414
4448
|
*/
|
|
4415
4449
|
function assertWritableThrough(hop, path) {
|
|
4416
4450
|
const { relation } = hop;
|
|
4417
|
-
if (relation.writable && relation.cardinality === "many") return;
|
|
4418
|
-
if (
|
|
4451
|
+
if (relation.kind !== "via" && relation.writable && relation.cardinality === "many") return;
|
|
4452
|
+
if (relation.kind === "via") throw ApiError.badRequest(`"${path}" ends in '${hop.relationKey}', a \`via\` relation, which a nested path does not write through — write the row at "${hop.targetCollection.slug}" directly` + (relation.writable ? `, and link it by setting '${hop.relationKey}' in an update of the '${hop.parentCollection.slug}' row.` : "."), "RELATION_NOT_WRITABLE");
|
|
4419
4453
|
throw ApiError.badRequest(`"${path}" ends in the to-one relation '${hop.relationKey}', which cannot be written through: the foreign key for a to-one relation lives on '${hop.parentCollection.slug}', not on '${hop.targetCollection.slug}'. Write the target row at "${hop.targetCollection.slug}" and set '${hop.relationKey}' on the parent instead.`, "RELATION_NOT_WRITABLE");
|
|
4420
4454
|
}
|
|
4421
4455
|
//#endregion
|
|
@@ -5338,6 +5372,38 @@ var FetchService = class FetchService {
|
|
|
5338
5372
|
return values;
|
|
5339
5373
|
}
|
|
5340
5374
|
/**
|
|
5375
|
+
* Every row of a collection's table, as stored, a page at a time, in key
|
|
5376
|
+
* order — the driver's `scanStoredRows`.
|
|
5377
|
+
*
|
|
5378
|
+
* Deliberately none of the read pipeline: no `beforeQuery` (a sweep has no
|
|
5379
|
+
* caller to scope to, and the documented hook narrows a caller-less read to
|
|
5380
|
+
* the rows owned by nobody), no soft-delete filter, no `afterRead`, no
|
|
5381
|
+
* field stripping. The columns come back under their table names; a sweep
|
|
5382
|
+
* reads values, not keys. Paged by the key rather than an offset, so a row
|
|
5383
|
+
* deleted during the scan never shifts another past the next page.
|
|
5384
|
+
*/
|
|
5385
|
+
async *scanStoredRows(collectionPath, pageSize = 500) {
|
|
5386
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
5387
|
+
const table = getTableForCollection(collection, this.registry);
|
|
5388
|
+
const keys = primaryKeyColumns(table, requirePrimaryKeys(collection, this.registry), collectionPath);
|
|
5389
|
+
const keyNames = keys.map((column) => {
|
|
5390
|
+
const entry = Object.entries(getTableColumns(table)).find(([, candidate]) => candidate === column);
|
|
5391
|
+
if (!entry) throw new Error(`Key column '${column.name}' not found in table for collection '${collectionPath}'`);
|
|
5392
|
+
return entry[0];
|
|
5393
|
+
});
|
|
5394
|
+
const limit = Math.max(1, pageSize);
|
|
5395
|
+
let after;
|
|
5396
|
+
for (;;) {
|
|
5397
|
+
const past = after ? or(...keys.map((column, i) => and(...keys.slice(0, i).map((earlier, j) => eq(earlier, after[j])), gt(column, after[i])))) : void 0;
|
|
5398
|
+
const query = this.db.select().from(table);
|
|
5399
|
+
const rows = await (past ? query.where(past) : query).orderBy(...keys.map((column) => asc(column))).limit(limit);
|
|
5400
|
+
if (rows.length > 0) yield rows;
|
|
5401
|
+
if (rows.length < limit) return;
|
|
5402
|
+
const last = rows[rows.length - 1];
|
|
5403
|
+
after = keyNames.map((name) => last[name]);
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
/**
|
|
5341
5407
|
* Fetch a collection of rows
|
|
5342
5408
|
*/
|
|
5343
5409
|
async fetchCollection(collectionPath, options = {}) {
|
|
@@ -6803,6 +6869,7 @@ var PersistService = class {
|
|
|
6803
6869
|
}
|
|
6804
6870
|
}
|
|
6805
6871
|
const collection = getCollectionByPath(effectiveCollectionPath, this.registry);
|
|
6872
|
+
assertNoReadOnlyRelationValues(effectiveValues, collection);
|
|
6806
6873
|
const table = getTableForCollection(collection, this.registry);
|
|
6807
6874
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6808
6875
|
idInfoArray.map((info) => info.fieldName);
|
|
@@ -9476,10 +9543,29 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
9476
9543
|
createBranch: this.branchService.createBranch.bind(this.branchService),
|
|
9477
9544
|
deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
|
|
9478
9545
|
listBranches: this.branchService.listBranches.bind(this.branchService),
|
|
9479
|
-
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
|
|
9546
|
+
getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService),
|
|
9547
|
+
diffBranch: async (name, options) => this.nameDiffCollections(await diffDatabaseBranch(this.admin, name, options)),
|
|
9548
|
+
mergeBranch: (name, options) => mergeDatabaseBranch(this.admin, name, options)
|
|
9480
9549
|
} : {}
|
|
9481
9550
|
};
|
|
9482
9551
|
}
|
|
9552
|
+
/** The diff with each table named by the collection it backs, where one does. */
|
|
9553
|
+
nameDiffCollections(diff) {
|
|
9554
|
+
const collections = this.registry?.getCollections() ?? [];
|
|
9555
|
+
const slugOf = (schema, table) => collections.find((collection) => {
|
|
9556
|
+
return ("schema" in collection && typeof collection.schema === "string" ? collection.schema : "public") === schema && getTableName$1(collection) === table;
|
|
9557
|
+
})?.slug;
|
|
9558
|
+
return {
|
|
9559
|
+
...diff,
|
|
9560
|
+
data: diff.data.map((entry) => {
|
|
9561
|
+
const collection = slugOf(entry.schema, entry.table);
|
|
9562
|
+
return collection ? {
|
|
9563
|
+
...entry,
|
|
9564
|
+
collection
|
|
9565
|
+
} : entry;
|
|
9566
|
+
})
|
|
9567
|
+
};
|
|
9568
|
+
}
|
|
9483
9569
|
/**
|
|
9484
9570
|
* `SQLAdmin.listen`: a `LISTEN` on a connection of its own, through the
|
|
9485
9571
|
* listener CDC and the channel bus use, which reconnects and heartbeats.
|
|
@@ -9574,6 +9660,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
9574
9660
|
const client = this.client;
|
|
9575
9661
|
return {
|
|
9576
9662
|
user: this.user,
|
|
9663
|
+
...this.user?.credential ? { credential: this.user.credential } : {},
|
|
9577
9664
|
driver: this,
|
|
9578
9665
|
data: this.data,
|
|
9579
9666
|
get client() {
|
|
@@ -9743,6 +9830,15 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
9743
9830
|
return rows;
|
|
9744
9831
|
}
|
|
9745
9832
|
/**
|
|
9833
|
+
* Every stored row of a collection, past every read hook, for a whole-store
|
|
9834
|
+
* sweep — see `DataDriver.scanStoredRows`. Only this unscoped driver has
|
|
9835
|
+
* it: {@link AuthenticatedPostgresBackendDriver} reads as a caller, and a
|
|
9836
|
+
* caller's reads are scoped.
|
|
9837
|
+
*/
|
|
9838
|
+
scanStoredRows(props) {
|
|
9839
|
+
return this.dataService.getFetchService().scanStoredRows(props.path, props.pageSize);
|
|
9840
|
+
}
|
|
9841
|
+
/**
|
|
9746
9842
|
* Listen to a collection: the rows now, then again after every change.
|
|
9747
9843
|
*
|
|
9748
9844
|
* The realtime service runs every read, the first one included, as
|
|
@@ -11082,6 +11178,7 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
11082
11178
|
const client = this.delegate.client;
|
|
11083
11179
|
writeScope?.setSettledContext({
|
|
11084
11180
|
user: this.user,
|
|
11181
|
+
...this.user?.credential ? { credential: this.user.credential } : {},
|
|
11085
11182
|
driver: this,
|
|
11086
11183
|
data: this.data,
|
|
11087
11184
|
get client() {
|
|
@@ -13042,9 +13139,13 @@ var ChannelPresenceStore = class {
|
|
|
13042
13139
|
* `tablesReady` is now set from a probe of what exists, not from having been
|
|
13043
13140
|
* the instance that created it.
|
|
13044
13141
|
*/
|
|
13045
|
-
async ensureTables() {
|
|
13046
|
-
if (this.tablesReady) return;
|
|
13142
|
+
async ensureTables(options = {}) {
|
|
13143
|
+
if (this.tablesReady) return true;
|
|
13047
13144
|
const ddl = drizzleDdlBootstrapper(this.db, "channel-presence");
|
|
13145
|
+
if (options.provision === false) {
|
|
13146
|
+
this.tablesReady = await ddl.isReadable("rebase.channel_presence");
|
|
13147
|
+
return this.tablesReady;
|
|
13148
|
+
}
|
|
13048
13149
|
await ddl.ensureObject("rebase schema", "CREATE SCHEMA IF NOT EXISTS rebase");
|
|
13049
13150
|
await ddl.ensureObject("channel_presence table", `
|
|
13050
13151
|
CREATE TABLE IF NOT EXISTS rebase.channel_presence (
|
|
@@ -13068,6 +13169,7 @@ var ChannelPresenceStore = class {
|
|
|
13068
13169
|
await ddl.step("channel_presence revoke", () => this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence"))));
|
|
13069
13170
|
this.tablesReady = true;
|
|
13070
13171
|
}
|
|
13172
|
+
return this.tablesReady;
|
|
13071
13173
|
}
|
|
13072
13174
|
/** Record (or refresh) a client's presence. */
|
|
13073
13175
|
async track(channel, clientId, state) {
|
|
@@ -13750,6 +13852,14 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
13750
13852
|
refetchQueue = /* @__PURE__ */ new Set();
|
|
13751
13853
|
/** Group refetches running now — at most {@link maxConcurrentRefetches}. */
|
|
13752
13854
|
refetchesRunning = 0;
|
|
13855
|
+
/**
|
|
13856
|
+
* What a group refetch runs inside — see {@link setScopeRunner}. A refetch
|
|
13857
|
+
* runs on a timer armed by whatever announced the change; one armed by a
|
|
13858
|
+
* change-capture notification inherits the LISTEN connection's context,
|
|
13859
|
+
* which is no backend's, so a hook the read reached saw the process
|
|
13860
|
+
* default as `rebase`.
|
|
13861
|
+
*/
|
|
13862
|
+
runInScope = (fn) => fn();
|
|
13753
13863
|
/** See {@link refetchConcurrencyFor}. */
|
|
13754
13864
|
maxConcurrentRefetches;
|
|
13755
13865
|
/**
|
|
@@ -14018,6 +14128,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14018
14128
|
if (group.pending !== "all") group.pending.delete(key);
|
|
14019
14129
|
if (group.members.size > 0) return;
|
|
14020
14130
|
if (group.timer) clearTimeout(group.timer);
|
|
14131
|
+
if (group.recountTimer) clearTimeout(group.recountTimer);
|
|
14021
14132
|
this.refetchQueue.delete(group);
|
|
14022
14133
|
this.groups.delete(group.key);
|
|
14023
14134
|
const [index, address] = group.type === "collection" ? [this.collectionGroupsByPath, group.path] : [this.singleGroupsByAddress, `${group.path}\u0000${group.id}`];
|
|
@@ -14091,7 +14202,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14091
14202
|
this.refetchQueue.delete(group);
|
|
14092
14203
|
if (this.groups.get(group.key) !== group) continue;
|
|
14093
14204
|
this.refetchesRunning++;
|
|
14094
|
-
this.refetchGroup(group).finally(() => {
|
|
14205
|
+
this.runInScope(() => this.refetchGroup(group)).finally(() => {
|
|
14095
14206
|
this.refetchesRunning--;
|
|
14096
14207
|
this.drainRefetchQueue();
|
|
14097
14208
|
});
|
|
@@ -14865,6 +14976,17 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14865
14976
|
});
|
|
14866
14977
|
}
|
|
14867
14978
|
/**
|
|
14979
|
+
* Refetch `group` — and so count it again — at `at`, when the total its
|
|
14980
|
+
* last frame reused expires. One pending at a time per group.
|
|
14981
|
+
*/
|
|
14982
|
+
scheduleRecount(group, at) {
|
|
14983
|
+
if (group.recountTimer) return;
|
|
14984
|
+
group.recountTimer = setTimeout(() => {
|
|
14985
|
+
group.recountTimer = void 0;
|
|
14986
|
+
if (this.groups.get(group.key) === group) this.scheduleGroupRefetch(group);
|
|
14987
|
+
}, Math.max(0, at - Date.now()) + 1);
|
|
14988
|
+
}
|
|
14989
|
+
/**
|
|
14868
14990
|
* The `meta` a `collection_update` frame carries beside its rows, counted
|
|
14869
14991
|
* through `service` — the subscriber's own transaction, read only, with
|
|
14870
14992
|
* its auth context and `beforeQuery` narrowing already bound.
|
|
@@ -14893,6 +15015,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
14893
15015
|
else if (reusable && this.recountIntervalMs !== void 0 && reusable.total >= this.recountMinRows && Date.now() - reusable.at < this.recountIntervalMs) {
|
|
14894
15016
|
total = reusable.total;
|
|
14895
15017
|
totalAsOf = new Date(reusable.at).toISOString();
|
|
15018
|
+
if (group) this.scheduleRecount(group, reusable.at + this.recountIntervalMs);
|
|
14896
15019
|
} else {
|
|
14897
15020
|
try {
|
|
14898
15021
|
total = await service.count(notifyPath, {
|
|
@@ -15471,7 +15594,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15471
15594
|
* what it was before this existed, whereas refusing to boot takes the whole
|
|
15472
15595
|
* backend down for it.
|
|
15473
15596
|
*/
|
|
15474
|
-
async configureChannelBus(bus) {
|
|
15597
|
+
async configureChannelBus(bus, options = {}) {
|
|
15475
15598
|
if (bus.kind === "memory") {
|
|
15476
15599
|
this.bus = bus;
|
|
15477
15600
|
return;
|
|
@@ -15487,7 +15610,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15487
15610
|
this.bus = bus;
|
|
15488
15611
|
try {
|
|
15489
15612
|
const store = new ChannelPresenceStore(this.db, this.instanceId);
|
|
15490
|
-
await store.ensureTables();
|
|
15613
|
+
if (!await store.ensureTables({ provision: options.provision }) && options.provision === false) throw new Error("rebase.channel_presence does not exist, and this backend changes nothing in its database. A provisioning run with the same channel bus configured creates it.");
|
|
15491
15614
|
this.presenceStore = store;
|
|
15492
15615
|
this.ensurePresenceSweep();
|
|
15493
15616
|
} catch (error) {
|
|
@@ -15818,6 +15941,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15818
15941
|
for (const group of this.groups.values()) {
|
|
15819
15942
|
if (group.timer) clearTimeout(group.timer);
|
|
15820
15943
|
group.timer = void 0;
|
|
15944
|
+
if (group.recountTimer) clearTimeout(group.recountTimer);
|
|
15945
|
+
group.recountTimer = void 0;
|
|
15821
15946
|
}
|
|
15822
15947
|
this.unscheduledListGroups.clear();
|
|
15823
15948
|
this.listPathChangedAt.clear();
|
|
@@ -15877,6 +16002,10 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
15877
16002
|
isCdcActive() {
|
|
15878
16003
|
return this.cdcActive && this.cdcListener?.connected === true;
|
|
15879
16004
|
}
|
|
16005
|
+
/** See `RealtimeProvider.setScopeRunner`. */
|
|
16006
|
+
setScopeRunner(run) {
|
|
16007
|
+
this.runInScope = run;
|
|
16008
|
+
}
|
|
15880
16009
|
/** The LISTEN connections this service depends on — see `RealtimeProvider.health`. */
|
|
15881
16010
|
health() {
|
|
15882
16011
|
const listeners = [];
|
|
@@ -16411,7 +16540,7 @@ function columnNames(table) {
|
|
|
16411
16540
|
}
|
|
16412
16541
|
return names;
|
|
16413
16542
|
}
|
|
16414
|
-
var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
|
|
16543
|
+
var quote$1 = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
|
|
16415
16544
|
/** `on.from` / `on.to` accept a single column or a composite tuple. */
|
|
16416
16545
|
var asColumns = (value) => Array.isArray(value) ? value : [value];
|
|
16417
16546
|
/**
|
|
@@ -16433,7 +16562,7 @@ function compositeKeyDefect(relation, source, target, registry) {
|
|
|
16433
16562
|
const keys = getPrimaryKeys(keyed, registry);
|
|
16434
16563
|
if (keys.length < 2) return void 0;
|
|
16435
16564
|
return {
|
|
16436
|
-
problem: `${linkColumn} is one column, and \`${keyed.slug}\` is keyed on ${quote(keys.map((key) => key.fieldName))} together — one column cannot reference a composite key`,
|
|
16565
|
+
problem: `${linkColumn} is one column, and \`${keyed.slug}\` is keyed on ${quote$1(keys.map((key) => key.fieldName))} together — one column cannot reference a composite key`,
|
|
16437
16566
|
fix
|
|
16438
16567
|
};
|
|
16439
16568
|
};
|
|
@@ -16562,7 +16691,7 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16562
16691
|
} : {
|
|
16563
16692
|
...at,
|
|
16564
16693
|
problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
|
|
16565
|
-
fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
|
|
16694
|
+
fix: `add the column, or set \`localKey\` to one of: ${quote$1(sourceColumns)}`
|
|
16566
16695
|
});
|
|
16567
16696
|
}
|
|
16568
16697
|
break;
|
|
@@ -16576,13 +16705,13 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16576
16705
|
} : {
|
|
16577
16706
|
...at,
|
|
16578
16707
|
problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
|
|
16579
|
-
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
|
|
16708
|
+
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote$1(targetColumns)}`
|
|
16580
16709
|
});
|
|
16581
16710
|
}
|
|
16582
16711
|
if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
|
|
16583
16712
|
...at,
|
|
16584
16713
|
problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
|
|
16585
|
-
fix: targetColumns.has(relation.sourceKey) ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names the column on this collection that the target's foreign key points at, so it must be one of: ${quote(sourceColumns)}` : `add the column, or set \`sourceKey\` to one of: ${quote(sourceColumns)}`
|
|
16714
|
+
fix: targetColumns.has(relation.sourceKey) ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names the column on this collection that the target's foreign key points at, so it must be one of: ${quote$1(sourceColumns)}` : `add the column, or set \`sourceKey\` to one of: ${quote$1(sourceColumns)}`
|
|
16586
16715
|
});
|
|
16587
16716
|
break;
|
|
16588
16717
|
case "manyToMany": {
|
|
@@ -16609,7 +16738,7 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16609
16738
|
} : {
|
|
16610
16739
|
...at,
|
|
16611
16740
|
problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
|
|
16612
|
-
fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
|
|
16741
|
+
fix: `set \`through.${label}\` to one of: ${quote$1(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
|
|
16613
16742
|
});
|
|
16614
16743
|
}
|
|
16615
16744
|
break;
|
|
@@ -16641,12 +16770,12 @@ function findRelationDefects(collections, registry, given) {
|
|
|
16641
16770
|
for (const column of asColumns(step.on.from)) if (!prevColumns.has(column)) defects.push({
|
|
16642
16771
|
...at,
|
|
16643
16772
|
problem: `step ${i + 1} joins \`${prevName}.${column}\` → \`${step.table}\`, but \`${column}\` is not a column on \`${prevName}\``,
|
|
16644
|
-
fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote(prevColumns)}`
|
|
16773
|
+
fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote$1(prevColumns)}`
|
|
16645
16774
|
});
|
|
16646
16775
|
for (const column of asColumns(step.on.to)) if (!stepColumns.has(column)) defects.push({
|
|
16647
16776
|
...at,
|
|
16648
16777
|
problem: `step ${i + 1} joins into \`${step.table}.${column}\`, but \`${column}\` is not a column on \`${step.table}\``,
|
|
16649
|
-
fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote(stepColumns)}`
|
|
16778
|
+
fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote$1(stepColumns)}`
|
|
16650
16779
|
});
|
|
16651
16780
|
if (asColumns(step.on.from).length !== asColumns(step.on.to).length) defects.push({
|
|
16652
16781
|
...at,
|
|
@@ -17138,14 +17267,26 @@ var UserService = class {
|
|
|
17138
17267
|
return result.rows[0].roles ?? [];
|
|
17139
17268
|
}
|
|
17140
17269
|
/**
|
|
17141
|
-
* Set roles for a user (replaces existing roles)
|
|
17270
|
+
* Set roles for a user (replaces existing roles).
|
|
17271
|
+
*
|
|
17272
|
+
* Each role is its own bound parameter. The array used to be spelled as a
|
|
17273
|
+
* literal, `{${roles.join(",")}}`, so the database parsed what the caller
|
|
17274
|
+
* had only checked as strings: `"support,admin"` was stored as two roles,
|
|
17275
|
+
* one of them `admin`, and `"NULL"` as a SQL NULL. A name that would be read
|
|
17276
|
+
* back as other roles is refused (`roleNameProblem`) — RLS splits the
|
|
17277
|
+
* comma-joined `app.user_roles` the same way.
|
|
17142
17278
|
*/
|
|
17143
17279
|
async setUserRoles(uid, roleIds) {
|
|
17280
|
+
const problems = roleIds.flatMap((role) => {
|
|
17281
|
+
const problem = roleNameProblem(role);
|
|
17282
|
+
return problem ? [`${JSON.stringify(role)} ${problem}`] : [];
|
|
17283
|
+
});
|
|
17284
|
+
if (problems.length > 0) throw ApiError.badRequest(`Not valid role names: ${problems.join("; ")}`, "INVALID_ROLE");
|
|
17144
17285
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
17145
|
-
const rolesArray = `{${
|
|
17286
|
+
const rolesArray = roleIds.length === 0 ? sql`'{}'::text[]` : sql`ARRAY[${sql.join(roleIds.map((role) => sql`${role}`), sql`, `)}]::text[]`;
|
|
17146
17287
|
await this.withServerContext(async (db) => db.execute(sql`
|
|
17147
17288
|
UPDATE ${sql.raw(usersTableName)}
|
|
17148
|
-
SET roles = ${rolesArray}
|
|
17289
|
+
SET roles = ${rolesArray}, updated_at = NOW()
|
|
17149
17290
|
WHERE id = ${uid}
|
|
17150
17291
|
`));
|
|
17151
17292
|
}
|
|
@@ -18167,6 +18308,59 @@ var MfaService = class {
|
|
|
18167
18308
|
}
|
|
18168
18309
|
};
|
|
18169
18310
|
//#endregion
|
|
18311
|
+
//#region src/auth/verify-auth-tables.ts
|
|
18312
|
+
/**
|
|
18313
|
+
* Whether the auth tables a backend reads are there, on a backend that may not
|
|
18314
|
+
* create them.
|
|
18315
|
+
*
|
|
18316
|
+
* A driver opened without provisioning skips `ensureAuthTablesExist` — the
|
|
18317
|
+
* tables are another process's to make — and before this nothing took its
|
|
18318
|
+
* place. A database that lacked `rebase.refresh_tokens`, or had a users table
|
|
18319
|
+
* from a release before `disabled_at`, booted green and then failed every
|
|
18320
|
+
* sign-in with a 500 whose cause was one missing relation. Auth cannot run on
|
|
18321
|
+
* part of its schema, so this is a refusal at boot, naming each table.
|
|
18322
|
+
*
|
|
18323
|
+
* Asked by reading, not by the catalogue: a `SELECT` of every column the auth
|
|
18324
|
+
* services name, `WHERE false`. That answers "missing", "out of date" and "this
|
|
18325
|
+
* role may not read it" in one statement per table, with the database's own
|
|
18326
|
+
* words for whichever it is.
|
|
18327
|
+
*/
|
|
18328
|
+
var quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
18329
|
+
/** What a Drizzle table definition says auth will query. */
|
|
18330
|
+
function expectationOf(table) {
|
|
18331
|
+
const config = getTableConfig(table);
|
|
18332
|
+
return {
|
|
18333
|
+
table: config.schema ? `${quote(config.schema)}.${quote(config.name)}` : quote(config.name),
|
|
18334
|
+
columns: Object.values(getTableColumns(table)).map((column) => column.name)
|
|
18335
|
+
};
|
|
18336
|
+
}
|
|
18337
|
+
/**
|
|
18338
|
+
* The users table, which is the project's own when it declares an auth
|
|
18339
|
+
* collection: only the columns the auth bootstrap guarantees on it are
|
|
18340
|
+
* expected, never the collection's other fields — those are the collection
|
|
18341
|
+
* schema's business, checked where collections are.
|
|
18342
|
+
*/
|
|
18343
|
+
function usersExpectation(qualifiedTable) {
|
|
18344
|
+
return {
|
|
18345
|
+
table: qualifiedTable,
|
|
18346
|
+
columns: ["id", ...AUTH_USERS_COLUMNS.map((spec) => spec.column)]
|
|
18347
|
+
};
|
|
18348
|
+
}
|
|
18349
|
+
/** Each table that cannot be read as expected, with the database's reason. */
|
|
18350
|
+
async function findUnusableAuthTables(db, expectations) {
|
|
18351
|
+
const problems = [];
|
|
18352
|
+
for (const { table, columns } of expectations) try {
|
|
18353
|
+
await db.execute(sql.raw(`SELECT ${columns.map(quote).join(", ")} FROM ${table} WHERE false`));
|
|
18354
|
+
} catch (err) {
|
|
18355
|
+
problems.push(`${table}: ${deepestErrorMessage(err)}`);
|
|
18356
|
+
}
|
|
18357
|
+
return problems;
|
|
18358
|
+
}
|
|
18359
|
+
/** The refusal, phrased as what to run. */
|
|
18360
|
+
function unusableAuthTablesError(problems) {
|
|
18361
|
+
return /* @__PURE__ */ new Error("Auth cannot run on this database, and this backend changes nothing in it (provisioning is off):\n" + problems.map((problem) => ` - ${problem}`).join("\n") + "\nCreate them with a provisioning run against this database (REBASE_PROVISION_ONLY=true, without REBASE_PROVISION=false), or apply the SQL such a run recorded (REBASE_PROVISION_SQL_OUT).");
|
|
18362
|
+
}
|
|
18363
|
+
//#endregion
|
|
18170
18364
|
//#region src/history/ensure-history-table.ts
|
|
18171
18365
|
/** How a history entry gets written from inside the write it records. */
|
|
18172
18366
|
var RECORD_HISTORY_FUNCTION = "rebase.record_history";
|
|
@@ -18570,6 +18764,28 @@ var HistoryService = class {
|
|
|
18570
18764
|
`)) ?? null;
|
|
18571
18765
|
}
|
|
18572
18766
|
/**
|
|
18767
|
+
* Every entry's `values` and `previous_values`, of every row, a page at a
|
|
18768
|
+
* time in id order — for a whole-store sweep. `rebase storage orphans`
|
|
18769
|
+
* reads it because a history revert restores the values an entry holds,
|
|
18770
|
+
* so a file only an entry names is still in use.
|
|
18771
|
+
*/
|
|
18772
|
+
async *scanStoredValues(pageSize = 1e3) {
|
|
18773
|
+
const limit = Math.max(1, pageSize);
|
|
18774
|
+
let after;
|
|
18775
|
+
for (;;) {
|
|
18776
|
+
const rows = sqlRows(await this.db.execute(sql`
|
|
18777
|
+
SELECT id, "values", previous_values
|
|
18778
|
+
FROM rebase.entity_history
|
|
18779
|
+
${after === void 0 ? sql`` : sql`WHERE id > ${after}`}
|
|
18780
|
+
ORDER BY id
|
|
18781
|
+
LIMIT ${limit}
|
|
18782
|
+
`));
|
|
18783
|
+
if (rows.length > 0) yield rows.map((row) => [row.values, row.previous_values]);
|
|
18784
|
+
if (rows.length < limit) return;
|
|
18785
|
+
after = rows[rows.length - 1].id;
|
|
18786
|
+
}
|
|
18787
|
+
}
|
|
18788
|
+
/**
|
|
18573
18789
|
* Each entry's `format`. A table without the column holds only entries
|
|
18574
18790
|
* that store the whole previous row, which is what `1` says.
|
|
18575
18791
|
*/
|
|
@@ -19206,6 +19422,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
19206
19422
|
driver.listenConnectionString = directUrl;
|
|
19207
19423
|
realtimeService.setDataDriver(driver);
|
|
19208
19424
|
let rlsEnforcement;
|
|
19425
|
+
let exposedInternalTables;
|
|
19209
19426
|
{
|
|
19210
19427
|
const runSql = async (text) => {
|
|
19211
19428
|
return (await schemaAwareDb.execute(sql.raw(text))).rows ?? [];
|
|
@@ -19223,6 +19440,10 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
19223
19440
|
if (!provision) {
|
|
19224
19441
|
const roleExists = (await runSql(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE$1}'`)).length > 0;
|
|
19225
19442
|
switched = roleExists || bypassesNow;
|
|
19443
|
+
if (roleExists) {
|
|
19444
|
+
exposedInternalTables = await findExposedInternalTables(runSql, managedSchemas);
|
|
19445
|
+
if (exposedInternalTables.length > 0) logger.warn(`⚠️ RLS enforcement: the request role "${REBASE_USER_ROLE$1}" can reach framework tables no user should: ${exposedInternalTables.join(", ")}. A provisioning boot revokes this; this driver was opened without provisioning, so nothing does.`);
|
|
19446
|
+
}
|
|
19226
19447
|
if (!roleExists && bypassesNow) logger.warn(`⚠️ RLS enforcement: connection role "${posture.role}" bypasses row-level security and the restricted role "${REBASE_USER_ROLE$1}" does not exist yet — this driver was opened without provisioning, so it was not created. Requests made as a user will fail until a server boot (or \`rebase db push\`) creates it; requests as the connection itself are unaffected.`);
|
|
19227
19448
|
} else try {
|
|
19228
19449
|
await ensureAppRole(runSql, managedSchemas);
|
|
@@ -19262,7 +19483,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
19262
19483
|
if (!realtimeOff && (isChannelBusInstance(busSetting) || busSetting.type !== "memory")) await realtimeService.configureChannelBus(createChannelBus(busSetting, {
|
|
19263
19484
|
db: schemaAwareDb,
|
|
19264
19485
|
directUrl
|
|
19265
|
-
}));
|
|
19486
|
+
}), { provision });
|
|
19266
19487
|
} catch (err) {
|
|
19267
19488
|
logger.warn("⚠️ [ChannelBus] Could not configure the channel bus — channel broadcast and presence stay per-instance", { error: err });
|
|
19268
19489
|
}
|
|
@@ -19397,6 +19618,7 @@ async function openPostgresDriver(pgConfig, config) {
|
|
|
19397
19618
|
poolManager,
|
|
19398
19619
|
provisioned: provision,
|
|
19399
19620
|
rlsEnforcement,
|
|
19621
|
+
exposedInternalTables,
|
|
19400
19622
|
provisionCdcForTables
|
|
19401
19623
|
};
|
|
19402
19624
|
return {
|
|
@@ -19467,7 +19689,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19467
19689
|
const openIndexBuildSession = (driverResult) => async () => {
|
|
19468
19690
|
const db = requireProvisioningDb(driverResult);
|
|
19469
19691
|
const pool = "$client" in db ? db.$client : void 0;
|
|
19470
|
-
const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-
|
|
19692
|
+
const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
|
|
19471
19693
|
if (isPgPool(pool)) return poolIndexBuildSession(pool)();
|
|
19472
19694
|
const queryable = provisioningQueryable(driverResult);
|
|
19473
19695
|
return {
|
|
@@ -19493,7 +19715,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19493
19715
|
*/
|
|
19494
19716
|
const provisionRlsRuntime = async (driverResult) => {
|
|
19495
19717
|
const db = requireProvisioningDb(driverResult);
|
|
19496
|
-
const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./
|
|
19718
|
+
const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./migrate-baseline-B-2Ef4Mv.js").then((n) => n.w);
|
|
19497
19719
|
const run = async (text) => sqlRows(await db.execute(sql.raw(text)));
|
|
19498
19720
|
if (await rlsRuntimeIsCurrent(run).catch(() => false)) return;
|
|
19499
19721
|
await db.transaction(async (tx) => {
|
|
@@ -19526,6 +19748,9 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19526
19748
|
const init = config;
|
|
19527
19749
|
const opened = await openPostgresDriver(effectiveConfig(init.dataSourceKey), init);
|
|
19528
19750
|
if (opened.internals.rlsEnforcement) logger.info(opened.internals.rlsEnforcement);
|
|
19751
|
+
const exposed = opened.internals.exposedInternalTables ?? [];
|
|
19752
|
+
if (exposed.length > 0) throw new Error(`The request role "${REBASE_USER_ROLE$1}" can read or write framework tables that hold sessions, key hashes and job payloads: ${exposed.join(", ")}.\nA provisioning boot revokes this, and this backend changes nothing in its database (provisioning is off). Revoke it as part of your change process:
|
|
19753
|
+
` + exposed.map((table) => ` REVOKE ALL ON ${table.split(".").map((part) => `"${part}"`).join(".")} FROM ${REBASE_USER_ROLE$1};`).join("\n"));
|
|
19529
19754
|
return opened;
|
|
19530
19755
|
},
|
|
19531
19756
|
async initializeAuth(config, driverResult) {
|
|
@@ -19536,7 +19761,23 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19536
19761
|
const registry = internals.registry;
|
|
19537
19762
|
const authCollection = authConfig.collection;
|
|
19538
19763
|
if (internals.provisioned !== false) await ensureAuthTablesExist(db, authCollection);
|
|
19539
|
-
else
|
|
19764
|
+
else {
|
|
19765
|
+
await assertAuthSchemaCompatible(db, resolveAuthSchema(authCollection));
|
|
19766
|
+
const usersSchema = authCollection ? "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "public" : "rebase";
|
|
19767
|
+
const usersTableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : "users";
|
|
19768
|
+
const expected = createAuthSchema(usersSchema === "public" ? "rebase" : usersSchema);
|
|
19769
|
+
const problems = await findUnusableAuthTables(db, [usersExpectation(usersSchema === "public" ? `"${usersTableName}"` : `"${usersSchema}"."${usersTableName}"`), ...[
|
|
19770
|
+
expected.refreshTokens,
|
|
19771
|
+
expected.passwordResetTokens,
|
|
19772
|
+
expected.appConfig,
|
|
19773
|
+
expected.userIdentities,
|
|
19774
|
+
expected.mfaFactors,
|
|
19775
|
+
expected.mfaChallenges,
|
|
19776
|
+
expected.recoveryCodes,
|
|
19777
|
+
expected.magicLinkTokens
|
|
19778
|
+
].map(expectationOf)]);
|
|
19779
|
+
if (problems.length > 0) throw unusableAuthTablesError(problems);
|
|
19780
|
+
}
|
|
19540
19781
|
if (authCollection && internals.provisionCdcForTables) {
|
|
19541
19782
|
const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
|
|
19542
19783
|
const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
|
|
@@ -19587,11 +19828,11 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19587
19828
|
unavailable: historyUnavailableReason(state, { requireTransaction: false })
|
|
19588
19829
|
}) };
|
|
19589
19830
|
}
|
|
19590
|
-
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-
|
|
19831
|
+
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-EsgqDkx0.js").then((n) => n.i);
|
|
19591
19832
|
const historyTable = await ensureHistoryTableExists(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
|
|
19592
19833
|
const historyIndexes = await ensureHistoryIndexes(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
|
|
19593
19834
|
if (historyIndexes.length > 0) {
|
|
19594
|
-
const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-
|
|
19835
|
+
const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
|
|
19595
19836
|
const indexOptions = {
|
|
19596
19837
|
log: (message) => logger.info(`[history] ${message}`),
|
|
19597
19838
|
lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS
|
|
@@ -19647,9 +19888,9 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19647
19888
|
* proved it can bootstrap with.
|
|
19648
19889
|
*/
|
|
19649
19890
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
19650
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
19891
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-CFIRq8ni.js").then((n) => n.n);
|
|
19651
19892
|
const queryable = provisioningQueryable(driverResult);
|
|
19652
|
-
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-
|
|
19893
|
+
const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-EsgqDkx0.js").then((n) => n.i);
|
|
19653
19894
|
const provisionOnly = ["1", "true"].includes((process.env.REBASE_PROVISION_ONLY ?? "").trim().toLowerCase());
|
|
19654
19895
|
const plan = await ensureCollectionTables(queryable, collections, log, {
|
|
19655
19896
|
lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS,
|
|
@@ -19661,7 +19902,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19661
19902
|
pendingIndexBuild?.cancel();
|
|
19662
19903
|
pendingIndexBuild = void 0;
|
|
19663
19904
|
if (!provisionOnly && plan.deferredIndexes.length > 0) {
|
|
19664
|
-
const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-
|
|
19905
|
+
const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
|
|
19665
19906
|
logger.info(`[schema] ${plan.deferredIndexes.length} index(es) are missing, or were left INVALID, on tables that already exist. They are built in the background once the server is serving, one at a time, without blocking reads or writes.`);
|
|
19666
19907
|
pendingIndexBuild = scheduleDeferredIndexBuild(openIndexBuildSession(driverResult), plan.deferredIndexes, {
|
|
19667
19908
|
log: (message) => logger.info(`[schema] ${message}`),
|
|
@@ -19701,7 +19942,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19701
19942
|
await provisionRlsRuntime(driverResult);
|
|
19702
19943
|
},
|
|
19703
19944
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
19704
|
-
const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-
|
|
19945
|
+
const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-DLWgabQ2.js");
|
|
19705
19946
|
const queryable = provisioningQueryable(driverResult);
|
|
19706
19947
|
const { rows } = await queryable.query("SELECT to_regprocedure('rebase.roles()') IS NOT NULL AS present");
|
|
19707
19948
|
if (!rows[0]?.present) await provisionRlsRuntime(driverResult);
|
|
@@ -19726,7 +19967,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19726
19967
|
for (const u of outcome.unsecured.filter((u) => u.grantWithdrawn)) logger.error(`🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. Its privileges have been revoked from ${REBASE_USER_ROLE$1}, so the table is unreachable rather than unprotected. Reads and writes to that collection will fail until RLS can be enabled.`);
|
|
19727
19968
|
if (unrevoked.length > 0) throw new Error("Refusing to start: row-level security could not be enabled on " + unrevoked.map((u) => `"${u.table}" (${u.error})`).join(", ") + `, and the privileges granted to ${REBASE_USER_ROLE$1} could not be revoked either. The table would be served with no row filtering. Fix the database permissions (the connection role must own these tables, or be able to ALTER them) and boot again.`);
|
|
19728
19969
|
try {
|
|
19729
|
-
const { dropLegacyAuthSchema } = await import("./
|
|
19970
|
+
const { dropLegacyAuthSchema } = await import("./migrate-baseline-B-2Ef4Mv.js").then((n) => n.w);
|
|
19730
19971
|
await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
|
|
19731
19972
|
info: (m) => logger.info(m),
|
|
19732
19973
|
warn: (m) => logger.warn(m)
|
|
@@ -19746,14 +19987,14 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19746
19987
|
async readCollectionsSchemaVersion(driverResult) {
|
|
19747
19988
|
const db = provisioningDb(driverResult);
|
|
19748
19989
|
if (!db) return null;
|
|
19749
|
-
const { readCollectionsSchemaVersion } = await import("./collections-schema-version-
|
|
19990
|
+
const { readCollectionsSchemaVersion } = await import("./collections-schema-version-D7bDI0PJ.js");
|
|
19750
19991
|
return readCollectionsSchemaVersion(db, SCHEMA_META_SCHEMA);
|
|
19751
19992
|
},
|
|
19752
19993
|
/** Record what this process just applied. Only the provisioning process calls this. */
|
|
19753
19994
|
async stampCollectionsSchemaVersion(version, driverResult) {
|
|
19754
19995
|
const db = provisioningDb(driverResult);
|
|
19755
19996
|
if (!db) return;
|
|
19756
|
-
const { stampCollectionsSchemaVersion } = await import("./collections-schema-version-
|
|
19997
|
+
const { stampCollectionsSchemaVersion } = await import("./collections-schema-version-D7bDI0PJ.js");
|
|
19757
19998
|
await stampCollectionsSchemaVersion(db, SCHEMA_META_SCHEMA, version);
|
|
19758
19999
|
},
|
|
19759
20000
|
/**
|
|
@@ -19817,7 +20058,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
19817
20058
|
return driverResult.internals.driver.admin;
|
|
19818
20059
|
},
|
|
19819
20060
|
async initializeWebsockets(server, realtimeService, driver, config, adapter, limits) {
|
|
19820
|
-
const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-
|
|
20061
|
+
const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-CKYc3V4N.js").then((n) => n.c);
|
|
19821
20062
|
if (!isSocketRealtimeService(realtimeService)) throw new Error(`The realtime service handed to the Postgres socket cannot serve it — it has no ${socketRealtimeMissing(realtimeService).join(", ")}. Every socket would fail on its first frame.`);
|
|
19822
20063
|
const realtimeConfig = effectiveConfig().realtime;
|
|
19823
20064
|
const maxSocketsPerUser = realtimeConfig?.maxSocketsPerUser !== void 0 ? parseMaxSocketsPerUser(realtimeConfig.maxSocketsPerUser, "realtime.maxSocketsPerUser") : void 0;
|
|
@@ -19891,6 +20132,7 @@ async function openRebase(options) {
|
|
|
19891
20132
|
const hint = extractPgError(error)?.hint;
|
|
19892
20133
|
throw new Error(`Cannot connect to PostgreSQL at ${parseHostInfo(connectionString ?? "")}: ${deepestErrorMessage(error) ?? "the connection failed"}.${hint ? ` ${hint}` : ""}`, { cause: error });
|
|
19893
20134
|
}
|
|
20135
|
+
const ownKey = options.dataSourceKey ?? DEFAULT_DATA_SOURCE_KEY;
|
|
19894
20136
|
let opened;
|
|
19895
20137
|
try {
|
|
19896
20138
|
opened = await openPostgresDriver({
|
|
@@ -19898,7 +20140,7 @@ async function openRebase(options) {
|
|
|
19898
20140
|
connectionString
|
|
19899
20141
|
}, {
|
|
19900
20142
|
collections: options.collections,
|
|
19901
|
-
dataSourceKey:
|
|
20143
|
+
dataSourceKey: ownKey,
|
|
19902
20144
|
introspectCollections: options.collections.length === 0,
|
|
19903
20145
|
provision: false,
|
|
19904
20146
|
realtime: false,
|
|
@@ -19913,6 +20155,7 @@ async function openRebase(options) {
|
|
|
19913
20155
|
}
|
|
19914
20156
|
const driver = opened.internals.driver;
|
|
19915
20157
|
const registry = opened.internals.registry;
|
|
20158
|
+
if (options.callbacks) registry.setGlobalCallbacks(options.callbacks);
|
|
19916
20159
|
if (opened.internals.rlsEnforcement) logger.debug(opened.internals.rlsEnforcement);
|
|
19917
20160
|
const collections = opened.collections ?? options.collections;
|
|
19918
20161
|
if (collections.some((collection) => collection.history)) try {
|
|
@@ -19937,10 +20180,13 @@ async function openRebase(options) {
|
|
|
19937
20180
|
collections,
|
|
19938
20181
|
async sql(query, sqlOptions) {
|
|
19939
20182
|
if (!isSQLAdmin(admin)) throw new Error("This driver cannot run raw SQL.");
|
|
20183
|
+
if (sqlOptions?.database !== void 0) {
|
|
20184
|
+
const asked = resourceKeyOf(sqlOptions.database);
|
|
20185
|
+
if (asked !== ownKey) throw new Error(`openRebase opened the "${ownKey}" data source only, so it cannot run SQL on "${asked}". Open that one with its own openRebase({ databaseUrl, dataSourceKey: "${asked}" }).`);
|
|
20186
|
+
}
|
|
19940
20187
|
return admin.executeSql(query, sqlOptions && {
|
|
19941
20188
|
role: sqlOptions.role,
|
|
19942
|
-
params: sqlOptions.params
|
|
19943
|
-
...sqlOptions.database !== void 0 ? { database: resourceKeyOf(sqlOptions.database) } : {}
|
|
20189
|
+
params: sqlOptions.params
|
|
19944
20190
|
});
|
|
19945
20191
|
},
|
|
19946
20192
|
async withAuth(user) {
|
|
@@ -19956,4 +20202,4 @@ async function openRebase(options) {
|
|
|
19956
20202
|
//#endregion
|
|
19957
20203
|
export { AuthenticatedPostgresBackendDriver as $, budgetSendBuffers as A, mfaFactors as B, PG_NOTIFY_MAX_PAYLOAD_BYTES as C, MemoryChannelBus as D, parseChannelBusPayload as E, createAuthSchema as F, recoveryCodesRelations as G, passwordResetTokens as H, magicLinkTokens as I, userIdentities as J, refreshTokens as K, magicLinkTokensRelations as L, terminateIfBacklogged as M, assertReadRequestReadable as N, frameByteLength as O, appConfig as P, usersSchema as Q, mfaChallenges as R, DEFAULT_BATCH_WINDOW_MS as S, parseChannelBusFrame as T, passwordResetTokensRelations as U, mfaFactorsRelations as V, recoveryCodes as W, users as X, userIdentitiesRelations as Y, usersRelations as Z, parseRealtimeTuning as _, listedColumnKeys as _t, openPostgresDriver as a, isRoleSwitchingOptedOut as at, resolveChannelBusSetting as b, resolveSourceUrls as c, SqlResultTooLargeError as ct, DEFAULT_RECOUNT_MIN_ROWS as d, DrizzleConditionBuilder as dt, CONNECTION_OWNER as et, PostgresRealtimeProvider as f, PostgresConditionBuilder as ft, parseMaxSubscriptionsPerSocket as g, joinColumn as gt, parseMaxSocketsPerUser as h, getUnknownFilterFieldsMode as ht, isScaffoldedLocalDatabase as i, effectiveSqlRole as it, reapSilentSockets as j, isChannelBusInstance$1 as k, PostgresCollectionRegistry as l, isNestedPath as lt, packEntityChanges as m, escapeLikePattern as mt, createPostgresBootstrapper as n, RoleSwitchUnavailableError as nt, postgresDriverConfigFromEnv as o, sqlRoleRanAs as ot, RealtimeService as p, configureUnknownFilterFields as pt, refreshTokensRelations as q, describeSchemaDriftCause as r, branchParentDatabase as rt, resolveDriftCheckName as s, SQL_RESULT_BYTE_BUDGET as st, openRebase as t, PostgresBackendDriver as tt, DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET as u, resolveNestedPath as ut, refetchConcurrencyFor as v, PostgresChannelBus as w, CHANNEL_BUS_NOTIFY_CHANNEL as x, createChannelBus as y, mfaChallengesRelations as z };
|
|
19958
20204
|
|
|
19959
|
-
//# sourceMappingURL=open-rebase-
|
|
20205
|
+
//# sourceMappingURL=open-rebase-C8jQ6s5a.js.map
|