@rebasepro/server-postgres 0.21.1-canary.g8c5a265 → 0.21.1
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-W3DMfcZZ.js → BranchService-CucnFcSE.js} +2 -2
- package/dist/{BranchService-W3DMfcZZ.js.map → BranchService-CucnFcSE.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +23 -6
- package/dist/{cli-errors-C3g_kHBw.js → cli-errors-Dka89exj.js} +19 -2
- package/dist/cli-errors-Dka89exj.js.map +1 -0
- package/dist/cli.js +14 -7
- package/dist/cli.js.map +1 -1
- package/dist/{doctor-ByFWL_ET.js → doctor-Gzzd8wTq.js} +4 -4
- package/dist/{doctor-ByFWL_ET.js.map → doctor-Gzzd8wTq.js.map} +1 -1
- package/dist/{ensure-collection-policies-CQGHln-y.js → ensure-collection-policies-B4IDFtQ-.js} +2 -2
- package/dist/{ensure-collection-policies-CQGHln-y.js.map → ensure-collection-policies-B4IDFtQ-.js.map} +1 -1
- package/dist/{ensure-collection-tables-Bkvehxdm.js → ensure-collection-tables-Cr2ye5JC.js} +2 -2
- package/dist/{ensure-collection-tables-Bkvehxdm.js.map → ensure-collection-tables-Cr2ye5JC.js.map} +1 -1
- package/dist/{generate-drizzle-schema-vSK-VdYT.js → generate-drizzle-schema-D2MDdJt-.js} +2 -2
- package/dist/{generate-drizzle-schema-vSK-VdYT.js.map → generate-drizzle-schema-D2MDdJt-.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-Dfyf_MRu.js → generate-drizzle-schema-logic-DFa9qy9u.js} +4 -2
- package/dist/{generate-drizzle-schema-logic-Dfyf_MRu.js.map → generate-drizzle-schema-logic-DFa9qy9u.js.map} +1 -1
- package/dist/{generated-schema-staleness-DRQe2BpC.js → generated-schema-staleness-Di9c4ZxN.js} +9 -5
- package/dist/{generated-schema-staleness-DRQe2BpC.js.map → generated-schema-staleness-Di9c4ZxN.js.map} +1 -1
- package/dist/index.es.js +216 -145
- package/dist/index.es.js.map +1 -1
- package/dist/schema/doctor-cli.js +2 -2
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generated-schema-staleness.d.ts +22 -0
- package/dist/services/realtimeService.d.ts +75 -12
- package/package.json +7 -7
- package/dist/cli-errors-C3g_kHBw.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -3,20 +3,20 @@ __createRequire(import.meta.url);
|
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
4
|
import { a as createDirectDatabaseConnection, c as guardPoolAgainstDirtyRelease, l as pinSearchPath, o as createPostgresDatabaseConnection, r as cappedPoolMax, s as createReadReplicaConnection, t as DatabasePoolManager, u as poolMaxCeiling } from "./databasePoolManager-Bj5FbeAs.js";
|
|
5
5
|
import { f as hiddenColumnsOption, l as SEARCH_UNACCENT_FN, m as searchColumnNames, o as sharedRelationName, u as buildSearchColumnSpec, x as visibleColumnProjection } from "./plan-schema-Hgl62S-w.js";
|
|
6
|
-
import { d as extractPgError, f as isRoleSwitchingPermissionError, g as sanitizeErrorForClient, h as reachedDatabase, i as diagnoseDbError, l as classifyConnectFailure, m as pgErrorToFriendlyMessage, n as deepestErrorMessage, o as isEconnrefused, p as isRowLevelSecurityDenial, s as parseHostInfo, u as extractCauseMessage } from "./cli-errors-
|
|
7
|
-
import { r as BranchingUnsupportedError, t as BranchService } from "./BranchService-
|
|
6
|
+
import { d as extractPgError, f as isRoleSwitchingPermissionError, g as sanitizeErrorForClient, h as reachedDatabase, i as diagnoseDbError, l as classifyConnectFailure, m as pgErrorToFriendlyMessage, n as deepestErrorMessage, o as isEconnrefused, p as isRowLevelSecurityDenial, s as parseHostInfo, u as extractCauseMessage } from "./cli-errors-Dka89exj.js";
|
|
7
|
+
import { r as BranchingUnsupportedError, t as BranchService } from "./BranchService-CucnFcSE.js";
|
|
8
8
|
import { c as warnOnLegacyRlsFunctions, i as ensureAppRole, l as warnOnRoleSchemaCollision, n as applyAuthContext, o as validatePolicyPgRoles, r as detectConnectionPosture, s as warnOnAnonymousGrants, t as REBASE_USER_ROLE } from "./rls-enforcement-C6Xk0lA6.js";
|
|
9
9
|
import { l as resolveColumnName } from "./column-plan-helpers-CpILzHJS.js";
|
|
10
10
|
import { i as isAuthCollection, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-D2LBFrMH.js";
|
|
11
11
|
import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "./generate-postgres-ddl-logic-CR2xcS7e.js";
|
|
12
|
-
import { t as generateSchema } from "./generate-drizzle-schema-logic-
|
|
13
|
-
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-
|
|
14
|
-
import { t as main } from "./generate-drizzle-schema-
|
|
12
|
+
import { t as generateSchema } from "./generate-drizzle-schema-logic-DFa9qy9u.js";
|
|
13
|
+
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-Cr2ye5JC.js";
|
|
14
|
+
import { t as main } from "./generate-drizzle-schema-D2MDdJt-.js";
|
|
15
15
|
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 getServerVersionMajor, d as pruneBackups, f as resolvePgBinary, g as selectBackupsToPrune, h as validateDump, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as listBackups, m as uploadBackup, n as applyGlobals, o as discardPartialDump, p as restoreDump, s as ensureDatabaseExists, t as BackupToolError, u as preflight, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-BNLwvxuy.js";
|
|
16
16
|
import { i as resolveAuthSchema, r as probeAuthSchema, t as ensureAuthTablesExist } from "./ensure-tables-BnEvEJPr.js";
|
|
17
17
|
import { c as identifyJoinTables, d as singularize, h as humanize, i as buildTablesMap, m as mapPgType, r as buildEnumMap, s as getIconForTable } from "./introspect-db-logic-C6LQdTxj.js";
|
|
18
18
|
import { Client } from "pg";
|
|
19
|
-
import { ApiError, assertFieldOpsValid, assertWriteRequestValid, createDdlBootstrapper, createEmailService, extractUserFromToken, logger, rawQueryLoggingEnabled, resolveBatchRefs, resolveRequireAuth, safeCompare, splitFieldOps } from "@rebasepro/server";
|
|
19
|
+
import { ApiError, assertFieldOpsValid, assertWriteRequestValid, createDdlBootstrapper, createEmailService, declaredErrorAnswer, extractUserFromToken, logger, rawQueryLoggingEnabled, resolveBatchRefs, resolveRequireAuth, safeCompare, splitFieldOps } from "@rebasepro/server";
|
|
20
20
|
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, isNotNull, isNull, isTable, lt, not, notInArray, or, relations, sql } from "drizzle-orm";
|
|
21
21
|
import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, declaredDatabaseExtensions, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, isSchemaAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit } from "@rebasepro/types";
|
|
22
22
|
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 as REBASE_USER_ROLE$1, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createRelationRef, createRelationRefWithData, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys, getJunctionCollectionConfig, getJunctionConfigForRelation, getTableName as getTableName$1, getTenantConfig, isAddressableId, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeToEntityRelation, parseIdValues, parseOrderBySpecStrict, requireCallbackClient, requireCallbackCollection, resolveCollectionRelations, resolveJunctionSpecs, resolveTenantWrite, revokeInternalTableSql, sqlRows, tenantBypassRoles, toCallbackError, toFilterTuples, updateDateAutoValues, updateUserAutoValues } from "@rebasepro/common";
|
|
@@ -6092,6 +6092,35 @@ function assertActingUserForAutoValues(properties, status, uid, path) {
|
|
|
6092
6092
|
});
|
|
6093
6093
|
}
|
|
6094
6094
|
}
|
|
6095
|
+
/**
|
|
6096
|
+
* A listener's `onError`, in the shape the realtime service calls it with.
|
|
6097
|
+
*
|
|
6098
|
+
* Registered with the subscription so a failed refetch reaches it. Before, only
|
|
6099
|
+
* the initial fetch did: a refetch after a change failed into a log line, and
|
|
6100
|
+
* the listener kept its last rows as if they were current. The error arrives as
|
|
6101
|
+
* thrown, not masked the way a socket frame is: this is trusted server code.
|
|
6102
|
+
*/
|
|
6103
|
+
function fetchErrorListener(onError) {
|
|
6104
|
+
if (!onError) return void 0;
|
|
6105
|
+
return (error) => onError(error instanceof Error ? error : new Error(String(error)));
|
|
6106
|
+
}
|
|
6107
|
+
/**
|
|
6108
|
+
* The principal a user's reads run as, for the transaction a request opens and
|
|
6109
|
+
* for a subscription alike.
|
|
6110
|
+
*
|
|
6111
|
+
* One definition because there were two. The subscription's copy was built by
|
|
6112
|
+
* hand after the fact and left out `isAnonymous`, so a guest's refetches ran as
|
|
6113
|
+
* an account: a policy that excludes guests filtered the first read and not the
|
|
6114
|
+
* ones after a change.
|
|
6115
|
+
*/
|
|
6116
|
+
function authContextOf(user) {
|
|
6117
|
+
return {
|
|
6118
|
+
uid: user?.uid || ANONYMOUS_USER_ID,
|
|
6119
|
+
roles: user?.roles ?? [],
|
|
6120
|
+
isAnonymous: user?.isAnonymous === true,
|
|
6121
|
+
...user?.claims ? { claims: user.claims } : {}
|
|
6122
|
+
};
|
|
6123
|
+
}
|
|
6095
6124
|
var PostgresBackendDriver = class PostgresBackendDriver {
|
|
6096
6125
|
db;
|
|
6097
6126
|
registry;
|
|
@@ -6410,46 +6439,31 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6410
6439
|
}
|
|
6411
6440
|
return rows;
|
|
6412
6441
|
}
|
|
6413
|
-
|
|
6442
|
+
/**
|
|
6443
|
+
* Listen to a collection: the rows now, then again after every change.
|
|
6444
|
+
*
|
|
6445
|
+
* The realtime service runs every read, the first one included, as
|
|
6446
|
+
* `authContext` — which {@link AuthenticatedPostgresBackendDriver} supplies,
|
|
6447
|
+
* and which defaults to this driver's own user (a driver bound to a
|
|
6448
|
+
* request's transaction has one). With neither, the reads run as the
|
|
6449
|
+
* anonymous user, as the refetches always did.
|
|
6450
|
+
*/
|
|
6451
|
+
listenCollection({ path, collection, startAfter, onUpdate, onError, vectorSearch: _vectorSearch, page: _page, withDeleted: _withDeleted, ...query }, authContext = this.user ? authContextOf(this.user) : void 0) {
|
|
6414
6452
|
const subscriptionId = this.generateSubscriptionId();
|
|
6415
|
-
|
|
6416
|
-
onUpdate(rows);
|
|
6417
|
-
};
|
|
6418
|
-
this.realtimeService.registerDataDriverSubscription(subscriptionId, {
|
|
6419
|
-
clientId: "driver",
|
|
6453
|
+
this.realtimeService.startDataDriverSubscription(subscriptionId, {
|
|
6420
6454
|
type: "collection",
|
|
6421
6455
|
path,
|
|
6422
6456
|
collectionRequest: {
|
|
6423
|
-
|
|
6424
|
-
orderBy,
|
|
6425
|
-
order,
|
|
6426
|
-
limit,
|
|
6427
|
-
offset,
|
|
6457
|
+
...query,
|
|
6428
6458
|
startAfter,
|
|
6429
|
-
databaseId: collection?.databaseId
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
path,
|
|
6436
|
-
collection,
|
|
6437
|
-
filter,
|
|
6438
|
-
limit,
|
|
6439
|
-
offset,
|
|
6440
|
-
startAfter,
|
|
6441
|
-
orderBy,
|
|
6442
|
-
searchString,
|
|
6443
|
-
order
|
|
6444
|
-
}).then((rows) => {
|
|
6445
|
-
callbackWrapper(rows);
|
|
6446
|
-
}).catch((error) => {
|
|
6447
|
-
if (onError) onError(error);
|
|
6459
|
+
databaseId: collection?.databaseId
|
|
6460
|
+
},
|
|
6461
|
+
authContext,
|
|
6462
|
+
onError: fetchErrorListener(onError)
|
|
6463
|
+
}, (rows) => {
|
|
6464
|
+
if (Array.isArray(rows)) onUpdate(rows);
|
|
6448
6465
|
});
|
|
6449
|
-
return () =>
|
|
6450
|
-
this.realtimeService.removeSubscriptionCallback(subscriptionId);
|
|
6451
|
-
this.realtimeService.subscriptions.delete(subscriptionId);
|
|
6452
|
-
};
|
|
6466
|
+
return () => this.realtimeService.unsubscribe(subscriptionId);
|
|
6453
6467
|
}
|
|
6454
6468
|
async fetchOne({ path, id, databaseId, collection, withDeleted }) {
|
|
6455
6469
|
let row = await this.dataService.fetchOne(path, id, databaseId || collection?.databaseId, withDeleted);
|
|
@@ -6478,31 +6492,23 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6478
6492
|
}
|
|
6479
6493
|
return row;
|
|
6480
6494
|
}
|
|
6481
|
-
|
|
6495
|
+
/**
|
|
6496
|
+
* Listen to one row: the row now, then again after every change, and
|
|
6497
|
+
* `null` when it is not there — deleted, never created, or not readable by
|
|
6498
|
+
* the subscriber. Reads as {@link listenCollection} does.
|
|
6499
|
+
*/
|
|
6500
|
+
listenOne({ path, id, onUpdate, onError }, authContext = this.user ? authContextOf(this.user) : void 0) {
|
|
6482
6501
|
const subscriptionId = this.generateSubscriptionId();
|
|
6483
|
-
|
|
6484
|
-
if (row) onUpdate(row);
|
|
6485
|
-
};
|
|
6486
|
-
this.realtimeService.registerDataDriverSubscription(subscriptionId, {
|
|
6487
|
-
clientId: "driver",
|
|
6502
|
+
this.realtimeService.startDataDriverSubscription(subscriptionId, {
|
|
6488
6503
|
type: "single",
|
|
6489
|
-
path,
|
|
6490
|
-
id
|
|
6491
|
-
});
|
|
6492
|
-
this.realtimeService.addSubscriptionCallback(subscriptionId, callbackWrapper);
|
|
6493
|
-
this.fetchOne({
|
|
6494
6504
|
path,
|
|
6495
6505
|
id,
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
if (onError) onError(error);
|
|
6506
|
+
authContext,
|
|
6507
|
+
onError: fetchErrorListener(onError)
|
|
6508
|
+
}, (row) => {
|
|
6509
|
+
if (!Array.isArray(row)) onUpdate(row);
|
|
6501
6510
|
});
|
|
6502
|
-
return () =>
|
|
6503
|
-
this.realtimeService.removeSubscriptionCallback(subscriptionId);
|
|
6504
|
-
this.realtimeService.subscriptions.delete(subscriptionId);
|
|
6505
|
-
};
|
|
6511
|
+
return () => this.realtimeService.unsubscribe(subscriptionId);
|
|
6506
6512
|
}
|
|
6507
6513
|
/**
|
|
6508
6514
|
* How many membership rows a single write will read to answer "which
|
|
@@ -7402,19 +7408,9 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
7402
7408
|
const pendingNotifications = [];
|
|
7403
7409
|
const writeScope = options?.accessMode === "read only" ? void 0 : new WriteTransactionScope();
|
|
7404
7410
|
const result = await withFieldViewer({ roles: this.user?.roles ?? [] }, async () => await runInWriteScope(writeScope, () => this.delegate.db.transaction(async (tx) => {
|
|
7405
|
-
|
|
7406
|
-
if (!uid) {
|
|
7407
|
-
logger.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object", { detail: this.user });
|
|
7408
|
-
uid = "anonymous";
|
|
7409
|
-
}
|
|
7410
|
-
const userRoles = this.user?.roles ?? [];
|
|
7411
|
+
if (!this.user?.uid) logger.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object", { detail: this.user });
|
|
7411
7412
|
if (!this.user?.roles) logger.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object", { detail: this.user });
|
|
7412
|
-
await applyAuthContext(tx,
|
|
7413
|
-
uid,
|
|
7414
|
-
roles: userRoles,
|
|
7415
|
-
isAnonymous: this.user?.isAnonymous === true,
|
|
7416
|
-
claims: this.user?.claims
|
|
7417
|
-
}, this.delegate.rlsUserRole);
|
|
7413
|
+
await applyAuthContext(tx, authContextOf(this.user), this.delegate.rlsUserRole);
|
|
7418
7414
|
const txEntityService = new DataService(tx, this.delegate.registry);
|
|
7419
7415
|
const txDelegate = new PostgresBackendDriver(tx, this.delegate.realtimeService, this.delegate.registry, this.user, this.delegate.poolManager, this.delegate.historyService);
|
|
7420
7416
|
txDelegate.dataService = txEntityService;
|
|
@@ -7444,28 +7440,21 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
7444
7440
|
return this.withTransaction((delegate) => delegate.fetchCollection(props), { accessMode: "read only" });
|
|
7445
7441
|
}
|
|
7446
7442
|
/**
|
|
7447
|
-
*
|
|
7448
|
-
*
|
|
7443
|
+
* The delegate is the base driver, on the owner connection. The user's
|
|
7444
|
+
* identity goes in with the subscription, so the first read runs as them
|
|
7445
|
+
* like every refetch after it. It used to be stamped onto the last
|
|
7446
|
+
* registered subscription once the delegate had returned — by which time
|
|
7447
|
+
* the first read had been issued unscoped, and a listener was handed rows
|
|
7448
|
+
* its policies deny.
|
|
7449
7449
|
*/
|
|
7450
|
-
injectAuthContext(unsubscribe) {
|
|
7451
|
-
const authContext = {
|
|
7452
|
-
uid: this.user?.uid || "anonymous",
|
|
7453
|
-
roles: this.user?.roles ?? [],
|
|
7454
|
-
...this.user?.claims ? { claims: this.user.claims } : {}
|
|
7455
|
-
};
|
|
7456
|
-
const entries = Array.from(this.delegate.realtimeService.subscriptions.entries());
|
|
7457
|
-
const lastSub = entries[entries.length - 1]?.[1];
|
|
7458
|
-
if (lastSub && lastSub.clientId === "driver") lastSub.authContext = authContext;
|
|
7459
|
-
return unsubscribe;
|
|
7460
|
-
}
|
|
7461
7450
|
listenCollection(props) {
|
|
7462
|
-
return this.
|
|
7451
|
+
return this.delegate.listenCollection(props, authContextOf(this.user));
|
|
7463
7452
|
}
|
|
7464
7453
|
async fetchOne(props) {
|
|
7465
7454
|
return this.withTransaction((delegate) => delegate.fetchOne(props), { accessMode: "read only" });
|
|
7466
7455
|
}
|
|
7467
7456
|
listenOne(props) {
|
|
7468
|
-
return this.
|
|
7457
|
+
return this.delegate.listenOne(props, authContextOf(this.user));
|
|
7469
7458
|
}
|
|
7470
7459
|
async save(props) {
|
|
7471
7460
|
return this.withTransaction((delegate) => delegate.save(props));
|
|
@@ -9100,36 +9089,67 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9100
9089
|
*
|
|
9101
9090
|
* The last two are identity, not presence: the map has to still hold *this
|
|
9102
9091
|
* exact object*, not merely something under this id.
|
|
9092
|
+
*
|
|
9093
|
+
* A failure goes through the same slot, by `mayReportFailure()`: an error
|
|
9094
|
+
* frame from a stale, cancelled or replaced fetch would mark a view that
|
|
9095
|
+
* already shows newer rows as failed, or fail another subscription's view.
|
|
9096
|
+
* It also answers yes to the delivery that already claimed the slot, since
|
|
9097
|
+
* that is the send itself failing (a row that will not serialise) after the
|
|
9098
|
+
* check passed and before anything reached the subscriber.
|
|
9103
9099
|
*/
|
|
9104
9100
|
beginDelivery(subscriptionId, subscription) {
|
|
9105
9101
|
const seq = ++subscription.started;
|
|
9106
|
-
|
|
9102
|
+
const canDeliver = () => {
|
|
9107
9103
|
if (this._subscriptions.get(subscriptionId) !== subscription) return false;
|
|
9108
9104
|
if (seq <= subscription.delivered) return false;
|
|
9109
9105
|
subscription.delivered = seq;
|
|
9110
9106
|
return true;
|
|
9111
9107
|
};
|
|
9108
|
+
const mayReportFailure = () => canDeliver() || this._subscriptions.get(subscriptionId) === subscription && subscription.delivered === seq;
|
|
9109
|
+
return Object.assign(canDeliver, { mayReportFailure });
|
|
9112
9110
|
}
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9111
|
+
/**
|
|
9112
|
+
* Start an in-process listener: register it, then deliver its first rows.
|
|
9113
|
+
*
|
|
9114
|
+
* The first fetch runs here, as the subscriber and through a delivery slot,
|
|
9115
|
+
* the way every refetch after it does. `PostgresBackendDriver.listen*` used
|
|
9116
|
+
* to run it themselves, outside this service, and got all three wrong: a
|
|
9117
|
+
* listener on `withAuth(user)` read its first rows on the owner connection,
|
|
9118
|
+
* so it was handed rows its policies deny; a slow first fetch landed over a
|
|
9119
|
+
* newer refetch, and after an unsubscribe; and a row listener never heard a
|
|
9120
|
+
* `null`. The subscriber's identity is part of the request because a
|
|
9121
|
+
* subscription registered first and stamped with it afterwards has a first
|
|
9122
|
+
* fetch that ran before the stamp.
|
|
9123
|
+
*
|
|
9124
|
+
* No `authContext` reads as the anonymous user, first fetch and refetches
|
|
9125
|
+
* alike — see {@link fetchCollectionWithAuth}.
|
|
9126
|
+
*/
|
|
9127
|
+
startDataDriverSubscription(subscriptionId, request, callback) {
|
|
9128
|
+
this.debugLog("📋 [RealtimeService] Starting DataDriver subscription:", subscriptionId, request.authContext ? "(with auth)" : "(no auth)");
|
|
9129
|
+
const subscription = {
|
|
9130
|
+
clientId: "driver",
|
|
9131
|
+
...request,
|
|
9117
9132
|
started: 0,
|
|
9118
9133
|
delivered: 0
|
|
9119
|
-
}
|
|
9120
|
-
|
|
9121
|
-
addSubscriptionCallback(subscriptionId, callback) {
|
|
9122
|
-
this.debugLog("📋 [RealtimeService] Adding callback for subscription:", subscriptionId);
|
|
9134
|
+
};
|
|
9135
|
+
this._subscriptions.set(subscriptionId, subscription);
|
|
9123
9136
|
this.subscriptionCallbacks.set(subscriptionId, callback);
|
|
9137
|
+
this.deliverFirstToDataDriver(subscriptionId, subscription, callback);
|
|
9124
9138
|
}
|
|
9125
|
-
|
|
9126
|
-
this.
|
|
9127
|
-
|
|
9139
|
+
async deliverFirstToDataDriver(subscriptionId, subscription, callback) {
|
|
9140
|
+
const canDeliver = this.beginDelivery(subscriptionId, subscription);
|
|
9141
|
+
try {
|
|
9142
|
+
const data = subscription.type === "collection" ? await this.fetchCollectionWithAuth(subscription.path, subscription.collectionRequest ?? {}, subscription.authContext) : await this.fetchEntityWithAuth(subscription.path, String(subscription.id), subscription.authContext) ?? null;
|
|
9143
|
+
if (canDeliver()) callback(data);
|
|
9144
|
+
} catch (error) {
|
|
9145
|
+
logger.error(`❌ [RealtimeService] Error in the first fetch for DataDriver subscription ${subscriptionId}`, { error });
|
|
9146
|
+
this.reportDriverFetchFailure(subscriptionId, subscription, canDeliver, error);
|
|
9147
|
+
}
|
|
9128
9148
|
}
|
|
9129
9149
|
/**
|
|
9130
9150
|
* Subscribe to collection changes (RealtimeProvider interface)
|
|
9131
9151
|
*/
|
|
9132
|
-
subscribeToCollection(subscriptionId, config, callback) {
|
|
9152
|
+
subscribeToCollection(subscriptionId, config, callback, onError) {
|
|
9133
9153
|
this._subscriptions.set(subscriptionId, {
|
|
9134
9154
|
clientId: config.clientId,
|
|
9135
9155
|
type: "collection",
|
|
@@ -9144,6 +9164,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9144
9164
|
searchString: config.searchString,
|
|
9145
9165
|
searchExplain: config.searchExplain
|
|
9146
9166
|
},
|
|
9167
|
+
onError,
|
|
9147
9168
|
started: 0,
|
|
9148
9169
|
delivered: 0
|
|
9149
9170
|
});
|
|
@@ -9152,12 +9173,13 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9152
9173
|
/**
|
|
9153
9174
|
* Subscribe to single row changes (RealtimeProvider interface)
|
|
9154
9175
|
*/
|
|
9155
|
-
subscribeToOne(subscriptionId, config, callback) {
|
|
9176
|
+
subscribeToOne(subscriptionId, config, callback, onError) {
|
|
9156
9177
|
this._subscriptions.set(subscriptionId, {
|
|
9157
9178
|
clientId: config.clientId,
|
|
9158
9179
|
type: "single",
|
|
9159
9180
|
path: config.path,
|
|
9160
9181
|
id: config.id,
|
|
9182
|
+
onError,
|
|
9161
9183
|
started: 0,
|
|
9162
9184
|
delivered: 0
|
|
9163
9185
|
});
|
|
@@ -9239,6 +9261,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9239
9261
|
}
|
|
9240
9262
|
async handleCollectionSubscription(clientId, request, authContext) {
|
|
9241
9263
|
const subscriptionId = request.subscriptionId;
|
|
9264
|
+
let canDeliver;
|
|
9242
9265
|
try {
|
|
9243
9266
|
if (!this.registry.getCollectionByPath(request.path)) {
|
|
9244
9267
|
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
@@ -9295,17 +9318,17 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9295
9318
|
delivered: 0
|
|
9296
9319
|
};
|
|
9297
9320
|
this._subscriptions.set(subscriptionId, subscription);
|
|
9298
|
-
|
|
9321
|
+
canDeliver = this.beginDelivery(subscriptionId, subscription);
|
|
9299
9322
|
const rows = await this.fetchCollectionWithAuth(request.path, subscription.collectionRequest, authContext);
|
|
9300
9323
|
const meta = await this.collectionMetaWithAuth(request.path, subscription.collectionRequest, rows, authContext);
|
|
9301
9324
|
if (canDeliver()) this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path, meta);
|
|
9302
9325
|
} catch (error) {
|
|
9303
|
-
|
|
9304
|
-
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9326
|
+
this.reportSocketFetchFailure(clientId, subscriptionId, request.path, error, canDeliver);
|
|
9305
9327
|
}
|
|
9306
9328
|
}
|
|
9307
9329
|
async handleEntitySubscription(clientId, request, authContext) {
|
|
9308
9330
|
const subscriptionId = request.subscriptionId;
|
|
9331
|
+
let canDeliver;
|
|
9309
9332
|
try {
|
|
9310
9333
|
if (!this.registry.getCollectionByPath(request.path)) {
|
|
9311
9334
|
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
@@ -9324,14 +9347,31 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9324
9347
|
delivered: 0
|
|
9325
9348
|
};
|
|
9326
9349
|
this._subscriptions.set(subscriptionId, subscription);
|
|
9327
|
-
|
|
9350
|
+
canDeliver = this.beginDelivery(subscriptionId, subscription);
|
|
9328
9351
|
const row = await this.fetchEntityWithAuth(request.path, String(request.id), authContext);
|
|
9329
9352
|
if (canDeliver()) this.sendSingleUpdate(clientId, subscriptionId, row || null);
|
|
9330
9353
|
} catch (error) {
|
|
9331
|
-
|
|
9332
|
-
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9354
|
+
this.reportSocketFetchFailure(clientId, subscriptionId, request.path, error, canDeliver);
|
|
9333
9355
|
}
|
|
9334
9356
|
}
|
|
9357
|
+
/**
|
|
9358
|
+
* Answer a subscriber's failed fetch with an error frame, through the slot
|
|
9359
|
+
* its rows would have used.
|
|
9360
|
+
*
|
|
9361
|
+
* The error is always logged (`sanitizeErrorForClient` does that), but the
|
|
9362
|
+
* frame goes out only while this fetch holds the newest slot. A failed
|
|
9363
|
+
* straggler used to send one regardless. The client routes errors by
|
|
9364
|
+
* `subscriptionId`, so it called `onError` for a view already showing newer
|
|
9365
|
+
* rows, or for the subscription that replaced this one under the same id.
|
|
9366
|
+
*
|
|
9367
|
+
* With no slot, the subscribe failed before its fetch began. That part runs
|
|
9368
|
+
* synchronously, so nothing else can have answered the request yet.
|
|
9369
|
+
*/
|
|
9370
|
+
reportSocketFetchFailure(clientId, subscriptionId, path, error, canDeliver) {
|
|
9371
|
+
const sanitized = sanitizeErrorForClient(error, path);
|
|
9372
|
+
if (canDeliver && !canDeliver.mayReportFailure()) return;
|
|
9373
|
+
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9374
|
+
}
|
|
9335
9375
|
async handleUnsubscribe(_clientId, subscriptionId) {
|
|
9336
9376
|
this._subscriptions.delete(subscriptionId);
|
|
9337
9377
|
this.subscriptionCallbacks.delete(subscriptionId);
|
|
@@ -9464,8 +9504,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9464
9504
|
const meta = await this.collectionMetaWithAuth(notifyPath, subscription.collectionRequest, rows, subscription.authContext);
|
|
9465
9505
|
if (canDeliver()) this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath, meta);
|
|
9466
9506
|
} catch (error) {
|
|
9467
|
-
|
|
9468
|
-
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9507
|
+
this.reportSocketFetchFailure(subscription.clientId, subscriptionId, notifyPath, error, canDeliver);
|
|
9469
9508
|
}
|
|
9470
9509
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9471
9510
|
}
|
|
@@ -9485,10 +9524,30 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9485
9524
|
if (canDeliver()) callback(rows);
|
|
9486
9525
|
} catch (error) {
|
|
9487
9526
|
logger.error(`❌ [RealtimeService] Error in debounced driver refetch for ${subscriptionId}`, { error });
|
|
9527
|
+
this.reportDriverFetchFailure(subscriptionId, subscription, canDeliver, error);
|
|
9488
9528
|
}
|
|
9489
9529
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9490
9530
|
}
|
|
9491
9531
|
/**
|
|
9532
|
+
* Tell an in-process listener its refetch failed, through the slot the
|
|
9533
|
+
* rows would have used.
|
|
9534
|
+
*
|
|
9535
|
+
* The two driver-refetch `catch` blocks used to log and stop, so the
|
|
9536
|
+
* listener heard nothing and kept its last rows as if they were current.
|
|
9537
|
+
* The error goes to it as thrown: this is trusted server code, and the
|
|
9538
|
+
* masking on the socket path is for clients. The slot keeps a failed
|
|
9539
|
+
* straggler from reporting over newer rows, and keeps a failure away from
|
|
9540
|
+
* a subscription that was cancelled or replaced while it ran.
|
|
9541
|
+
*/
|
|
9542
|
+
reportDriverFetchFailure(subscriptionId, subscription, canDeliver, error) {
|
|
9543
|
+
if (!subscription.onError || !canDeliver()) return;
|
|
9544
|
+
try {
|
|
9545
|
+
subscription.onError(error);
|
|
9546
|
+
} catch (listenerError) {
|
|
9547
|
+
logger.error(`❌ [RealtimeService] onError threw for DataDriver subscription ${subscriptionId}`, { error: listenerError });
|
|
9548
|
+
}
|
|
9549
|
+
}
|
|
9550
|
+
/**
|
|
9492
9551
|
* Fetch a collection with optional RLS auth context.
|
|
9493
9552
|
* When authContext is provided, the fetch runs inside a transaction
|
|
9494
9553
|
* with set_config calls so PostgreSQL RLS policies are enforced.
|
|
@@ -9644,8 +9703,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9644
9703
|
const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);
|
|
9645
9704
|
if (canDeliver()) this.sendSingleUpdate(subscription.clientId, subscriptionId, row || null);
|
|
9646
9705
|
} catch (error) {
|
|
9647
|
-
|
|
9648
|
-
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9706
|
+
this.reportSocketFetchFailure(subscription.clientId, subscriptionId, notifyPath, error, canDeliver);
|
|
9649
9707
|
}
|
|
9650
9708
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9651
9709
|
}
|
|
@@ -9665,6 +9723,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9665
9723
|
if (canDeliver()) callback(row || null);
|
|
9666
9724
|
} catch (error) {
|
|
9667
9725
|
logger.error(`❌ [RealtimeService] Error in debounced row driver refetch for ${subscriptionId}`, { error });
|
|
9726
|
+
this.reportDriverFetchFailure(subscriptionId, subscription, canDeliver, error);
|
|
9668
9727
|
}
|
|
9669
9728
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9670
9729
|
}
|
|
@@ -10787,6 +10846,31 @@ function extractErrorMessage(error) {
|
|
|
10787
10846
|
/**
|
|
10788
10847
|
* Check if the current session belongs to an admin user.
|
|
10789
10848
|
*/
|
|
10849
|
+
/**
|
|
10850
|
+
* Who a socket reads and writes as, for its request frames and its
|
|
10851
|
+
* subscriptions alike.
|
|
10852
|
+
*
|
|
10853
|
+
* One answer because there were two, and they disagreed about guests. The
|
|
10854
|
+
* subscriptions carried the session's `isAnonymous`; the request frames wrote
|
|
10855
|
+
* `false` for every signed-in session, so a guest read and wrote as an account
|
|
10856
|
+
* through `FETCH_*`, `SAVE` and `DELETE` while its subscriptions did not. And a
|
|
10857
|
+
* socket with no session was a guest on the request frames (`true`) but not on
|
|
10858
|
+
* its subscriptions or over REST: an unauthenticated caller is not a guest
|
|
10859
|
+
* session.
|
|
10860
|
+
*/
|
|
10861
|
+
function sessionAuthContext(session) {
|
|
10862
|
+
if (!session?.user) return {
|
|
10863
|
+
uid: ANONYMOUS_USER_ID,
|
|
10864
|
+
roles: ["anon"],
|
|
10865
|
+
isAnonymous: false
|
|
10866
|
+
};
|
|
10867
|
+
return {
|
|
10868
|
+
uid: session.user.uid,
|
|
10869
|
+
roles: session.user.roles ?? [],
|
|
10870
|
+
isAnonymous: session.user.isAnonymous === true,
|
|
10871
|
+
...session.user.claims ? { claims: session.user.claims } : {}
|
|
10872
|
+
};
|
|
10873
|
+
}
|
|
10790
10874
|
function isAdminSession(session) {
|
|
10791
10875
|
if (!session?.user) return false;
|
|
10792
10876
|
if (session.user.isAdmin) return true;
|
|
@@ -10872,13 +10956,15 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
10872
10956
|
if (adapterUser) verifiedUser = {
|
|
10873
10957
|
uid: adapterUser.uid,
|
|
10874
10958
|
roles: adapterUser.roles,
|
|
10875
|
-
isAdmin: adapterUser.isAdmin
|
|
10959
|
+
isAdmin: adapterUser.isAdmin,
|
|
10960
|
+
isAnonymous: adapterUser.isAnonymous === true
|
|
10876
10961
|
};
|
|
10877
10962
|
} catch {}
|
|
10878
10963
|
else if (authConfig?.serviceKey && safeCompare(token, authConfig.serviceKey)) verifiedUser = {
|
|
10879
10964
|
uid: "service",
|
|
10880
10965
|
roles: ["admin"],
|
|
10881
|
-
isAdmin: true
|
|
10966
|
+
isAdmin: true,
|
|
10967
|
+
isAnonymous: false
|
|
10882
10968
|
};
|
|
10883
10969
|
else {
|
|
10884
10970
|
const jwtPayload = await extractUserFromToken(token);
|
|
@@ -10966,25 +11052,17 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
10966
11052
|
assertFieldOpsValid(values, collection);
|
|
10967
11053
|
};
|
|
10968
11054
|
const getScopedDelegate = async () => {
|
|
10969
|
-
const session = clientSessions.get(clientId);
|
|
10970
11055
|
if (typeof driver.withAuth === "function") try {
|
|
10971
|
-
const
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
email: null,
|
|
10975
|
-
photoURL: null,
|
|
10976
|
-
providerId: "websocket",
|
|
10977
|
-
isAnonymous: false,
|
|
10978
|
-
roles: session.user.roles ?? [],
|
|
10979
|
-
claims: session.user.claims
|
|
10980
|
-
} : {
|
|
10981
|
-
uid: ANONYMOUS_USER_ID,
|
|
11056
|
+
const who = sessionAuthContext(clientSessions.get(clientId));
|
|
11057
|
+
const userForAuth = {
|
|
11058
|
+
uid: who.uid,
|
|
10982
11059
|
displayName: null,
|
|
10983
11060
|
email: null,
|
|
10984
11061
|
photoURL: null,
|
|
10985
11062
|
providerId: "websocket",
|
|
10986
|
-
isAnonymous: true,
|
|
10987
|
-
roles:
|
|
11063
|
+
isAnonymous: who.isAnonymous === true,
|
|
11064
|
+
roles: who.roles,
|
|
11065
|
+
...who.claims ? { claims: who.claims } : {}
|
|
10988
11066
|
};
|
|
10989
11067
|
return await driver.withAuth(userForAuth);
|
|
10990
11068
|
} catch (e) {
|
|
@@ -11287,17 +11365,7 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
11287
11365
|
case "presence_state":
|
|
11288
11366
|
case "channel_history": {
|
|
11289
11367
|
wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
|
|
11290
|
-
const
|
|
11291
|
-
const authContext = session?.user ? {
|
|
11292
|
-
uid: session.user.uid,
|
|
11293
|
-
roles: session.user.roles ?? [],
|
|
11294
|
-
isAnonymous: session.user.isAnonymous === true,
|
|
11295
|
-
claims: session.user.claims
|
|
11296
|
-
} : {
|
|
11297
|
-
uid: ANONYMOUS_USER_ID,
|
|
11298
|
-
roles: ["anon"],
|
|
11299
|
-
isAnonymous: false
|
|
11300
|
-
};
|
|
11368
|
+
const authContext = sessionAuthContext(clientSessions.get(clientId));
|
|
11301
11369
|
await realtimeService.handleClientMessage(clientId, {
|
|
11302
11370
|
type,
|
|
11303
11371
|
payload,
|
|
@@ -11333,15 +11401,18 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
11333
11401
|
}));
|
|
11334
11402
|
return;
|
|
11335
11403
|
}
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11404
|
+
const answer = declaredErrorAnswer(error);
|
|
11405
|
+
if (answer) {
|
|
11406
|
+
const line = `[WebSocket Server] Refused: ${answer.code} ${answer.message}`;
|
|
11407
|
+
if (answer.expected) logger.debug(line);
|
|
11408
|
+
else logger.warn(line);
|
|
11339
11409
|
ws.send(JSON.stringify({
|
|
11340
11410
|
type: "ERROR",
|
|
11341
11411
|
requestId,
|
|
11342
11412
|
payload: { error: {
|
|
11343
|
-
message:
|
|
11344
|
-
code:
|
|
11413
|
+
message: answer.message,
|
|
11414
|
+
code: answer.code,
|
|
11415
|
+
...answer.details !== void 0 && { details: answer.details }
|
|
11345
11416
|
} }
|
|
11346
11417
|
}));
|
|
11347
11418
|
return;
|
|
@@ -15068,7 +15139,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
15068
15139
|
* proved it can bootstrap with.
|
|
15069
15140
|
*/
|
|
15070
15141
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
15071
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
15142
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-Cr2ye5JC.js").then((n) => n.n);
|
|
15072
15143
|
const plan = await ensureCollectionTables(provisioningQueryable(driverResult), collections, log);
|
|
15073
15144
|
for (const failure of plan.failures) logger.warn(describeEnsureFailure(failure));
|
|
15074
15145
|
return { applied: plan.actions.length - plan.failures.length };
|
|
@@ -15102,7 +15173,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
15102
15173
|
await provisionRlsRuntime(provisioningQueryable(driverResult));
|
|
15103
15174
|
},
|
|
15104
15175
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
15105
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
15176
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-B4IDFtQ-.js");
|
|
15106
15177
|
const queryable = provisioningQueryable(driverResult);
|
|
15107
15178
|
const { rows } = await queryable.query("SELECT to_regprocedure('rebase.roles()') IS NOT NULL AS present");
|
|
15108
15179
|
if (!rows[0]?.present) await provisionRlsRuntime(queryable);
|