velocious 1.0.600 → 1.0.602
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 +9 -11
- package/build/configuration-types.js +2 -2
- package/build/configuration.js +7 -5
- package/build/database/live-query.js +4 -1
- package/build/database/operation.js +9 -0
- package/build/database/record/index.js +50 -2
- package/build/database/record-changes.js +18 -4
- package/build/frontend-model-controller.js +16 -20
- package/build/src/configuration-types.d.ts +4 -4
- package/build/src/configuration-types.js +3 -3
- package/build/src/configuration.d.ts +2 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +8 -6
- package/build/src/database/live-query.d.ts +5 -0
- package/build/src/database/live-query.d.ts.map +1 -1
- package/build/src/database/live-query.js +4 -2
- package/build/src/database/operation.d.ts +5 -0
- package/build/src/database/operation.d.ts.map +1 -1
- package/build/src/database/operation.js +9 -1
- package/build/src/database/record/index.d.ts +36 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +43 -3
- package/build/src/database/record-changes.d.ts +9 -1
- package/build/src/database/record-changes.d.ts.map +1 -1
- package/build/src/database/record-changes.js +18 -5
- package/build/src/frontend-model-controller.d.ts +1 -4
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +16 -20
- package/build/src/sync/local-mutation-log.d.ts +7 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +17 -1
- package/build/src/sync/sync-api-client-types.d.ts +8 -4
- package/build/src/sync/sync-api-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-api-client-types.js +3 -3
- package/build/src/sync/sync-client-types.d.ts +25 -0
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +6 -1
- package/build/src/sync/sync-client.d.ts +43 -1
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +187 -37
- package/build/src/sync/sync-realtime-bridge.js +2 -2
- package/build/src/sync/sync-scope-store.d.ts +15 -1
- package/build/src/sync/sync-scope-store.d.ts.map +1 -1
- package/build/src/sync/sync-scope-store.js +31 -4
- package/build/src/tenants/tenant-handle.d.ts +81 -0
- package/build/src/tenants/tenant-handle.d.ts.map +1 -1
- package/build/src/tenants/tenant-handle.js +143 -1
- package/build/sync/local-mutation-log.js +17 -0
- package/build/sync/sync-api-client-types.js +2 -2
- package/build/sync/sync-client-types.js +5 -0
- package/build/sync/sync-client.js +198 -38
- package/build/sync/sync-realtime-bridge.js +1 -1
- package/build/sync/sync-scope-store.js +32 -3
- package/build/tenants/tenant-handle.js +165 -0
- package/package.json +4 -4
- package/src/configuration-types.js +2 -2
- package/src/configuration.js +7 -5
- package/src/database/live-query.js +4 -1
- package/src/database/operation.js +9 -0
- package/src/database/record/index.js +50 -2
- package/src/database/record-changes.js +18 -4
- package/src/frontend-model-controller.js +16 -20
- package/src/sync/local-mutation-log.js +17 -0
- package/src/sync/sync-api-client-types.js +2 -2
- package/src/sync/sync-client-types.js +5 -0
- package/src/sync/sync-client.js +198 -38
- package/src/sync/sync-realtime-bridge.js +1 -1
- package/src/sync/sync-scope-store.js +32 -3
- package/src/tenants/tenant-handle.js +165 -0
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Controllers and views for HTTP endpoints
|
|
16
16
|
* Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses and one-budget WebSocket startup controls (see [docs/frontend-models.md](docs/frontend-models.md) and [docs/websocket-channels.md](docs/websocket-channels.md))
|
|
17
17
|
* Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see the [shared-resource sync developer guide](docs/shared-resource-sync-guide.md) and [offline sync architecture](docs/offline-sync.md))
|
|
18
|
-
* Declarative client sync scopes with per-scope cursors, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery
|
|
19
|
-
* Reactive `useLiveQuery(Model.where(...))` queries
|
|
18
|
+
* Declarative client sync scopes with per-scope cursors, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery, and immutable-handle project clients whose queues, scopes, cursors, receipts, conflicts, pulls, and reconnect catch-up stay in one physical tenant database (see [docs/sync-client.md](docs/sync-client.md) and [docs/offline-sync.md](docs/offline-sync.md))
|
|
19
|
+
* Reactive `useLiveQuery(Model.where(...))` queries for default databases plus immutable-handle tenant live-query sources whose committed events and refreshes stay on the captured physical tenant (see [docs/live-queries.md](docs/live-queries.md))
|
|
20
20
|
* Server-side sync envelope replay orchestration for app-owned sync receivers, including allowlisted authoritative values for conflict resolution (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
|
|
21
21
|
* Self-sustaining sync feeds: upstream imports triggered by the changes pull itself, with framework-owned coalescing and throttling (see [docs/sync-upstream-imports.md](docs/sync-upstream-imports.md))
|
|
22
22
|
* AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see the [developer guide](docs/shared-resource-sync-guide.md) and [proof](docs/awesome-tasks-offline-sync-proof.md))
|
|
@@ -775,22 +775,20 @@ Use `await FrontendModelBase.waitForIdle()` when a test harness or app lifecycle
|
|
|
775
775
|
|
|
776
776
|
Frontend-model HTTP requests always use `credentials: "include"` so shared custom commands can set session cookies without app-level transport overrides.
|
|
777
777
|
|
|
778
|
-
Unexpected frontend-model endpoint failures return their original message by default
|
|
778
|
+
Unexpected frontend-model endpoint failures return their original message and full stack trace by default in every environment, including production. Responses use `errorType: "internal_error"`, a server-generated `correlationId` shared with the matching framework-error report, and the established `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields. Expected application failures can use `VelociousError.safe(message, {errorType, details, code})`; generated frontend-model callers preserve the server's safe error fields without adding irrelevant debug fields. See [docs/frontend-models.md](docs/frontend-models.md#error-payloads).
|
|
779
779
|
Invalid client query descriptors, such as unknown `select`, `where`, `search`, `joins`, `preload`, `group`, `sort`, `pluck`, or Ransack attributes, return the specific frontend-model query error message with `velocious.code: "frontend-model-query-error"` and are not emitted as framework errors.
|
|
780
780
|
Invalid frontend-model write attributes and attachment names, including attributes rejected by `permittedParams()`, return the specific safe error message with `velocious.code: "frontend-model-attribute-error"` and are not emitted as framework errors.
|
|
781
|
-
|
|
782
|
-
Other non-production environments, such as `staging`, keep the same client-safe default unless you explicitly opt in with `exposeInternalErrorsToClients: true`:
|
|
781
|
+
To mask unexpected internal details, explicitly opt out for the application configuration:
|
|
783
782
|
|
|
784
783
|
```js
|
|
785
784
|
const configuration = new Configuration({
|
|
786
|
-
|
|
787
|
-
exposeInternalErrorsToClients: true
|
|
785
|
+
exposeInternalErrorsToClients: false
|
|
788
786
|
})
|
|
789
787
|
```
|
|
790
788
|
|
|
791
|
-
|
|
789
|
+
With this opt-out, built-in commands, custom commands, and sync replay failures return `errorMessage: "Request failed."` and omit the debug message and stack fields in every environment. `secureFrontendModelErrors: true` remains a deprecated compatibility alias when `exposeInternalErrorsToClients` is omitted; an explicit `exposeInternalErrorsToClients` value always wins.
|
|
792
790
|
|
|
793
|
-
Backends can append client-safe metadata to frontend-model error responses with `configuration.addClientErrorPayloadReporter(...)`. Reporters receive the caught `error`, the current `request`, a safe `requestDetails` snapshot, and a small `context` object, and should only return fields that are safe for clients to see. Frontend-model endpoint failures include `context.frontendModelEndpoint`, `action`, `commandType`, `model`, `requestId`, and `expectedError`. This is useful for attaching an error-reporting URL while keeping
|
|
791
|
+
Backends can append client-safe metadata to frontend-model error responses with `configuration.addClientErrorPayloadReporter(...)`. Reporters receive the caught `error`, the current `request`, a safe `requestDetails` snapshot, and a small `context` object, and should only return fields that are safe for clients to see. Frontend-model endpoint failures include `context.frontendModelEndpoint`, `action`, `commandType`, `model`, `requestId`, and `expectedError`. When exposure is disabled, Velocious strips the established debug fields even if a reporter supplies them. This is useful for attaching an error-reporting URL while keeping an opted-out error message generic:
|
|
794
792
|
|
|
795
793
|
```js
|
|
796
794
|
configuration.addClientErrorPayloadReporter(async ({error, requestDetails, context}) => {
|
|
@@ -1919,7 +1917,7 @@ configuration.getErrorEvents().on("all-error", ({error, errorType}) => {
|
|
|
1919
1917
|
})
|
|
1920
1918
|
```
|
|
1921
1919
|
|
|
1922
|
-
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return an `internal_error` response with the original message by default (or `Request failed.` when `
|
|
1920
|
+
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return an `internal_error` response with the original message and stack trace by default (or `Request failed.` without debug fields when `exposeInternalErrorsToClients: false`) and a correlation ID, then emits them as `framework-error`/`all-error` with the same correlation ID and `context.frontendModelEndpoint === true`. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` errors keep their expected-error status without irrelevant debug fields. A raw `errorType` property alone is not considered safe and does not suppress reporting.
|
|
1923
1921
|
|
|
1924
1922
|
Unexpected inbound decoded WebSocket dispatch failures emit one `framework-error` and one matching `all-error`. Established expected client-flow errors remain excluded from both events.
|
|
1925
1923
|
|
|
@@ -2689,7 +2687,7 @@ Tenant lifecycle commands print start and final counts, report each completed te
|
|
|
2689
2687
|
|
|
2690
2688
|
`afterMigrateTenant` hooks run inside the active default and tenant database connection scope for the tenant being migrated.
|
|
2691
2689
|
|
|
2692
|
-
At runtime, the apartment-style `Tenant` façade (`velocious/build/src/tenants/tenant.js`) is the single entry point: `Tenant.with(tenant, callback)` / `Tenant.current()` to switch into and read a Node async context, `Tenant.handle(tenant)` to deeply capture immutable application and physical database identity for overlapping browser/native work, `Tenant.each({identifier, callback, parallel?, filter?})` to run a callback within every provider-listed tenant, and `Tenant.drop({identifier, tenant})` (plus the `db:tenants:drop` CLI command) to drop a tenant's database through the provider's `dropDatabase` hook. SQLite handles additionally expose framework-owned `open`, `flush`, `close`, `delete`, `inspect`, and `withPin` lifecycle methods; `frontendTenantSqlite.maxOpenHandles` bounds resident identities and clean, idle, unpinned handles are evicted least-recently-used. `Tenant.handle(...).databaseOperation(...)` and `.transaction(...)` use bounded pool-owned checkouts and pin model/query/write/association/preload/audit/attachment/raw work to the captured database even if a later UI project switch changes ambient tenant state. `Model.usingTenant(tenant)` uses the same safe core and adds eager helpers plus general `databaseOperation`/`transaction` model callbacks; eager records preserve legacy ambient Node follow-up semantics, while browser/native follow-up database work belongs inside the callback APIs. Inactive identifiers, mixed physical tenants, unsupported/cyclic descriptors,
|
|
2690
|
+
At runtime, the apartment-style `Tenant` façade (`velocious/build/src/tenants/tenant.js`) is the single entry point: `Tenant.with(tenant, callback)` / `Tenant.current()` to switch into and read a Node async context, `Tenant.handle(tenant)` to deeply capture immutable application and physical database identity for overlapping browser/native work, `Tenant.each({identifier, callback, parallel?, filter?})` to run a callback within every provider-listed tenant, and `Tenant.drop({identifier, tenant})` (plus the `db:tenants:drop` CLI command) to drop a tenant's database through the provider's `dropDatabase` hook. SQLite handles additionally expose framework-owned `open`, `flush`, `close`, `delete`, `inspect`, and `withPin` lifecycle methods; `frontendTenantSqlite.maxOpenHandles` bounds resident identities and clean, idle, unpinned handles are evicted least-recently-used. `Tenant.handle(...).databaseOperation(...)` and `.transaction(...)` use bounded pool-owned checkouts and pin model/query/write/association/preload/audit/attachment/raw work to the captured database even if a later UI project switch changes ambient tenant state. The handle also builds tenant-bound live-query sources, exposes an opaque physical identity for filtered record-change subscriptions, and binds project `SyncClient` instances. `Model.usingTenant(tenant)` uses the same safe core and adds eager helpers plus general `databaseOperation`/`transaction` model callbacks; eager records preserve legacy ambient Node follow-up semantics, while browser/native follow-up database work belongs inside the callback APIs. Inactive identifiers, mixed physical tenants, unsupported/cyclic descriptors, expired operations, unscoped tenant event subscriptions, and stale/cross-tenant sync state fail closed. `Tenant.with` and `Tenant.each` retain their connection-establishing and model-initializing ambient behavior for Node request/job flows. `Tenant.aggregateAcross({identifier, aggregates, keyColumns, subquery, tenants?, filter?})` runs one aggregate over the same table across many tenant databases and returns the merged result — grouping tenants by server and using a single cross-database `UNION ALL` where the driver supports two-part `` `database`.`table` `` references (MySQL/MariaDB) or one query per tenant otherwise (PostgreSQL/SQLite/MSSQL).
|
|
2693
2691
|
|
|
2694
2692
|
`SchemaCloner` adds a missing auto-increment column and its separate source unique index in one schema alteration, including on MySQL/MariaDB where an auto-increment column must be keyed when it is created.
|
|
2695
2693
|
|
|
@@ -697,9 +697,9 @@
|
|
|
697
697
|
* @property {boolean} [enforceTenantDatabaseScopes] - Require tenant-switched model queries to resolve a tenant database identifier. Defaults to true.
|
|
698
698
|
* @property {string} [environment] - Current environment name.
|
|
699
699
|
* @property {import("./environment-handlers/base.js").default} environmentHandler - Environment handler instance.
|
|
700
|
-
* @property {boolean} [exposeInternalErrorsToClients] - Return unexpected internal error
|
|
700
|
+
* @property {boolean} [exposeInternalErrorsToClients] - Return unexpected internal error messages and stack traces in frontend-model client payloads in every environment. Defaults to true.
|
|
701
701
|
* @property {{maxOpenHandles?: number}} [frontendTenantSqlite] - Bounded frontend tenant SQLite lifecycle configuration.
|
|
702
|
-
* @property {boolean} [secureFrontendModelErrors] -
|
|
702
|
+
* @property {boolean} [secureFrontendModelErrors] - Deprecated compatibility alias for `exposeInternalErrorsToClients: false` when the authoritative option is omitted.
|
|
703
703
|
* @property {HttpServerConfiguration} [httpServer] - Default HTTP server configuration for applications started from this configuration.
|
|
704
704
|
* @property {LoggingConfiguration} [logging] - Logging configuration.
|
|
705
705
|
* @property {BackgroundJobsConfiguration} [backgroundJobs] - Background jobs configuration.
|
package/build/configuration.js
CHANGED
|
@@ -236,7 +236,7 @@ export default class VelociousConfiguration {
|
|
|
236
236
|
* Runs constructor.
|
|
237
237
|
* @param {import("./configuration-types.js").ConfigurationArgsType} args - Configuration arguments.
|
|
238
238
|
*/
|
|
239
|
-
constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients
|
|
239
|
+
constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients, frontendTenantSqlite, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
|
|
240
240
|
restArgsError(restArgs)
|
|
241
241
|
|
|
242
242
|
this._abilityResolver = abilityResolver
|
|
@@ -283,8 +283,9 @@ export default class VelociousConfiguration {
|
|
|
283
283
|
this._environment = environment || globalThis.process?.env.VELOCIOUS_ENV || globalThis.process?.env.NODE_ENV || "development"
|
|
284
284
|
this._environmentHandler = environmentHandler
|
|
285
285
|
this._enforceTenantDatabaseScopes = enforceTenantDatabaseScopes
|
|
286
|
-
this._exposeInternalErrorsToClients = exposeInternalErrorsToClients
|
|
287
|
-
|
|
286
|
+
this._exposeInternalErrorsToClients = exposeInternalErrorsToClients === undefined
|
|
287
|
+
? secureFrontendModelErrors !== true
|
|
288
|
+
: exposeInternalErrorsToClients
|
|
288
289
|
this._directory = directory
|
|
289
290
|
this._initializeModels = initializeModels
|
|
290
291
|
/** @type {VelociousPackage[]} */
|
|
@@ -461,9 +462,10 @@ export default class VelociousConfiguration {
|
|
|
461
462
|
|
|
462
463
|
/**
|
|
463
464
|
* Returns whether frontend-model errors expose only explicitly safe messages.
|
|
464
|
-
* @
|
|
465
|
+
* @deprecated Use `getExposeInternalErrorsToClients()`.
|
|
466
|
+
* @returns {boolean} Whether frontend-model internal error exposure is disabled.
|
|
465
467
|
*/
|
|
466
|
-
getSecureFrontendModelErrors() { return this.
|
|
468
|
+
getSecureFrontendModelErrors() { return !this.getExposeInternalErrorsToClients() }
|
|
467
469
|
|
|
468
470
|
/**
|
|
469
471
|
* Runs get debug endpoint.
|
|
@@ -16,6 +16,7 @@ import restArgsError from "../utils/rest-args-error.js"
|
|
|
16
16
|
* @template T
|
|
17
17
|
* @typedef {object} LiveQuerySource
|
|
18
18
|
* @property {() => RecordModelClass} getModelClass - Root model class the query reads.
|
|
19
|
+
* @property {(modelClass: RecordModelClass) => string} [databaseIdentityForModel] - Captured physical identity for an observed model.
|
|
19
20
|
* @property {() => Promise<T[]>} toArray - Runs the query and resolves the current rows.
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -117,7 +118,9 @@ class LiveQuery {
|
|
|
117
118
|
this._started = true
|
|
118
119
|
|
|
119
120
|
for (const modelClass of this._modelClasses) {
|
|
120
|
-
this.
|
|
121
|
+
const databaseIdentity = this._query.databaseIdentityForModel?.(modelClass)
|
|
122
|
+
|
|
123
|
+
this._unsubscribes.push(recordChanges.subscribe(modelClass, this._onRecordChange, {databaseIdentity}))
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
this._run()
|
|
@@ -127,6 +127,7 @@ export default class VelociousDatabaseOperation {
|
|
|
127
127
|
this.assertActive()
|
|
128
128
|
this.assertModel(record.getModelClass())
|
|
129
129
|
record.bindDatabaseOperation(this)
|
|
130
|
+
record.captureDatabaseIdentity(this.databaseIdentity())
|
|
130
131
|
|
|
131
132
|
return record
|
|
132
133
|
}
|
|
@@ -203,6 +204,14 @@ export default class VelociousDatabaseOperation {
|
|
|
203
204
|
return `${this._databaseIdentifier}:${this._configurationReuseKey}`
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Returns the tenant schema generation captured when this operation started.
|
|
209
|
+
* @returns {string | undefined} - Captured schema generation.
|
|
210
|
+
*/
|
|
211
|
+
schemaGeneration() {
|
|
212
|
+
return this._schemaGeneration
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
/**
|
|
207
216
|
* Initializes a model through this operation's captured connection.
|
|
208
217
|
* @param {typeof import("./record/index.js").default} ModelClass - Model class.
|
|
@@ -3618,6 +3618,29 @@ class VelociousDatabaseRecord {
|
|
|
3618
3618
|
return this
|
|
3619
3619
|
}
|
|
3620
3620
|
|
|
3621
|
+
/**
|
|
3622
|
+
* Captures and validates the physical database identity that owns this record.
|
|
3623
|
+
* @param {string} databaseIdentity - Opaque operation/connection identity.
|
|
3624
|
+
* @returns {this} This record.
|
|
3625
|
+
*/
|
|
3626
|
+
captureDatabaseIdentity(databaseIdentity) {
|
|
3627
|
+
if (this._databaseIdentity && this._databaseIdentity !== databaseIdentity) {
|
|
3628
|
+
throw new Error("Record belongs to a different physical tenant database")
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
this._databaseIdentity = databaseIdentity
|
|
3632
|
+
|
|
3633
|
+
return this
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
/**
|
|
3637
|
+
* Returns the captured physical database identity.
|
|
3638
|
+
* @returns {string | undefined} Captured physical database identity.
|
|
3639
|
+
*/
|
|
3640
|
+
databaseIdentity() {
|
|
3641
|
+
return this._databaseIdentity
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3621
3644
|
/**
|
|
3622
3645
|
* Releases this record from a completed eager-helper operation while
|
|
3623
3646
|
* preserving the legacy ambient follow-up behavior of `usingTenant` finders.
|
|
@@ -3742,7 +3765,27 @@ class VelociousDatabaseRecord {
|
|
|
3742
3765
|
if (this._databaseOperation) return this._databaseOperation.connection()
|
|
3743
3766
|
if (this.__connection) return this.__connection
|
|
3744
3767
|
|
|
3745
|
-
|
|
3768
|
+
const connection = this.getModelClass().connection()
|
|
3769
|
+
|
|
3770
|
+
if (this._databaseIdentity) this.captureDatabaseIdentity(this._databaseIdentityForConnection(connection))
|
|
3771
|
+
|
|
3772
|
+
return connection
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
/**
|
|
3776
|
+
* Resolves the identity of an already selected concrete connection.
|
|
3777
|
+
* @param {import("../drivers/base.js").default} connection - Concrete connection.
|
|
3778
|
+
* @returns {string} Physical database identity.
|
|
3779
|
+
*/
|
|
3780
|
+
_databaseIdentityForConnection(connection) {
|
|
3781
|
+
const modelClass = this.getModelClass()
|
|
3782
|
+
const databaseIdentifier = modelClass.getDatabaseIdentifier()
|
|
3783
|
+
const reuseKey = modelClass
|
|
3784
|
+
._getConfiguration()
|
|
3785
|
+
.getDatabasePool(databaseIdentifier)
|
|
3786
|
+
.getConnectionConfigurationReuseKey(connection)
|
|
3787
|
+
|
|
3788
|
+
return `${databaseIdentifier}:${reuseKey}`
|
|
3746
3789
|
}
|
|
3747
3790
|
|
|
3748
3791
|
/**
|
|
@@ -3993,9 +4036,14 @@ class VelociousDatabaseRecord {
|
|
|
3993
4036
|
if (!recordChanges.hasListeners(modelClass)) return
|
|
3994
4037
|
|
|
3995
4038
|
const record = this
|
|
4039
|
+
const databaseIdentity = this._databaseOperation
|
|
4040
|
+
? this._databaseOperation.databaseIdentity()
|
|
4041
|
+
: this._databaseIdentityForConnection(this._connection())
|
|
4042
|
+
|
|
4043
|
+
this.captureDatabaseIdentity(databaseIdentity)
|
|
3996
4044
|
|
|
3997
4045
|
await this._connection().afterCommit(() => {
|
|
3998
|
-
recordChanges.emit({modelClass, operation, record})
|
|
4046
|
+
recordChanges.emit({databaseIdentity, modelClass, operation, record})
|
|
3999
4047
|
})
|
|
4000
4048
|
}
|
|
4001
4049
|
|
|
@@ -14,6 +14,7 @@ import EventEmitter from "../utils/event-emitter.js"
|
|
|
14
14
|
* RecordChangeEvent type.
|
|
15
15
|
* @typedef {object} RecordChangeEvent
|
|
16
16
|
* @property {RecordModelClass} modelClass - Model class whose row changed.
|
|
17
|
+
* @property {string} databaseIdentity - Opaque physical database identity where the commit occurred.
|
|
17
18
|
* @property {RecordChangeOperation} operation - The committed operation.
|
|
18
19
|
* @property {InstanceType<RecordModelClass>} record - The committed record instance.
|
|
19
20
|
*/
|
|
@@ -49,15 +50,25 @@ class RecordChanges {
|
|
|
49
50
|
* Subscribes a listener to committed changes of a model class.
|
|
50
51
|
* @param {RecordModelClass} modelClass - Model class to observe.
|
|
51
52
|
* @param {RecordChangeListener} listener - Listener called with each change event.
|
|
53
|
+
* @param {{databaseIdentity?: string}} [options] - Captured physical identity filter.
|
|
52
54
|
* @returns {() => void} Unsubscribe callback.
|
|
53
55
|
*/
|
|
54
|
-
subscribe(modelClass, listener) {
|
|
56
|
+
subscribe(modelClass, listener, {databaseIdentity} = {}) {
|
|
57
|
+
if (modelClass.hasTenantDatabaseIdentifierResolver() && !databaseIdentity) {
|
|
58
|
+
throw new Error(`Tenant-scoped record-change subscriptions for ${modelClass.getModelName()} require a captured databaseIdentity`)
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
const eventName = modelClass.getModelName()
|
|
62
|
+
const subscribedListener = databaseIdentity
|
|
63
|
+
? (/** @type {RecordChangeEvent} */ event) => {
|
|
64
|
+
if (event.databaseIdentity === databaseIdentity) listener(event)
|
|
65
|
+
}
|
|
66
|
+
: listener
|
|
56
67
|
|
|
57
|
-
this._emitter.on(eventName,
|
|
68
|
+
this._emitter.on(eventName, subscribedListener)
|
|
58
69
|
|
|
59
70
|
return () => {
|
|
60
|
-
this._emitter.off(eventName,
|
|
71
|
+
this._emitter.off(eventName, subscribedListener)
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
|
|
@@ -81,7 +92,10 @@ class RecordChanges {
|
|
|
81
92
|
*/
|
|
82
93
|
emit(event) {
|
|
83
94
|
if (this._batchDepth > 0) {
|
|
84
|
-
|
|
95
|
+
const modelName = event.modelClass.getModelName()
|
|
96
|
+
const eventKey = `${modelName.length}:${modelName}:${event.databaseIdentity}`
|
|
97
|
+
|
|
98
|
+
this._bufferedEvents.set(eventKey, event)
|
|
85
99
|
|
|
86
100
|
return
|
|
87
101
|
}
|
|
@@ -175,7 +175,6 @@ const frontendModelJoinedPathsSymbol = Symbol("frontendModelJoinedPaths")
|
|
|
175
175
|
const frontendModelGroupedColumnsSymbol = Symbol("frontendModelGroupedColumns")
|
|
176
176
|
const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
|
|
177
177
|
const frontendModelClientSafeErrorMessage = "Request failed."
|
|
178
|
-
const frontendModelDebugErrorEnvironments = new Set(["development", "test"])
|
|
179
178
|
|
|
180
179
|
/**
|
|
181
180
|
* Builds a client-safe sync replay validation error.
|
|
@@ -277,10 +276,10 @@ function frontendModelVelociousMetadataForError(error) {
|
|
|
277
276
|
/**
|
|
278
277
|
* Runs frontend model client message for error.
|
|
279
278
|
* @param {unknown} error - Caught error.
|
|
280
|
-
* @param {boolean}
|
|
279
|
+
* @param {boolean} exposeInternalErrorsToClients - Whether unexpected error messages may be exposed.
|
|
281
280
|
* @returns {string} - Message safe to return to API clients.
|
|
282
281
|
*/
|
|
283
|
-
function frontendModelClientMessageForError(error,
|
|
282
|
+
function frontendModelClientMessageForError(error, exposeInternalErrorsToClients) {
|
|
284
283
|
if (error instanceof RecordNotFoundError) {
|
|
285
284
|
return "Record not found."
|
|
286
285
|
}
|
|
@@ -301,7 +300,7 @@ function frontendModelClientMessageForError(error, forwardUnexpectedErrorMessage
|
|
|
301
300
|
return error.message
|
|
302
301
|
}
|
|
303
302
|
|
|
304
|
-
if (
|
|
303
|
+
if (exposeInternalErrorsToClients && error instanceof Error) return error.message
|
|
305
304
|
|
|
306
305
|
return frontendModelClientSafeErrorMessage
|
|
307
306
|
}
|
|
@@ -310,14 +309,11 @@ function frontendModelClientMessageForError(error, forwardUnexpectedErrorMessage
|
|
|
310
309
|
* Runs frontend model debug payload for error.
|
|
311
310
|
* @param {object} args - Arguments.
|
|
312
311
|
* @param {import("./configuration.js").default} args.configuration - Current configuration.
|
|
313
|
-
* @param {string} args.environment - Current environment.
|
|
314
312
|
* @param {unknown} args.error - Caught error.
|
|
315
|
-
* @returns {import("./configuration-types.js").ClientErrorPayloadReporterPayload} - Optional
|
|
313
|
+
* @returns {import("./configuration-types.js").ClientErrorPayloadReporterPayload} - Optional internal error details when client exposure is enabled.
|
|
316
314
|
*/
|
|
317
|
-
function frontendModelDebugPayloadForError({configuration,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (!debugAllowed) {
|
|
315
|
+
function frontendModelDebugPayloadForError({configuration, error}) {
|
|
316
|
+
if (!configuration.getExposeInternalErrorsToClients()) {
|
|
321
317
|
return {}
|
|
322
318
|
}
|
|
323
319
|
|
|
@@ -3259,10 +3255,9 @@ export default class FrontendModelController extends Controller {
|
|
|
3259
3255
|
* Runs frontend model client error payload for error.
|
|
3260
3256
|
* @param {unknown} error - Caught error.
|
|
3261
3257
|
* @param {FrontendModelEndpointErrorContext | undefined} [endpointErrorContext] - Frontend-model endpoint error context.
|
|
3262
|
-
* @param {{forwardUnexpectedErrorMessage?: boolean}} [options] - Client error rendering options.
|
|
3263
3258
|
* @returns {Promise<import("./configuration-types.js").ClientErrorPayloadReporterPayload>} - Client payload for the current environment.
|
|
3264
3259
|
*/
|
|
3265
|
-
async frontendModelClientErrorPayloadForError(error, endpointErrorContext
|
|
3260
|
+
async frontendModelClientErrorPayloadForError(error, endpointErrorContext) {
|
|
3266
3261
|
const velociousMetadata = frontendModelVelociousMetadataForError(error)
|
|
3267
3262
|
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
3268
3263
|
/** @type {import("./configuration-types.js").ClientErrorPayloadReporterPayload} */
|
|
@@ -3312,15 +3307,20 @@ export default class FrontendModelController extends Controller {
|
|
|
3312
3307
|
request: this.getRequest()
|
|
3313
3308
|
})
|
|
3314
3309
|
|
|
3310
|
+
if (!this.getConfiguration().getExposeInternalErrorsToClients()) {
|
|
3311
|
+
delete reporterPayload.debugBacktrace
|
|
3312
|
+
delete reporterPayload.debugErrorClass
|
|
3313
|
+
delete reporterPayload.debugErrorMessage
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3315
3316
|
return {
|
|
3316
3317
|
...reporterPayload,
|
|
3317
3318
|
...this.frontendModelErrorPayload(frontendModelClientMessageForError(
|
|
3318
3319
|
error,
|
|
3319
|
-
|
|
3320
|
+
this.getConfiguration().getExposeInternalErrorsToClients()
|
|
3320
3321
|
)),
|
|
3321
3322
|
...frontendModelDebugPayloadForError({
|
|
3322
3323
|
configuration: this.getConfiguration(),
|
|
3323
|
-
environment: this.getConfiguration().getEnvironment(),
|
|
3324
3324
|
error
|
|
3325
3325
|
}),
|
|
3326
3326
|
...(velociousMetadata ? {velocious: velociousMetadata} : {}),
|
|
@@ -4397,9 +4397,7 @@ export default class FrontendModelController extends Controller {
|
|
|
4397
4397
|
|
|
4398
4398
|
responses.push({
|
|
4399
4399
|
requestId,
|
|
4400
|
-
response: await this.frontendModelClientErrorPayloadForError(error, errorContext
|
|
4401
|
-
forwardUnexpectedErrorMessage: !isBuiltInCommand
|
|
4402
|
-
})
|
|
4400
|
+
response: await this.frontendModelClientErrorPayloadForError(error, errorContext)
|
|
4403
4401
|
})
|
|
4404
4402
|
}
|
|
4405
4403
|
}
|
|
@@ -4623,9 +4621,7 @@ export default class FrontendModelController extends Controller {
|
|
|
4623
4621
|
await this.frontendModelLogEndpointError({error, errorContext})
|
|
4624
4622
|
|
|
4625
4623
|
await this.render({
|
|
4626
|
-
json: /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext,
|
|
4627
|
-
forwardUnexpectedErrorMessage: true
|
|
4628
|
-
}), this.transportSerializationOptions()))
|
|
4624
|
+
json: /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext), this.transportSerializationOptions()))
|
|
4629
4625
|
})
|
|
4630
4626
|
}
|
|
4631
4627
|
}
|
|
@@ -1512,7 +1512,7 @@ export type ConfigurationArgsType = {
|
|
|
1512
1512
|
*/
|
|
1513
1513
|
environmentHandler: import("./environment-handlers/base.js").default;
|
|
1514
1514
|
/**
|
|
1515
|
-
* - Return unexpected internal error
|
|
1515
|
+
* - Return unexpected internal error messages and stack traces in frontend-model client payloads in every environment. Defaults to true.
|
|
1516
1516
|
*/
|
|
1517
1517
|
exposeInternalErrorsToClients?: boolean;
|
|
1518
1518
|
/**
|
|
@@ -1522,7 +1522,7 @@ export type ConfigurationArgsType = {
|
|
|
1522
1522
|
maxOpenHandles?: number;
|
|
1523
1523
|
};
|
|
1524
1524
|
/**
|
|
1525
|
-
* -
|
|
1525
|
+
* - Deprecated compatibility alias for `exposeInternalErrorsToClients: false` when the authoritative option is omitted.
|
|
1526
1526
|
*/
|
|
1527
1527
|
secureFrontendModelErrors?: boolean;
|
|
1528
1528
|
/**
|
|
@@ -2247,9 +2247,9 @@ export type ConfigurationArgsType = {
|
|
|
2247
2247
|
* @property {boolean} [enforceTenantDatabaseScopes] - Require tenant-switched model queries to resolve a tenant database identifier. Defaults to true.
|
|
2248
2248
|
* @property {string} [environment] - Current environment name.
|
|
2249
2249
|
* @property {import("./environment-handlers/base.js").default} environmentHandler - Environment handler instance.
|
|
2250
|
-
* @property {boolean} [exposeInternalErrorsToClients] - Return unexpected internal error
|
|
2250
|
+
* @property {boolean} [exposeInternalErrorsToClients] - Return unexpected internal error messages and stack traces in frontend-model client payloads in every environment. Defaults to true.
|
|
2251
2251
|
* @property {{maxOpenHandles?: number}} [frontendTenantSqlite] - Bounded frontend tenant SQLite lifecycle configuration.
|
|
2252
|
-
* @property {boolean} [secureFrontendModelErrors] -
|
|
2252
|
+
* @property {boolean} [secureFrontendModelErrors] - Deprecated compatibility alias for `exposeInternalErrorsToClients: false` when the authoritative option is omitted.
|
|
2253
2253
|
* @property {HttpServerConfiguration} [httpServer] - Default HTTP server configuration for applications started from this configuration.
|
|
2254
2254
|
* @property {LoggingConfiguration} [logging] - Logging configuration.
|
|
2255
2255
|
* @property {BackgroundJobsConfiguration} [backgroundJobs] - Background jobs configuration.
|