velocious 1.0.459 → 1.0.461
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 +1 -1
- package/build/current.js +2 -2
- package/build/database/record/index.js +1 -1
- package/build/frontend-models/websocket-channel.js +113 -61
- package/build/src/current.d.ts +4 -4
- package/build/src/current.d.ts.map +1 -1
- package/build/src/current.js +3 -3
- package/build/src/database/record/index.d.ts +2 -2
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +2 -2
- package/build/src/frontend-models/websocket-channel.d.ts +15 -0
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +110 -63
- package/package.json +1 -1
- package/src/current.js +2 -2
- package/src/database/record/index.js +1 -1
- package/src/frontend-models/websocket-channel.js +113 -61
|
@@ -6,6 +6,10 @@ import {serializeFrontendModelTransportValue} from "./transport-serialization.js
|
|
|
6
6
|
|
|
7
7
|
const EVENT_FILTER_KEYS = new Set(["joins", "key", "searches", "where"])
|
|
8
8
|
|
|
9
|
+
// Mirrors FRONTEND_MODELS_CHANNEL_NAME in ./websocket-publishers.js, duplicated here
|
|
10
|
+
// to avoid the configuration → logger → websocket-publishers import cycle.
|
|
11
|
+
const FRONTEND_MODELS_CHANNEL_NAME = "frontend-models"
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Defines this typedef.
|
|
11
15
|
* @typedef {{action?: string, id?: string | number, matchedEventFilterKeys?: string[], record?: import("./query.js").FrontendModelTransportValue, [key: string]: import("./query.js").FrontendModelTransportValue | string[] | undefined}} FrontendModelLifecycleBroadcastBody
|
|
@@ -360,6 +364,48 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
360
364
|
return controller
|
|
361
365
|
}
|
|
362
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Resolves the subscriber's tenant for the broadcast record and runs `callback` inside that tenant
|
|
369
|
+
* context. Broadcast delivery runs in whatever ambient tenant context the publisher left behind. For
|
|
370
|
+
* multi-tenant records that ambient tenant may have been resolved without the subscriber's request
|
|
371
|
+
* (e.g. a relay endpoint or background job mutating the row), so it lacks the subscriber's per-record
|
|
372
|
+
* access flags and the per-event authorization query wrongly finds nothing. Re-resolving the tenant
|
|
373
|
+
* from the event record id plus the subscriber's request makes the authorization queries run against
|
|
374
|
+
* the subscriber's own tenant/ability scope. When no tenant resolves (non-multitenant configs), the
|
|
375
|
+
* callback runs directly so the ambient context is preserved.
|
|
376
|
+
* @template T
|
|
377
|
+
* @param {string | number} id - Event record id.
|
|
378
|
+
* @param {() => Promise<T>} callback - Authorized-query callback.
|
|
379
|
+
* @returns {Promise<T>} - Callback result.
|
|
380
|
+
*/
|
|
381
|
+
async _withEventTenant(id, callback) {
|
|
382
|
+
const configuration = this.session.configuration
|
|
383
|
+
|
|
384
|
+
if (!configuration || typeof configuration.resolveTenant !== "function") {
|
|
385
|
+
return await callback()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Mirror the subscribe-time tenant resolution (`WebsocketSession._resolveTenant`):
|
|
389
|
+
// pass `subscription: {channel, params}` so resolvers that derive scope from the
|
|
390
|
+
// subscription behave the same for broadcasts as they did at `channel-subscribe`.
|
|
391
|
+
// The synthetic request forwards the subscriber's params (e.g. authenticationToken),
|
|
392
|
+
// matching this channel's ability resolution above.
|
|
393
|
+
const tenant = await configuration.resolveTenant({
|
|
394
|
+
params: {...this.params, id, model: this._modelName()},
|
|
395
|
+
request: /** @type {import("../http-server/client/request.js").default} */ (this._syntheticRequest()),
|
|
396
|
+
response: new Response({configuration}),
|
|
397
|
+
subscription: {channel: FRONTEND_MODELS_CHANNEL_NAME, params: this.params}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// Always enter `runWithTenant`, even when no tenant resolved. Broadcast fan-out
|
|
401
|
+
// runs in the publisher's ambient tenant context; falling back to `callback()`
|
|
402
|
+
// there would authorize a cross-tenant record against the publisher's tenant and
|
|
403
|
+
// could leak it to a subscriber whose own resolver could not resolve it.
|
|
404
|
+
return await configuration.runWithTenant(tenant, async () => {
|
|
405
|
+
return await configuration.ensureConnections({name: "Frontend model websocket event tenant"}, callback)
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
363
409
|
/**
|
|
364
410
|
* Whether the broadcast record is within the subscriber's authenticated ability scope. Used to gate
|
|
365
411
|
* unfiltered/unprojected create/update delivery so a scoped token never receives a record it cannot read.
|
|
@@ -368,15 +414,17 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
368
414
|
* @returns {Promise<boolean>} True when the record is readable by this subscription.
|
|
369
415
|
*/
|
|
370
416
|
async _eventIsAccessible(id, FrontendModelController) {
|
|
371
|
-
|
|
417
|
+
return await this._withEventTenant(id, async () => {
|
|
418
|
+
const controller = this._frontendModelController(FrontendModelController)
|
|
372
419
|
|
|
373
|
-
|
|
420
|
+
await controller.ensureFrontendModelClassInitialized()
|
|
374
421
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
422
|
+
const ModelClass = controller.frontendModelClass()
|
|
423
|
+
const primaryKey = ModelClass.primaryKey()
|
|
424
|
+
const query = controller.frontendModelAuthorizedQuery("find").where({[ModelClass.tableName()]: {[primaryKey]: id}})
|
|
378
425
|
|
|
379
|
-
|
|
426
|
+
return Boolean(await query.first())
|
|
427
|
+
})
|
|
380
428
|
}
|
|
381
429
|
|
|
382
430
|
/**
|
|
@@ -413,30 +461,32 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
413
461
|
* @returns {Promise<boolean>} Whether the record matches the filter.
|
|
414
462
|
*/
|
|
415
463
|
async _eventMatchesFilter({FrontendModelController, eventFilter, id}) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
464
|
+
return await this._withEventTenant(id, async () => {
|
|
465
|
+
const controller = this._frontendModelController(FrontendModelController, {
|
|
466
|
+
joins: eventFilter.joins,
|
|
467
|
+
searches: eventFilter.searches,
|
|
468
|
+
where: eventFilter.where
|
|
469
|
+
})
|
|
421
470
|
|
|
422
|
-
|
|
471
|
+
await controller.ensureFrontendModelClassInitialized()
|
|
423
472
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
473
|
+
const ModelClass = controller.frontendModelClass()
|
|
474
|
+
const primaryKey = ModelClass.primaryKey()
|
|
475
|
+
const where = controller.frontendModelWhere()
|
|
476
|
+
const joins = controller.frontendModelJoins()
|
|
477
|
+
// Start from the subscriber's authorized scope so a filter can only ever match records the
|
|
478
|
+
// subscription's ability permits to read.
|
|
479
|
+
let query = controller.frontendModelAuthorizedQuery("find").where({[ModelClass.tableName()]: {[primaryKey]: id}})
|
|
431
480
|
|
|
432
|
-
|
|
433
|
-
|
|
481
|
+
if (where) controller.applyFrontendModelWhere({query, where})
|
|
482
|
+
if (joins) controller.applyFrontendModelJoins({joins, query})
|
|
434
483
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
484
|
+
for (const search of controller.frontendModelSearches()) {
|
|
485
|
+
controller.applyFrontendModelSearch({query, search})
|
|
486
|
+
}
|
|
438
487
|
|
|
439
|
-
|
|
488
|
+
return Boolean(await query.first())
|
|
489
|
+
})
|
|
440
490
|
}
|
|
441
491
|
|
|
442
492
|
/**
|
|
@@ -446,51 +496,53 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
446
496
|
* @returns {Promise<Record<string, import("./query.js").FrontendModelTransportValue> | null>} - Serialized projected record.
|
|
447
497
|
*/
|
|
448
498
|
async _projectedRecordForEventId(id, FrontendModelController) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
499
|
+
return await this._withEventTenant(id, async () => {
|
|
500
|
+
const controller = this._frontendModelController(FrontendModelController)
|
|
501
|
+
|
|
502
|
+
await controller.ensureFrontendModelClassInitialized()
|
|
503
|
+
|
|
504
|
+
const ModelClass = controller.frontendModelClass()
|
|
505
|
+
const primaryKey = ModelClass.primaryKey()
|
|
506
|
+
// Reload through the subscriber's authorized scope so projected records are only ever sent for
|
|
507
|
+
// rows the subscription's ability permits to read.
|
|
508
|
+
let query = controller.frontendModelAuthorizedQuery("find").where({[ModelClass.tableName()]: {[primaryKey]: id}})
|
|
509
|
+
const preload = controller.frontendModelPreload()
|
|
510
|
+
|
|
511
|
+
if (preload) query = query.preload(preload)
|
|
512
|
+
|
|
513
|
+
for (const entry of controller.frontendModelWithCount()) {
|
|
514
|
+
/**
|
|
515
|
+
* Spec.
|
|
516
|
+
* @type {Record<string, boolean | {relationship?: string, where?: Record<string, import("./query.js").FrontendModelTransportValue>}>} */
|
|
517
|
+
const spec = {}
|
|
518
|
+
|
|
519
|
+
spec[entry.attributeName] = {
|
|
520
|
+
relationship: entry.relationshipName,
|
|
521
|
+
where: entry.where ? /**
|
|
522
|
+
* Narrows the runtime value to the documented type.
|
|
523
|
+
* @type {Record<string, import("./query.js").FrontendModelTransportValue>} */ (entry.where) : undefined
|
|
524
|
+
}
|
|
525
|
+
query.withCount(spec)
|
|
473
526
|
}
|
|
474
|
-
query.withCount(spec)
|
|
475
|
-
}
|
|
476
527
|
|
|
477
|
-
|
|
528
|
+
const queryData = controller.frontendModelQueryData()
|
|
478
529
|
|
|
479
|
-
|
|
530
|
+
if (queryData !== null) query.queryData(queryData)
|
|
480
531
|
|
|
481
|
-
|
|
532
|
+
query = controller.applyFrontendModelTranslatedAttributePreloads({query})
|
|
482
533
|
|
|
483
|
-
|
|
534
|
+
const model = await query.first()
|
|
484
535
|
|
|
485
|
-
|
|
536
|
+
if (!model) return null
|
|
486
537
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
538
|
+
if (this.params.abilities !== undefined) {
|
|
539
|
+
await controller.frontendModelComputeAbilities([model])
|
|
540
|
+
}
|
|
490
541
|
|
|
491
|
-
|
|
542
|
+
controller._frontendModelAbilityOverride = undefined
|
|
492
543
|
|
|
493
|
-
|
|
544
|
+
return await controller.frontendModelResourceInstance().serialize(model, "find")
|
|
545
|
+
})
|
|
494
546
|
}
|
|
495
547
|
|
|
496
548
|
/**
|