@rebasepro/server-postgres 0.21.0 → 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/README.md +15 -13
- 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 +26 -10
- package/dist/backup/backup-cron.d.ts +28 -1
- package/dist/{backup-cli-Bp-ou6o2.js → backup-cli-CkjsJEcu.js} +3 -2
- package/dist/backup-cli-CkjsJEcu.js.map +1 -0
- 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 +15 -8
- 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 +264 -180
- 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/backup-cli-Bp-ou6o2.js.map +0 -1
- package/dist/cli-errors-C3g_kHBw.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -3,22 +3,22 @@ __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
|
-
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, parseRelationAggregateSort, resolveClientListLimit } from "@rebasepro/types";
|
|
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";
|
|
23
23
|
import { camelCase, firstFreeKey, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, mergeDeep, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
|
|
24
24
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -6021,13 +6021,12 @@ async function generateSchemaCommit(input) {
|
|
|
6021
6021
|
* them. It is the only sanctioned way a statement that named a role runs
|
|
6022
6022
|
* without it — every other route now refuses.
|
|
6023
6023
|
*
|
|
6024
|
-
*
|
|
6025
|
-
*
|
|
6026
|
-
*
|
|
6027
|
-
* variable disagree with the rest.
|
|
6024
|
+
* Any spelling of yes, through the platform's one parser. It was an exact
|
|
6025
|
+
* `"true"` until that parser existed, deliberately, so that this variable would
|
|
6026
|
+
* not be fixed alone and left disagreeing with the rest.
|
|
6028
6027
|
*/
|
|
6029
6028
|
function isRoleSwitchingOptedOut() {
|
|
6030
|
-
return process.env.DISABLE_DB_ROLE_SWITCHING ===
|
|
6029
|
+
return parseEnvBoolean(process.env.DISABLE_DB_ROLE_SWITCHING) === true;
|
|
6031
6030
|
}
|
|
6032
6031
|
/**
|
|
6033
6032
|
* The role a statement will actually have run as, given the role it asked for.
|
|
@@ -6093,6 +6092,35 @@ function assertActingUserForAutoValues(properties, status, uid, path) {
|
|
|
6093
6092
|
});
|
|
6094
6093
|
}
|
|
6095
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
|
+
}
|
|
6096
6124
|
var PostgresBackendDriver = class PostgresBackendDriver {
|
|
6097
6125
|
db;
|
|
6098
6126
|
registry;
|
|
@@ -6411,46 +6439,31 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6411
6439
|
}
|
|
6412
6440
|
return rows;
|
|
6413
6441
|
}
|
|
6414
|
-
|
|
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) {
|
|
6415
6452
|
const subscriptionId = this.generateSubscriptionId();
|
|
6416
|
-
|
|
6417
|
-
onUpdate(rows);
|
|
6418
|
-
};
|
|
6419
|
-
this.realtimeService.registerDataDriverSubscription(subscriptionId, {
|
|
6420
|
-
clientId: "driver",
|
|
6453
|
+
this.realtimeService.startDataDriverSubscription(subscriptionId, {
|
|
6421
6454
|
type: "collection",
|
|
6422
6455
|
path,
|
|
6423
6456
|
collectionRequest: {
|
|
6424
|
-
|
|
6425
|
-
orderBy,
|
|
6426
|
-
order,
|
|
6427
|
-
limit,
|
|
6428
|
-
offset,
|
|
6457
|
+
...query,
|
|
6429
6458
|
startAfter,
|
|
6430
|
-
databaseId: collection?.databaseId
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
path,
|
|
6437
|
-
collection,
|
|
6438
|
-
filter,
|
|
6439
|
-
limit,
|
|
6440
|
-
offset,
|
|
6441
|
-
startAfter,
|
|
6442
|
-
orderBy,
|
|
6443
|
-
searchString,
|
|
6444
|
-
order
|
|
6445
|
-
}).then((rows) => {
|
|
6446
|
-
callbackWrapper(rows);
|
|
6447
|
-
}).catch((error) => {
|
|
6448
|
-
if (onError) onError(error);
|
|
6459
|
+
databaseId: collection?.databaseId
|
|
6460
|
+
},
|
|
6461
|
+
authContext,
|
|
6462
|
+
onError: fetchErrorListener(onError)
|
|
6463
|
+
}, (rows) => {
|
|
6464
|
+
if (Array.isArray(rows)) onUpdate(rows);
|
|
6449
6465
|
});
|
|
6450
|
-
return () =>
|
|
6451
|
-
this.realtimeService.removeSubscriptionCallback(subscriptionId);
|
|
6452
|
-
this.realtimeService.subscriptions.delete(subscriptionId);
|
|
6453
|
-
};
|
|
6466
|
+
return () => this.realtimeService.unsubscribe(subscriptionId);
|
|
6454
6467
|
}
|
|
6455
6468
|
async fetchOne({ path, id, databaseId, collection, withDeleted }) {
|
|
6456
6469
|
let row = await this.dataService.fetchOne(path, id, databaseId || collection?.databaseId, withDeleted);
|
|
@@ -6479,31 +6492,23 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6479
6492
|
}
|
|
6480
6493
|
return row;
|
|
6481
6494
|
}
|
|
6482
|
-
|
|
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) {
|
|
6483
6501
|
const subscriptionId = this.generateSubscriptionId();
|
|
6484
|
-
|
|
6485
|
-
if (row) onUpdate(row);
|
|
6486
|
-
};
|
|
6487
|
-
this.realtimeService.registerDataDriverSubscription(subscriptionId, {
|
|
6488
|
-
clientId: "driver",
|
|
6502
|
+
this.realtimeService.startDataDriverSubscription(subscriptionId, {
|
|
6489
6503
|
type: "single",
|
|
6490
|
-
path,
|
|
6491
|
-
id
|
|
6492
|
-
});
|
|
6493
|
-
this.realtimeService.addSubscriptionCallback(subscriptionId, callbackWrapper);
|
|
6494
|
-
this.fetchOne({
|
|
6495
6504
|
path,
|
|
6496
6505
|
id,
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
if (onError) onError(error);
|
|
6506
|
+
authContext,
|
|
6507
|
+
onError: fetchErrorListener(onError)
|
|
6508
|
+
}, (row) => {
|
|
6509
|
+
if (!Array.isArray(row)) onUpdate(row);
|
|
6502
6510
|
});
|
|
6503
|
-
return () =>
|
|
6504
|
-
this.realtimeService.removeSubscriptionCallback(subscriptionId);
|
|
6505
|
-
this.realtimeService.subscriptions.delete(subscriptionId);
|
|
6506
|
-
};
|
|
6511
|
+
return () => this.realtimeService.unsubscribe(subscriptionId);
|
|
6507
6512
|
}
|
|
6508
6513
|
/**
|
|
6509
6514
|
* How many membership rows a single write will read to answer "which
|
|
@@ -6917,20 +6922,10 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6917
6922
|
for (let i = 0; i < ids.length; i++) {
|
|
6918
6923
|
const id = ids[i];
|
|
6919
6924
|
try {
|
|
6920
|
-
const existing = await txDriver.fetchOne({
|
|
6921
|
-
path,
|
|
6922
|
-
id: String(id),
|
|
6923
|
-
collection
|
|
6924
|
-
});
|
|
6925
|
-
if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
|
|
6926
|
-
statusCode: 404,
|
|
6927
|
-
code: "NOT_FOUND"
|
|
6928
|
-
});
|
|
6929
6925
|
await txDriver.delete({
|
|
6930
6926
|
row: {
|
|
6931
6927
|
id: String(id),
|
|
6932
|
-
path
|
|
6933
|
-
values: existing
|
|
6928
|
+
path
|
|
6934
6929
|
},
|
|
6935
6930
|
collection,
|
|
6936
6931
|
hard
|
|
@@ -7010,27 +7005,16 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
7010
7005
|
status: "existing"
|
|
7011
7006
|
});
|
|
7012
7007
|
break;
|
|
7013
|
-
case "delete":
|
|
7014
|
-
const existing = await txDriver.fetchOne({
|
|
7015
|
-
path: operation.path,
|
|
7016
|
-
id,
|
|
7017
|
-
collection: operation.collection
|
|
7018
|
-
});
|
|
7019
|
-
if (!existing) throw Object.assign(/* @__PURE__ */ new Error(`No row with id ${JSON.stringify(id)}`), {
|
|
7020
|
-
statusCode: 404,
|
|
7021
|
-
code: "NOT_FOUND"
|
|
7022
|
-
});
|
|
7008
|
+
case "delete":
|
|
7023
7009
|
await txDriver.delete({
|
|
7024
7010
|
row: {
|
|
7025
7011
|
id,
|
|
7026
|
-
path: operation.path
|
|
7027
|
-
values: existing
|
|
7012
|
+
path: operation.path
|
|
7028
7013
|
},
|
|
7029
7014
|
collection: operation.collection
|
|
7030
7015
|
});
|
|
7031
7016
|
row = null;
|
|
7032
7017
|
break;
|
|
7033
|
-
}
|
|
7034
7018
|
}
|
|
7035
7019
|
if (operation.ref && row) named.set(operation.ref, row);
|
|
7036
7020
|
results.push(row);
|
|
@@ -7047,8 +7031,15 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
7047
7031
|
}
|
|
7048
7032
|
async delete({ row, collection, hard }) {
|
|
7049
7033
|
const targetPath = row.path;
|
|
7050
|
-
const targetRow = { ...row.values ?? {} };
|
|
7051
7034
|
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, targetPath);
|
|
7035
|
+
const stored = await this.fetchOne({
|
|
7036
|
+
path: targetPath,
|
|
7037
|
+
id: row.id,
|
|
7038
|
+
collection: resolvedCollection,
|
|
7039
|
+
withDeleted: hard ? true : void 0
|
|
7040
|
+
});
|
|
7041
|
+
if (!stored) throw ApiError.notFound(`No row "${row.id}" in "${targetPath}" to delete.`);
|
|
7042
|
+
const targetRow = { ...stored };
|
|
7052
7043
|
const contextForCallback = this.buildCallContext();
|
|
7053
7044
|
try {
|
|
7054
7045
|
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
@@ -7121,7 +7112,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
7121
7112
|
tableName: targetPath,
|
|
7122
7113
|
id: row.id.toString(),
|
|
7123
7114
|
action: "delete",
|
|
7124
|
-
values:
|
|
7115
|
+
values: stored,
|
|
7125
7116
|
updatedBy: this.user?.uid
|
|
7126
7117
|
});
|
|
7127
7118
|
if (this._deferNotifications) this._pendingNotifications.push({
|
|
@@ -7417,19 +7408,9 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
7417
7408
|
const pendingNotifications = [];
|
|
7418
7409
|
const writeScope = options?.accessMode === "read only" ? void 0 : new WriteTransactionScope();
|
|
7419
7410
|
const result = await withFieldViewer({ roles: this.user?.roles ?? [] }, async () => await runInWriteScope(writeScope, () => this.delegate.db.transaction(async (tx) => {
|
|
7420
|
-
|
|
7421
|
-
if (!uid) {
|
|
7422
|
-
logger.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object", { detail: this.user });
|
|
7423
|
-
uid = "anonymous";
|
|
7424
|
-
}
|
|
7425
|
-
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 });
|
|
7426
7412
|
if (!this.user?.roles) logger.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object", { detail: this.user });
|
|
7427
|
-
await applyAuthContext(tx,
|
|
7428
|
-
uid,
|
|
7429
|
-
roles: userRoles,
|
|
7430
|
-
isAnonymous: this.user?.isAnonymous === true,
|
|
7431
|
-
claims: this.user?.claims
|
|
7432
|
-
}, this.delegate.rlsUserRole);
|
|
7413
|
+
await applyAuthContext(tx, authContextOf(this.user), this.delegate.rlsUserRole);
|
|
7433
7414
|
const txEntityService = new DataService(tx, this.delegate.registry);
|
|
7434
7415
|
const txDelegate = new PostgresBackendDriver(tx, this.delegate.realtimeService, this.delegate.registry, this.user, this.delegate.poolManager, this.delegate.historyService);
|
|
7435
7416
|
txDelegate.dataService = txEntityService;
|
|
@@ -7459,28 +7440,21 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
7459
7440
|
return this.withTransaction((delegate) => delegate.fetchCollection(props), { accessMode: "read only" });
|
|
7460
7441
|
}
|
|
7461
7442
|
/**
|
|
7462
|
-
*
|
|
7463
|
-
*
|
|
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.
|
|
7464
7449
|
*/
|
|
7465
|
-
injectAuthContext(unsubscribe) {
|
|
7466
|
-
const authContext = {
|
|
7467
|
-
uid: this.user?.uid || "anonymous",
|
|
7468
|
-
roles: this.user?.roles ?? [],
|
|
7469
|
-
...this.user?.claims ? { claims: this.user.claims } : {}
|
|
7470
|
-
};
|
|
7471
|
-
const entries = Array.from(this.delegate.realtimeService.subscriptions.entries());
|
|
7472
|
-
const lastSub = entries[entries.length - 1]?.[1];
|
|
7473
|
-
if (lastSub && lastSub.clientId === "driver") lastSub.authContext = authContext;
|
|
7474
|
-
return unsubscribe;
|
|
7475
|
-
}
|
|
7476
7450
|
listenCollection(props) {
|
|
7477
|
-
return this.
|
|
7451
|
+
return this.delegate.listenCollection(props, authContextOf(this.user));
|
|
7478
7452
|
}
|
|
7479
7453
|
async fetchOne(props) {
|
|
7480
7454
|
return this.withTransaction((delegate) => delegate.fetchOne(props), { accessMode: "read only" });
|
|
7481
7455
|
}
|
|
7482
7456
|
listenOne(props) {
|
|
7483
|
-
return this.
|
|
7457
|
+
return this.delegate.listenOne(props, authContextOf(this.user));
|
|
7484
7458
|
}
|
|
7485
7459
|
async save(props) {
|
|
7486
7460
|
return this.withTransaction((delegate) => delegate.save(props));
|
|
@@ -9115,36 +9089,67 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9115
9089
|
*
|
|
9116
9090
|
* The last two are identity, not presence: the map has to still hold *this
|
|
9117
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.
|
|
9118
9099
|
*/
|
|
9119
9100
|
beginDelivery(subscriptionId, subscription) {
|
|
9120
9101
|
const seq = ++subscription.started;
|
|
9121
|
-
|
|
9102
|
+
const canDeliver = () => {
|
|
9122
9103
|
if (this._subscriptions.get(subscriptionId) !== subscription) return false;
|
|
9123
9104
|
if (seq <= subscription.delivered) return false;
|
|
9124
9105
|
subscription.delivered = seq;
|
|
9125
9106
|
return true;
|
|
9126
9107
|
};
|
|
9108
|
+
const mayReportFailure = () => canDeliver() || this._subscriptions.get(subscriptionId) === subscription && subscription.delivered === seq;
|
|
9109
|
+
return Object.assign(canDeliver, { mayReportFailure });
|
|
9127
9110
|
}
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
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,
|
|
9132
9132
|
started: 0,
|
|
9133
9133
|
delivered: 0
|
|
9134
|
-
}
|
|
9135
|
-
|
|
9136
|
-
addSubscriptionCallback(subscriptionId, callback) {
|
|
9137
|
-
this.debugLog("📋 [RealtimeService] Adding callback for subscription:", subscriptionId);
|
|
9134
|
+
};
|
|
9135
|
+
this._subscriptions.set(subscriptionId, subscription);
|
|
9138
9136
|
this.subscriptionCallbacks.set(subscriptionId, callback);
|
|
9137
|
+
this.deliverFirstToDataDriver(subscriptionId, subscription, callback);
|
|
9139
9138
|
}
|
|
9140
|
-
|
|
9141
|
-
this.
|
|
9142
|
-
|
|
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
|
+
}
|
|
9143
9148
|
}
|
|
9144
9149
|
/**
|
|
9145
9150
|
* Subscribe to collection changes (RealtimeProvider interface)
|
|
9146
9151
|
*/
|
|
9147
|
-
subscribeToCollection(subscriptionId, config, callback) {
|
|
9152
|
+
subscribeToCollection(subscriptionId, config, callback, onError) {
|
|
9148
9153
|
this._subscriptions.set(subscriptionId, {
|
|
9149
9154
|
clientId: config.clientId,
|
|
9150
9155
|
type: "collection",
|
|
@@ -9159,6 +9164,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9159
9164
|
searchString: config.searchString,
|
|
9160
9165
|
searchExplain: config.searchExplain
|
|
9161
9166
|
},
|
|
9167
|
+
onError,
|
|
9162
9168
|
started: 0,
|
|
9163
9169
|
delivered: 0
|
|
9164
9170
|
});
|
|
@@ -9167,12 +9173,13 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9167
9173
|
/**
|
|
9168
9174
|
* Subscribe to single row changes (RealtimeProvider interface)
|
|
9169
9175
|
*/
|
|
9170
|
-
subscribeToOne(subscriptionId, config, callback) {
|
|
9176
|
+
subscribeToOne(subscriptionId, config, callback, onError) {
|
|
9171
9177
|
this._subscriptions.set(subscriptionId, {
|
|
9172
9178
|
clientId: config.clientId,
|
|
9173
9179
|
type: "single",
|
|
9174
9180
|
path: config.path,
|
|
9175
9181
|
id: config.id,
|
|
9182
|
+
onError,
|
|
9176
9183
|
started: 0,
|
|
9177
9184
|
delivered: 0
|
|
9178
9185
|
});
|
|
@@ -9254,6 +9261,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9254
9261
|
}
|
|
9255
9262
|
async handleCollectionSubscription(clientId, request, authContext) {
|
|
9256
9263
|
const subscriptionId = request.subscriptionId;
|
|
9264
|
+
let canDeliver;
|
|
9257
9265
|
try {
|
|
9258
9266
|
if (!this.registry.getCollectionByPath(request.path)) {
|
|
9259
9267
|
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
@@ -9310,17 +9318,17 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9310
9318
|
delivered: 0
|
|
9311
9319
|
};
|
|
9312
9320
|
this._subscriptions.set(subscriptionId, subscription);
|
|
9313
|
-
|
|
9321
|
+
canDeliver = this.beginDelivery(subscriptionId, subscription);
|
|
9314
9322
|
const rows = await this.fetchCollectionWithAuth(request.path, subscription.collectionRequest, authContext);
|
|
9315
9323
|
const meta = await this.collectionMetaWithAuth(request.path, subscription.collectionRequest, rows, authContext);
|
|
9316
9324
|
if (canDeliver()) this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path, meta);
|
|
9317
9325
|
} catch (error) {
|
|
9318
|
-
|
|
9319
|
-
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9326
|
+
this.reportSocketFetchFailure(clientId, subscriptionId, request.path, error, canDeliver);
|
|
9320
9327
|
}
|
|
9321
9328
|
}
|
|
9322
9329
|
async handleEntitySubscription(clientId, request, authContext) {
|
|
9323
9330
|
const subscriptionId = request.subscriptionId;
|
|
9331
|
+
let canDeliver;
|
|
9324
9332
|
try {
|
|
9325
9333
|
if (!this.registry.getCollectionByPath(request.path)) {
|
|
9326
9334
|
const registered = this.registry.getCollections().map((c) => c.slug).join(", ");
|
|
@@ -9339,14 +9347,31 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9339
9347
|
delivered: 0
|
|
9340
9348
|
};
|
|
9341
9349
|
this._subscriptions.set(subscriptionId, subscription);
|
|
9342
|
-
|
|
9350
|
+
canDeliver = this.beginDelivery(subscriptionId, subscription);
|
|
9343
9351
|
const row = await this.fetchEntityWithAuth(request.path, String(request.id), authContext);
|
|
9344
9352
|
if (canDeliver()) this.sendSingleUpdate(clientId, subscriptionId, row || null);
|
|
9345
9353
|
} catch (error) {
|
|
9346
|
-
|
|
9347
|
-
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9354
|
+
this.reportSocketFetchFailure(clientId, subscriptionId, request.path, error, canDeliver);
|
|
9348
9355
|
}
|
|
9349
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
|
+
}
|
|
9350
9375
|
async handleUnsubscribe(_clientId, subscriptionId) {
|
|
9351
9376
|
this._subscriptions.delete(subscriptionId);
|
|
9352
9377
|
this.subscriptionCallbacks.delete(subscriptionId);
|
|
@@ -9479,8 +9504,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9479
9504
|
const meta = await this.collectionMetaWithAuth(notifyPath, subscription.collectionRequest, rows, subscription.authContext);
|
|
9480
9505
|
if (canDeliver()) this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath, meta);
|
|
9481
9506
|
} catch (error) {
|
|
9482
|
-
|
|
9483
|
-
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9507
|
+
this.reportSocketFetchFailure(subscription.clientId, subscriptionId, notifyPath, error, canDeliver);
|
|
9484
9508
|
}
|
|
9485
9509
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9486
9510
|
}
|
|
@@ -9500,10 +9524,30 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9500
9524
|
if (canDeliver()) callback(rows);
|
|
9501
9525
|
} catch (error) {
|
|
9502
9526
|
logger.error(`❌ [RealtimeService] Error in debounced driver refetch for ${subscriptionId}`, { error });
|
|
9527
|
+
this.reportDriverFetchFailure(subscriptionId, subscription, canDeliver, error);
|
|
9503
9528
|
}
|
|
9504
9529
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9505
9530
|
}
|
|
9506
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
|
+
/**
|
|
9507
9551
|
* Fetch a collection with optional RLS auth context.
|
|
9508
9552
|
* When authContext is provided, the fetch runs inside a transaction
|
|
9509
9553
|
* with set_config calls so PostgreSQL RLS policies are enforced.
|
|
@@ -9659,8 +9703,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9659
9703
|
const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);
|
|
9660
9704
|
if (canDeliver()) this.sendSingleUpdate(subscription.clientId, subscriptionId, row || null);
|
|
9661
9705
|
} catch (error) {
|
|
9662
|
-
|
|
9663
|
-
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
9706
|
+
this.reportSocketFetchFailure(subscription.clientId, subscriptionId, notifyPath, error, canDeliver);
|
|
9664
9707
|
}
|
|
9665
9708
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9666
9709
|
}
|
|
@@ -9680,6 +9723,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9680
9723
|
if (canDeliver()) callback(row || null);
|
|
9681
9724
|
} catch (error) {
|
|
9682
9725
|
logger.error(`❌ [RealtimeService] Error in debounced row driver refetch for ${subscriptionId}`, { error });
|
|
9726
|
+
this.reportDriverFetchFailure(subscriptionId, subscription, canDeliver, error);
|
|
9683
9727
|
}
|
|
9684
9728
|
}, RealtimeService.REFETCH_DEBOUNCE_MS));
|
|
9685
9729
|
}
|
|
@@ -10802,6 +10846,31 @@ function extractErrorMessage(error) {
|
|
|
10802
10846
|
/**
|
|
10803
10847
|
* Check if the current session belongs to an admin user.
|
|
10804
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
|
+
}
|
|
10805
10874
|
function isAdminSession(session) {
|
|
10806
10875
|
if (!session?.user) return false;
|
|
10807
10876
|
if (session.user.isAdmin) return true;
|
|
@@ -10887,13 +10956,15 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
10887
10956
|
if (adapterUser) verifiedUser = {
|
|
10888
10957
|
uid: adapterUser.uid,
|
|
10889
10958
|
roles: adapterUser.roles,
|
|
10890
|
-
isAdmin: adapterUser.isAdmin
|
|
10959
|
+
isAdmin: adapterUser.isAdmin,
|
|
10960
|
+
isAnonymous: adapterUser.isAnonymous === true
|
|
10891
10961
|
};
|
|
10892
10962
|
} catch {}
|
|
10893
10963
|
else if (authConfig?.serviceKey && safeCompare(token, authConfig.serviceKey)) verifiedUser = {
|
|
10894
10964
|
uid: "service",
|
|
10895
10965
|
roles: ["admin"],
|
|
10896
|
-
isAdmin: true
|
|
10966
|
+
isAdmin: true,
|
|
10967
|
+
isAnonymous: false
|
|
10897
10968
|
};
|
|
10898
10969
|
else {
|
|
10899
10970
|
const jwtPayload = await extractUserFromToken(token);
|
|
@@ -10981,25 +11052,17 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
10981
11052
|
assertFieldOpsValid(values, collection);
|
|
10982
11053
|
};
|
|
10983
11054
|
const getScopedDelegate = async () => {
|
|
10984
|
-
const session = clientSessions.get(clientId);
|
|
10985
11055
|
if (typeof driver.withAuth === "function") try {
|
|
10986
|
-
const
|
|
10987
|
-
|
|
11056
|
+
const who = sessionAuthContext(clientSessions.get(clientId));
|
|
11057
|
+
const userForAuth = {
|
|
11058
|
+
uid: who.uid,
|
|
10988
11059
|
displayName: null,
|
|
10989
11060
|
email: null,
|
|
10990
11061
|
photoURL: null,
|
|
10991
11062
|
providerId: "websocket",
|
|
10992
|
-
isAnonymous:
|
|
10993
|
-
roles:
|
|
10994
|
-
claims:
|
|
10995
|
-
} : {
|
|
10996
|
-
uid: ANONYMOUS_USER_ID,
|
|
10997
|
-
displayName: null,
|
|
10998
|
-
email: null,
|
|
10999
|
-
photoURL: null,
|
|
11000
|
-
providerId: "websocket",
|
|
11001
|
-
isAnonymous: true,
|
|
11002
|
-
roles: ["anon"]
|
|
11063
|
+
isAnonymous: who.isAnonymous === true,
|
|
11064
|
+
roles: who.roles,
|
|
11065
|
+
...who.claims ? { claims: who.claims } : {}
|
|
11003
11066
|
};
|
|
11004
11067
|
return await driver.withAuth(userForAuth);
|
|
11005
11068
|
} catch (e) {
|
|
@@ -11070,7 +11133,13 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
11070
11133
|
wsDebug("🗑️ [WebSocket Server] Processing DELETE_ENTITY request");
|
|
11071
11134
|
const request = payload;
|
|
11072
11135
|
wsDebug("🗑️ [WebSocket Server] Deleting row:", request.row);
|
|
11073
|
-
await (await getScopedDelegate()).delete(
|
|
11136
|
+
await (await getScopedDelegate()).delete({
|
|
11137
|
+
row: {
|
|
11138
|
+
id: request.row.id,
|
|
11139
|
+
path: request.row.path
|
|
11140
|
+
},
|
|
11141
|
+
hard: request.hard
|
|
11142
|
+
});
|
|
11074
11143
|
wsDebug("🗑️ [WebSocket Server] DELETE_ENTITY completed successfully");
|
|
11075
11144
|
const response = {
|
|
11076
11145
|
type: "DELETE_SUCCESS",
|
|
@@ -11296,17 +11365,7 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
11296
11365
|
case "presence_state":
|
|
11297
11366
|
case "channel_history": {
|
|
11298
11367
|
wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
|
|
11299
|
-
const
|
|
11300
|
-
const authContext = session?.user ? {
|
|
11301
|
-
uid: session.user.uid,
|
|
11302
|
-
roles: session.user.roles ?? [],
|
|
11303
|
-
isAnonymous: session.user.isAnonymous === true,
|
|
11304
|
-
claims: session.user.claims
|
|
11305
|
-
} : {
|
|
11306
|
-
uid: ANONYMOUS_USER_ID,
|
|
11307
|
-
roles: ["anon"],
|
|
11308
|
-
isAnonymous: false
|
|
11309
|
-
};
|
|
11368
|
+
const authContext = sessionAuthContext(clientSessions.get(clientId));
|
|
11310
11369
|
await realtimeService.handleClientMessage(clientId, {
|
|
11311
11370
|
type,
|
|
11312
11371
|
payload,
|
|
@@ -11342,15 +11401,18 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
11342
11401
|
}));
|
|
11343
11402
|
return;
|
|
11344
11403
|
}
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
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);
|
|
11348
11409
|
ws.send(JSON.stringify({
|
|
11349
11410
|
type: "ERROR",
|
|
11350
11411
|
requestId,
|
|
11351
11412
|
payload: { error: {
|
|
11352
|
-
message:
|
|
11353
|
-
code:
|
|
11413
|
+
message: answer.message,
|
|
11414
|
+
code: answer.code,
|
|
11415
|
+
...answer.details !== void 0 && { details: answer.details }
|
|
11354
11416
|
} }
|
|
11355
11417
|
}));
|
|
11356
11418
|
return;
|
|
@@ -11497,10 +11559,32 @@ function parseOptionalInt(value) {
|
|
|
11497
11559
|
* Create a {@link CronJobDefinition} that dumps the database, uploads the
|
|
11498
11560
|
* result to the configured destination, and prunes old backups. Object
|
|
11499
11561
|
* destinations require {@link BackupCronConfig.storage}.
|
|
11562
|
+
*
|
|
11563
|
+
* The dump excludes nothing unless `excludeSchemas` says otherwise, and that
|
|
11564
|
+
* default is not this function's to set: the option goes to `createDump`
|
|
11565
|
+
* untouched, so `buildPgDumpArgs` decides for the cron and `rebase db backup`
|
|
11566
|
+
* alike. The cron used to carry its own default of `["rebase"]`, meant to
|
|
11567
|
+
* leave out Atlas's revision table, and it left out a lot more. `rebase` is
|
|
11568
|
+
* where the framework keeps
|
|
11569
|
+
* everything that is not a collection: the auth tables (`users`, identities,
|
|
11570
|
+
* refresh tokens, MFA factors, recovery codes, `app_config`, `schema_meta`),
|
|
11571
|
+
* API keys, record history, the job queue, cron logs, channel history,
|
|
11572
|
+
* branches and idempotency keys. It also holds the functions that every
|
|
11573
|
+
* generated RLS policy and CDC trigger calls (`rebase.uid()`,
|
|
11574
|
+
* `rebase.roles()`, `rebase.rebase_cdc_notify()`).
|
|
11575
|
+
*
|
|
11576
|
+
* None of that can be left for boot to rebuild. Boot recreates the tables
|
|
11577
|
+
* and functions, but never the rows in them. And a dump that has a policy
|
|
11578
|
+
* or trigger on a `public` table without the function it calls cannot be
|
|
11579
|
+
* restored into an empty database: `pg_restore --exit-on-error` stops at
|
|
11580
|
+
* the first such statement, and every collection's generated policies call
|
|
11581
|
+
* `rebase.uid()`. So every scheduled backup lost every user account, and
|
|
11582
|
+
* could not be restored anyway, while `rebase db backup` took a complete
|
|
11583
|
+
* one. The revision table belongs in a backup too: without it, a restored
|
|
11584
|
+
* database has tables but no record of the migrations that made them.
|
|
11500
11585
|
*/
|
|
11501
11586
|
function createBackupCron(config) {
|
|
11502
11587
|
const dbName = parseDbNameFromUrl(config.connectionString) ?? "database";
|
|
11503
|
-
const excludeSchemas = config.excludeSchemas ?? ["rebase"];
|
|
11504
11588
|
return {
|
|
11505
11589
|
name: config.name ?? "Scheduled database backup",
|
|
11506
11590
|
schedule: config.schedule,
|
|
@@ -11517,7 +11601,7 @@ function createBackupCron(config) {
|
|
|
11517
11601
|
connectionString: config.connectionString,
|
|
11518
11602
|
dbName,
|
|
11519
11603
|
outDir,
|
|
11520
|
-
excludeSchemas
|
|
11604
|
+
excludeSchemas: config.excludeSchemas
|
|
11521
11605
|
});
|
|
11522
11606
|
log(`Dump created: ${dump.fileName} (${formatBytes(dump.sizeBytes)})`);
|
|
11523
11607
|
const check = await validateDump(dump.localFile);
|
|
@@ -15055,7 +15139,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
15055
15139
|
* proved it can bootstrap with.
|
|
15056
15140
|
*/
|
|
15057
15141
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
15058
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
15142
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-Cr2ye5JC.js").then((n) => n.n);
|
|
15059
15143
|
const plan = await ensureCollectionTables(provisioningQueryable(driverResult), collections, log);
|
|
15060
15144
|
for (const failure of plan.failures) logger.warn(describeEnsureFailure(failure));
|
|
15061
15145
|
return { applied: plan.actions.length - plan.failures.length };
|
|
@@ -15089,7 +15173,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
15089
15173
|
await provisionRlsRuntime(provisioningQueryable(driverResult));
|
|
15090
15174
|
},
|
|
15091
15175
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
15092
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
15176
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-B4IDFtQ-.js");
|
|
15093
15177
|
const queryable = provisioningQueryable(driverResult);
|
|
15094
15178
|
const { rows } = await queryable.query("SELECT to_regprocedure('rebase.roles()') IS NOT NULL AS present");
|
|
15095
15179
|
if (!rows[0]?.present) await provisionRlsRuntime(queryable);
|