velocious 1.0.472 → 1.0.474
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/build/authorization/base-resource.js +99 -0
- package/build/configuration-types.js +3 -2
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +1 -1
- package/build/frontend-model-controller.js +21 -17
- package/build/frontend-model-resource/base-resource.js +254 -46
- package/build/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/build/frontend-models/resource-definition.js +96 -21
- package/build/frontend-models/websocket-channel.js +24 -20
- package/build/routes/resolver.js +7 -5
- package/build/src/authorization/base-resource.d.ts +41 -0
- package/build/src/authorization/base-resource.d.ts.map +1 -1
- package/build/src/authorization/base-resource.js +88 -1
- package/build/src/configuration-types.d.ts +15 -3
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +4 -3
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +2 -2
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +22 -18
- package/build/src/frontend-model-resource/base-resource.d.ts +67 -0
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +242 -54
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +0 -2
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +1 -3
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +83 -25
- package/build/src/frontend-models/websocket-channel.d.ts +6 -0
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +23 -19
- package/build/src/routes/resolver.d.ts.map +1 -1
- package/build/src/routes/resolver.js +8 -6
- package/package.json +1 -1
- package/scripts/tensorbuzz-retry +21 -0
- package/src/authorization/base-resource.js +99 -0
- package/src/configuration-types.js +3 -2
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +1 -1
- package/src/frontend-model-controller.js +21 -17
- package/src/frontend-model-resource/base-resource.js +254 -46
- package/src/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/src/frontend-models/resource-definition.js +96 -21
- package/src/frontend-models/websocket-channel.js +24 -20
- package/src/routes/resolver.js +7 -5
|
@@ -148,6 +148,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
148
148
|
/** @type {Record<string, ?> | undefined} */
|
|
149
149
|
static attachments = undefined
|
|
150
150
|
/** @type {string[] | undefined} */
|
|
151
|
+
static commands = undefined
|
|
152
|
+
/** @type {string[] | undefined} */
|
|
151
153
|
static collectionCommands = undefined
|
|
152
154
|
/** @type {string[] | undefined} */
|
|
153
155
|
static builtInCollectionCommands = undefined
|
|
@@ -157,8 +159,16 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
157
159
|
static builtInMemberCommands = undefined
|
|
158
160
|
/** @type {string[] | undefined} */
|
|
159
161
|
static relationships = undefined
|
|
162
|
+
/** @type {string | undefined} */
|
|
163
|
+
static modelName = undefined
|
|
164
|
+
/** @type {string | undefined} */
|
|
165
|
+
static primaryKey = undefined
|
|
166
|
+
/** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration | undefined} */
|
|
167
|
+
static server = undefined
|
|
160
168
|
/** @type {string[] | undefined} */
|
|
161
169
|
static translatedAttributes = undefined
|
|
170
|
+
/** @type {?} */
|
|
171
|
+
static SharedResource = undefined
|
|
162
172
|
|
|
163
173
|
/**
|
|
164
174
|
* Runs constructor.
|
|
@@ -179,6 +189,149 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
179
189
|
this.modelNameValue = "modelName" in args ? args.modelName : this.modelClass().getModelName()
|
|
180
190
|
this.paramsValue = "params" in args ? args.params : undefined
|
|
181
191
|
this.resourceConfigurationValue = "resourceConfiguration" in args ? args.resourceConfiguration : defaultResourceConfiguration
|
|
192
|
+
/** @type {FrontendModelBaseResource | null | undefined} */
|
|
193
|
+
this.sharedResourceInstanceValue = undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns the configured shared resource class.
|
|
198
|
+
* @returns {?} - Shared resource class.
|
|
199
|
+
*/
|
|
200
|
+
static sharedResourceClass() {
|
|
201
|
+
return this.SharedResource
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reads a static resource config value from the environment resource first,
|
|
206
|
+
* then from the shared resource.
|
|
207
|
+
* @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "relationships" | "server" | "translatedAttributes"} name - Static config property name.
|
|
208
|
+
* @returns {?} - Resolved config value.
|
|
209
|
+
*/
|
|
210
|
+
static sharedResourceStaticValue(name) {
|
|
211
|
+
if (this[name] !== undefined) return this[name]
|
|
212
|
+
|
|
213
|
+
const SharedResource = /** @type {typeof FrontendModelBaseResource | undefined} */ (this.sharedResourceClass())
|
|
214
|
+
|
|
215
|
+
if (!SharedResource) return undefined
|
|
216
|
+
if (SharedResource[name] !== undefined) return SharedResource[name]
|
|
217
|
+
|
|
218
|
+
return undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolves translated attributes from environment and shared resources.
|
|
223
|
+
* @returns {string[] | undefined} - Translated attribute names.
|
|
224
|
+
*/
|
|
225
|
+
static translatedAttributesConfig() {
|
|
226
|
+
return /** @type {string[] | undefined} */ (this.sharedResourceStaticValue("translatedAttributes"))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Builds a resource instance for shared-resource fallback calls.
|
|
231
|
+
* @returns {FrontendModelBaseResource | null} - Shared resource instance when configured.
|
|
232
|
+
*/
|
|
233
|
+
sharedResourceInstance() {
|
|
234
|
+
if (this.sharedResourceInstanceValue !== undefined) return this.sharedResourceInstanceValue
|
|
235
|
+
|
|
236
|
+
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
|
|
237
|
+
const SharedResource = /** @type {typeof FrontendModelBaseResource | undefined} */ (ResourceClass.sharedResourceClass())
|
|
238
|
+
|
|
239
|
+
if (!SharedResource) {
|
|
240
|
+
this.sharedResourceInstanceValue = null
|
|
241
|
+
return this.sharedResourceInstanceValue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (SharedResource === ResourceClass) {
|
|
245
|
+
throw new Error(`${ResourceClass.name}.SharedResource cannot point to itself.`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.sharedResourceInstanceValue = new SharedResource({
|
|
249
|
+
ability: this.ability,
|
|
250
|
+
controller: this.controller,
|
|
251
|
+
context: this.context,
|
|
252
|
+
locals: this.locals,
|
|
253
|
+
modelClass: this.modelClass(),
|
|
254
|
+
modelName: this.modelName(),
|
|
255
|
+
params: this.params(),
|
|
256
|
+
resourceConfiguration: this.resourceConfiguration()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return this.sharedResourceInstanceValue
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Calls a shared-resource method only when the shared resource overrides the framework default.
|
|
264
|
+
* @param {string} methodName - Method name to resolve.
|
|
265
|
+
* @param {unknown[]} args - Method args.
|
|
266
|
+
* @returns {{called: boolean, result: unknown}} - Shared method call result.
|
|
267
|
+
*/
|
|
268
|
+
callSharedResourceMethod(methodName, args) {
|
|
269
|
+
const sharedResource = this.sharedResourceInstance()
|
|
270
|
+
|
|
271
|
+
if (!sharedResource) return {called: false, result: undefined}
|
|
272
|
+
|
|
273
|
+
const methodOwner = prototypeOwnerForMethod(sharedResource, methodName)
|
|
274
|
+
|
|
275
|
+
if (!methodOwner || methodOwner === FrontendModelBaseResource.prototype || methodOwner === AuthorizationBaseResource.prototype) {
|
|
276
|
+
return {called: false, result: undefined}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const method = /** @type {Record<string, (...methodArgs: unknown[]) => unknown>} */ (/** @type {unknown} */ (sharedResource))[methodName]
|
|
280
|
+
|
|
281
|
+
return {called: true, result: method.apply(sharedResource, args)}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Runs shared method result or a fallback callback.
|
|
286
|
+
* @template Result
|
|
287
|
+
* @param {string} methodName - Shared method name.
|
|
288
|
+
* @param {unknown[]} args - Shared method args.
|
|
289
|
+
* @param {() => Result} fallback - Fallback callback.
|
|
290
|
+
* @returns {Result} - Shared or fallback result.
|
|
291
|
+
*/
|
|
292
|
+
sharedResourceMethodOr(methodName, args, fallback) {
|
|
293
|
+
const sharedResult = this.callSharedResourceMethod(methodName, args)
|
|
294
|
+
|
|
295
|
+
if (sharedResult.called) return /** @type {Result} */ (sharedResult.result)
|
|
296
|
+
|
|
297
|
+
return fallback()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Resolves a method on this resource or its shared fallback.
|
|
302
|
+
* @param {string} methodName - Method name.
|
|
303
|
+
* @returns {{method: (...methodArgs: unknown[]) => unknown, resource: FrontendModelBaseResource} | null} - Resolved method and receiver.
|
|
304
|
+
*/
|
|
305
|
+
resourceMethod(methodName) {
|
|
306
|
+
const ownMethod = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (this))[methodName]
|
|
307
|
+
|
|
308
|
+
if (typeof ownMethod === "function") {
|
|
309
|
+
return {
|
|
310
|
+
method: /** @type {(...methodArgs: unknown[]) => unknown} */ (ownMethod),
|
|
311
|
+
resource: this
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const sharedResource = this.sharedResourceInstance()
|
|
316
|
+
|
|
317
|
+
if (!sharedResource) return null
|
|
318
|
+
|
|
319
|
+
const sharedMethod = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (sharedResource))[methodName]
|
|
320
|
+
|
|
321
|
+
if (typeof sharedMethod !== "function") return null
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
method: /** @type {(...methodArgs: unknown[]) => unknown} */ (sharedMethod),
|
|
325
|
+
resource: sharedResource
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Runs abilities.
|
|
331
|
+
* @returns {void} - No return value.
|
|
332
|
+
*/
|
|
333
|
+
abilities() {
|
|
334
|
+
this.sharedResourceMethodOr("abilities", [], () => undefined)
|
|
182
335
|
}
|
|
183
336
|
|
|
184
337
|
/**
|
|
@@ -194,18 +347,34 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
194
347
|
* @returns {import("../configuration-types.js").FrontendModelResourceConfiguration} - Static resource config (raw user input shape; consumers normalize).
|
|
195
348
|
*/
|
|
196
349
|
static resourceConfig() {
|
|
350
|
+
const attributes = this.sharedResourceStaticValue("attributes")
|
|
351
|
+
const abilities = this.sharedResourceStaticValue("abilities")
|
|
352
|
+
const attachments = this.sharedResourceStaticValue("attachments")
|
|
353
|
+
const commands = this.sharedResourceStaticValue("commands")
|
|
354
|
+
const builtInCollectionCommands = this.sharedResourceStaticValue("builtInCollectionCommands")
|
|
355
|
+
const builtInMemberCommands = this.sharedResourceStaticValue("builtInMemberCommands")
|
|
356
|
+
const collectionCommands = this.sharedResourceStaticValue("collectionCommands")
|
|
357
|
+
const memberCommands = this.sharedResourceStaticValue("memberCommands")
|
|
358
|
+
const modelName = this.sharedResourceStaticValue("modelName")
|
|
359
|
+
const primaryKey = this.sharedResourceStaticValue("primaryKey")
|
|
360
|
+
const relationships = this.sharedResourceStaticValue("relationships")
|
|
361
|
+
const server = this.sharedResourceStaticValue("server")
|
|
197
362
|
/** @type {import("../configuration-types.js").FrontendModelResourceConfiguration} */
|
|
198
363
|
const config = {
|
|
199
|
-
attributes:
|
|
364
|
+
attributes: /** @type {Record<string, ?> | string[]} */ (attributes || [])
|
|
200
365
|
}
|
|
201
366
|
|
|
202
|
-
if (
|
|
203
|
-
if (
|
|
204
|
-
if (
|
|
205
|
-
if (
|
|
206
|
-
if (
|
|
207
|
-
if (
|
|
208
|
-
if (
|
|
367
|
+
if (abilities) config.abilities = /** @type {string[]} */ (abilities)
|
|
368
|
+
if (attachments) config.attachments = /** @type {Record<string, ?>} */ (attachments)
|
|
369
|
+
if (commands) config.commands = /** @type {string[]} */ (commands)
|
|
370
|
+
if (builtInCollectionCommands) config.builtInCollectionCommands = /** @type {string[]} */ (builtInCollectionCommands)
|
|
371
|
+
if (builtInMemberCommands) config.builtInMemberCommands = /** @type {string[]} */ (builtInMemberCommands)
|
|
372
|
+
if (collectionCommands) config.collectionCommands = /** @type {string[]} */ (collectionCommands)
|
|
373
|
+
if (memberCommands) config.memberCommands = /** @type {string[]} */ (memberCommands)
|
|
374
|
+
if (modelName) config.modelName = /** @type {string} */ (modelName)
|
|
375
|
+
if (primaryKey) config.primaryKey = /** @type {string} */ (primaryKey)
|
|
376
|
+
if (relationships) config.relationships = /** @type {string[]} */ (relationships)
|
|
377
|
+
if (server) config.server = /** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration} */ (server)
|
|
209
378
|
|
|
210
379
|
return config
|
|
211
380
|
}
|
|
@@ -242,6 +411,14 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
242
411
|
return /** @type {TModelClass} */ (this.modelClassValue)
|
|
243
412
|
}
|
|
244
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Runs required model class for authorization helpers.
|
|
416
|
+
* @returns {TModelClass} - Backing model class.
|
|
417
|
+
*/
|
|
418
|
+
requiredModelClass() {
|
|
419
|
+
return this.modelClass()
|
|
420
|
+
}
|
|
421
|
+
|
|
245
422
|
/**
|
|
246
423
|
* Runs model name.
|
|
247
424
|
* @returns {string} - Model name.
|
|
@@ -307,9 +484,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
307
484
|
* @returns {Array<string | Record<string, ?>>} - Permit spec.
|
|
308
485
|
*/
|
|
309
486
|
permittedParams(arg) {
|
|
310
|
-
|
|
487
|
+
return this.sharedResourceMethodOr("permittedParams", [arg], () => {
|
|
488
|
+
void arg
|
|
311
489
|
|
|
312
|
-
|
|
490
|
+
return []
|
|
491
|
+
})
|
|
313
492
|
}
|
|
314
493
|
|
|
315
494
|
/**
|
|
@@ -319,9 +498,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
319
498
|
* @returns {FrontendModelResourceAttributePayload | Promise<FrontendModelResourceAttributePayload>} - Normalized attributes.
|
|
320
499
|
*/
|
|
321
500
|
normalizeCreateAttributes(attributes, options) {
|
|
322
|
-
|
|
501
|
+
return this.sharedResourceMethodOr("normalizeCreateAttributes", [attributes, options], () => {
|
|
502
|
+
void options
|
|
323
503
|
|
|
324
|
-
|
|
504
|
+
return attributes
|
|
505
|
+
})
|
|
325
506
|
}
|
|
326
507
|
|
|
327
508
|
/**
|
|
@@ -332,10 +513,12 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
332
513
|
* @returns {FrontendModelResourceAttributePayload | Promise<FrontendModelResourceAttributePayload>} - Normalized attributes.
|
|
333
514
|
*/
|
|
334
515
|
normalizeUpdateAttributes(model, attributes, options) {
|
|
335
|
-
|
|
336
|
-
|
|
516
|
+
return this.sharedResourceMethodOr("normalizeUpdateAttributes", [model, attributes, options], () => {
|
|
517
|
+
void model
|
|
518
|
+
void options
|
|
337
519
|
|
|
338
|
-
|
|
520
|
+
return attributes
|
|
521
|
+
})
|
|
339
522
|
}
|
|
340
523
|
|
|
341
524
|
/**
|
|
@@ -346,9 +529,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
346
529
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
347
530
|
*/
|
|
348
531
|
beforeCreate(model, attributes, options) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
532
|
+
return this.sharedResourceMethodOr("beforeCreate", [model, attributes, options], () => {
|
|
533
|
+
void model
|
|
534
|
+
void attributes
|
|
535
|
+
void options
|
|
536
|
+
})
|
|
352
537
|
}
|
|
353
538
|
|
|
354
539
|
/**
|
|
@@ -359,9 +544,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
359
544
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
360
545
|
*/
|
|
361
546
|
afterCreate(model, attributes, options) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
547
|
+
return this.sharedResourceMethodOr("afterCreate", [model, attributes, options], () => {
|
|
548
|
+
void model
|
|
549
|
+
void attributes
|
|
550
|
+
void options
|
|
551
|
+
})
|
|
365
552
|
}
|
|
366
553
|
|
|
367
554
|
/**
|
|
@@ -372,9 +559,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
372
559
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
373
560
|
*/
|
|
374
561
|
beforeUpdate(model, attributes, options) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
562
|
+
return this.sharedResourceMethodOr("beforeUpdate", [model, attributes, options], () => {
|
|
563
|
+
void model
|
|
564
|
+
void attributes
|
|
565
|
+
void options
|
|
566
|
+
})
|
|
378
567
|
}
|
|
379
568
|
|
|
380
569
|
/**
|
|
@@ -385,9 +574,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
385
574
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
386
575
|
*/
|
|
387
576
|
afterUpdate(model, attributes, options) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
577
|
+
return this.sharedResourceMethodOr("afterUpdate", [model, attributes, options], () => {
|
|
578
|
+
void model
|
|
579
|
+
void attributes
|
|
580
|
+
void options
|
|
581
|
+
})
|
|
391
582
|
}
|
|
392
583
|
|
|
393
584
|
/**
|
|
@@ -396,7 +587,9 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
396
587
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
397
588
|
*/
|
|
398
589
|
beforeDestroy(model) {
|
|
399
|
-
|
|
590
|
+
return this.sharedResourceMethodOr("beforeDestroy", [model], () => {
|
|
591
|
+
void model
|
|
592
|
+
})
|
|
400
593
|
}
|
|
401
594
|
|
|
402
595
|
/**
|
|
@@ -405,7 +598,9 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
405
598
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
406
599
|
*/
|
|
407
600
|
afterDestroy(model) {
|
|
408
|
-
|
|
601
|
+
return this.sharedResourceMethodOr("afterDestroy", [model], () => {
|
|
602
|
+
void model
|
|
603
|
+
})
|
|
409
604
|
}
|
|
410
605
|
|
|
411
606
|
/**
|
|
@@ -418,10 +613,12 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
418
613
|
* @returns {Promise<Result>} - Callback result.
|
|
419
614
|
*/
|
|
420
615
|
async runMutationTransaction({action, model, callback}) {
|
|
421
|
-
|
|
422
|
-
|
|
616
|
+
return await this.sharedResourceMethodOr("runMutationTransaction", [{action, model, callback}], async () => {
|
|
617
|
+
void action
|
|
618
|
+
void model
|
|
423
619
|
|
|
424
|
-
|
|
620
|
+
return await callback()
|
|
621
|
+
})
|
|
425
622
|
}
|
|
426
623
|
|
|
427
624
|
/**
|
|
@@ -517,9 +714,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
517
714
|
* @returns {boolean | void | Promise<boolean | void>} - Continue processing unless false.
|
|
518
715
|
*/
|
|
519
716
|
beforeAction(action) {
|
|
520
|
-
|
|
717
|
+
return this.sharedResourceMethodOr("beforeAction", [action], () => {
|
|
718
|
+
void action
|
|
521
719
|
|
|
522
|
-
|
|
720
|
+
// No-op by default.
|
|
721
|
+
})
|
|
523
722
|
}
|
|
524
723
|
|
|
525
724
|
/**
|
|
@@ -663,14 +862,14 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
663
862
|
/** @type {Record<string, ?>} */
|
|
664
863
|
const directAttributes = {}
|
|
665
864
|
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
|
|
666
|
-
const translatedSet = new Set(ResourceClass.
|
|
865
|
+
const translatedSet = new Set(ResourceClass.translatedAttributesConfig() || [])
|
|
667
866
|
|
|
668
867
|
for (const [name, value] of Object.entries(attributes)) {
|
|
669
868
|
const resourceSetterName = `set${inflection.camelize(name)}Attribute`
|
|
670
|
-
const resourceSetter =
|
|
869
|
+
const resourceSetter = this.resourceMethod(resourceSetterName)
|
|
671
870
|
|
|
672
|
-
if (
|
|
673
|
-
await resourceSetter.call(
|
|
871
|
+
if (resourceSetter) {
|
|
872
|
+
await resourceSetter.method.call(resourceSetter.resource, model, value)
|
|
674
873
|
} else if (translatedSet.has(name)) {
|
|
675
874
|
await this._setTranslatedAttributeOnModel(model, name, value)
|
|
676
875
|
} else {
|
|
@@ -1527,12 +1726,21 @@ function parsePermittedParams(permitSpec) {
|
|
|
1527
1726
|
}
|
|
1528
1727
|
|
|
1529
1728
|
/**
|
|
1530
|
-
*
|
|
1531
|
-
* @param {
|
|
1532
|
-
* @
|
|
1729
|
+
* Locates which prototype owns a method implementation.
|
|
1730
|
+
* @param {object} instance - Instance receiving the method.
|
|
1731
|
+
* @param {string} methodName - Method name.
|
|
1732
|
+
* @returns {object | null} - Prototype that owns the method.
|
|
1533
1733
|
*/
|
|
1534
|
-
function
|
|
1535
|
-
|
|
1734
|
+
function prototypeOwnerForMethod(instance, methodName) {
|
|
1735
|
+
let prototype = Object.getPrototypeOf(instance)
|
|
1736
|
+
|
|
1737
|
+
while (prototype) {
|
|
1738
|
+
if (Object.prototype.hasOwnProperty.call(prototype, methodName)) return prototype
|
|
1739
|
+
|
|
1740
|
+
prototype = Object.getPrototypeOf(prototype)
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return null
|
|
1536
1744
|
}
|
|
1537
1745
|
|
|
1538
1746
|
/**
|
|
@@ -1567,7 +1775,7 @@ function filterWritableFrontendModelAttributes(
|
|
|
1567
1775
|
if (resource) {
|
|
1568
1776
|
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (resource.constructor)
|
|
1569
1777
|
|
|
1570
|
-
translatedAttributes = ResourceClass.
|
|
1778
|
+
translatedAttributes = ResourceClass.translatedAttributesConfig() || []
|
|
1571
1779
|
}
|
|
1572
1780
|
|
|
1573
1781
|
const translatedSet = new Set(translatedAttributes)
|
|
@@ -1582,11 +1790,11 @@ function filterWritableFrontendModelAttributes(
|
|
|
1582
1790
|
const requestedSetterName = `set${inflection.camelize(resolvedAttributeName)}`
|
|
1583
1791
|
const setterName = receiverClass.findMemberNameInsensitive(receiver, requestedSetterName) || requestedSetterName
|
|
1584
1792
|
const resourceSetterName = `set${inflection.camelize(attributeName)}Attribute`
|
|
1585
|
-
const resourceSetter = resource
|
|
1793
|
+
const resourceSetter = resource?.resourceMethod(resourceSetterName)
|
|
1586
1794
|
|
|
1587
1795
|
if (setterName in receiver) {
|
|
1588
1796
|
writableAttributes[attributeName] = value
|
|
1589
|
-
} else if (
|
|
1797
|
+
} else if (resourceSetter) {
|
|
1590
1798
|
writableAttributes[attributeName] = value
|
|
1591
1799
|
} else if (translatedSet.has(attributeName)) {
|
|
1592
1800
|
writableAttributes[attributeName] = value
|
|
@@ -26,9 +26,6 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
|
|
|
26
26
|
updatedAt: {type: "datetime"}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** @type {string[]} */
|
|
30
|
-
static abilities = ["read"]
|
|
31
|
-
|
|
32
29
|
/** @type {string[]} */
|
|
33
30
|
static builtInCollectionCommands = ["index"]
|
|
34
31
|
|
|
@@ -5,6 +5,25 @@ import FrontendModelBaseResource from "../frontend-model-resource/base-resource.
|
|
|
5
5
|
import restArgsError from "../utils/rest-args-error.js"
|
|
6
6
|
import {validateFrontendModelResourceCommandName} from "./resource-config-validation.js"
|
|
7
7
|
|
|
8
|
+
const BASE_FRONTEND_MODEL_ABILITY_ACTIONS = ["create", "destroy", "read", "update"]
|
|
9
|
+
const RESOURCE_STATIC_CONFIG_KEYS = new Set([
|
|
10
|
+
"abilities",
|
|
11
|
+
"attachments",
|
|
12
|
+
"attributes",
|
|
13
|
+
"builtInCollectionCommands",
|
|
14
|
+
"builtInMemberCommands",
|
|
15
|
+
"collectionCommands",
|
|
16
|
+
"commands",
|
|
17
|
+
"memberCommands",
|
|
18
|
+
"modelName",
|
|
19
|
+
"ModelClass",
|
|
20
|
+
"primaryKey",
|
|
21
|
+
"relationships",
|
|
22
|
+
"server",
|
|
23
|
+
"SharedResource",
|
|
24
|
+
"translatedAttributes"
|
|
25
|
+
])
|
|
26
|
+
|
|
8
27
|
/**
|
|
9
28
|
* Runs the frontendModelResourcesForBackendProject helper.
|
|
10
29
|
* @param {import("../configuration-types.js").BackendProjectConfiguration} backendProject - Backend project config.
|
|
@@ -50,9 +69,74 @@ export function frontendModelResourceClassFromDefinition(resourceDefinition) {
|
|
|
50
69
|
export function frontendModelResourceConfigurationFromDefinition(resourceDefinition) {
|
|
51
70
|
if (!frontendModelResourceDefinitionIsClass(resourceDefinition)) return null
|
|
52
71
|
|
|
72
|
+
assertResourceConfigIsFrameworkDefined(resourceDefinition)
|
|
73
|
+
|
|
53
74
|
return normalizeFrontendModelResourceConfiguration(resourceDefinition.resourceConfig())
|
|
54
75
|
}
|
|
55
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Ensures resources use declarative static config properties instead of overriding resourceConfig().
|
|
79
|
+
* @param {import("../configuration-types.js").FrontendModelResourceClassType} ResourceClass - Resource class.
|
|
80
|
+
* @param {Set<import("../configuration-types.js").FrontendModelResourceClassType>} [visited] - Already inspected shared resources.
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
function assertResourceConfigIsFrameworkDefined(ResourceClass, visited = new Set()) {
|
|
84
|
+
if (visited.has(ResourceClass)) return
|
|
85
|
+
|
|
86
|
+
visited.add(ResourceClass)
|
|
87
|
+
assertKnownResourceStaticConfigProperties(ResourceClass)
|
|
88
|
+
|
|
89
|
+
const owner = staticMethodOwnerFor(ResourceClass, "resourceConfig")
|
|
90
|
+
|
|
91
|
+
if (owner && owner !== FrontendModelBaseResource) {
|
|
92
|
+
throw new Error(`${ResourceClass.name} overrides static resourceConfig(), which is not supported. Use static resource properties instead.`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SharedResource = ResourceClass.sharedResourceClass()
|
|
96
|
+
|
|
97
|
+
if (SharedResource) assertResourceConfigIsFrameworkDefined(SharedResource, visited)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Ensures declarative static resource config does not silently ignore typos or removed keys.
|
|
102
|
+
* @param {import("../configuration-types.js").FrontendModelResourceClassType} ResourceClass - Resource class.
|
|
103
|
+
* @returns {void}
|
|
104
|
+
*/
|
|
105
|
+
function assertKnownResourceStaticConfigProperties(ResourceClass) {
|
|
106
|
+
let currentClass = ResourceClass
|
|
107
|
+
|
|
108
|
+
while (currentClass && currentClass !== FrontendModelBaseResource && currentClass !== Function.prototype) {
|
|
109
|
+
/** @type {Record<string, ?>} */
|
|
110
|
+
const unknownStaticConfig = {}
|
|
111
|
+
|
|
112
|
+
for (const key of Object.keys(currentClass)) {
|
|
113
|
+
if (!RESOURCE_STATIC_CONFIG_KEYS.has(key)) unknownStaticConfig[key] = /** @type {Record<string, ?>} */ (/** @type {unknown} */ (currentClass))[key]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
restArgsError(unknownStaticConfig)
|
|
117
|
+
|
|
118
|
+
currentClass = Object.getPrototypeOf(currentClass)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Locates which constructor owns a static method implementation.
|
|
124
|
+
* @param {import("../configuration-types.js").FrontendModelResourceClassType} ResourceClass - Resource class.
|
|
125
|
+
* @param {string} methodName - Method name.
|
|
126
|
+
* @returns {import("../configuration-types.js").FrontendModelResourceClassType | typeof FrontendModelBaseResource | null} - Class that owns the static method.
|
|
127
|
+
*/
|
|
128
|
+
function staticMethodOwnerFor(ResourceClass, methodName) {
|
|
129
|
+
let currentClass = ResourceClass
|
|
130
|
+
|
|
131
|
+
while (currentClass && currentClass !== Function.prototype) {
|
|
132
|
+
if (Object.prototype.hasOwnProperty.call(currentClass, methodName)) return currentClass
|
|
133
|
+
|
|
134
|
+
currentClass = Object.getPrototypeOf(currentClass)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
56
140
|
/**
|
|
57
141
|
* Runs normalize frontend model resource configuration.
|
|
58
142
|
* @param {import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Raw resource configuration.
|
|
@@ -102,36 +186,27 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
|
|
|
102
186
|
* @returns {Record<string, string>} - Normalized abilities config.
|
|
103
187
|
*/
|
|
104
188
|
function normalizeFrontendModelResourceAbilities(abilities) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
const normalized = defaultCrudAbilities()
|
|
190
|
+
|
|
191
|
+
if (abilities === undefined) return normalized
|
|
108
192
|
|
|
109
193
|
if (!Array.isArray(abilities)) {
|
|
110
194
|
throw new Error("Resource abilities must be an array of action names. Object form is no longer supported.")
|
|
111
195
|
}
|
|
112
196
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
find: "manage",
|
|
118
|
-
index: "manage",
|
|
119
|
-
update: "manage"
|
|
120
|
-
}
|
|
197
|
+
const duplicatedBaseAbilities = abilities.filter((ability) => BASE_FRONTEND_MODEL_ABILITY_ACTIONS.includes(ability))
|
|
198
|
+
|
|
199
|
+
if (duplicatedBaseAbilities.length > 0) {
|
|
200
|
+
throw new Error(`Resource abilities must not include base actions: ${duplicatedBaseAbilities.join(", ")}`)
|
|
121
201
|
}
|
|
122
202
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
203
|
+
for (const ability of abilities) {
|
|
204
|
+
if (typeof ability !== "string" || ability.length < 1) {
|
|
205
|
+
throw new Error("Resource abilities entries must be non-empty strings.")
|
|
206
|
+
}
|
|
127
207
|
|
|
128
|
-
|
|
129
|
-
if (abilities.includes("destroy")) normalized.destroy = "destroy"
|
|
130
|
-
if (abilities.includes("read")) {
|
|
131
|
-
normalized.find = "read"
|
|
132
|
-
normalized.index = "read"
|
|
208
|
+
normalized[ability] = ability
|
|
133
209
|
}
|
|
134
|
-
if (abilities.includes("update")) normalized.update = "update"
|
|
135
210
|
|
|
136
211
|
return normalized
|
|
137
212
|
}
|
|
@@ -106,15 +106,6 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
106
106
|
* @returns {Promise<void>} Resolves after delivery.
|
|
107
107
|
*/
|
|
108
108
|
async deliverBroadcast(body, meta) {
|
|
109
|
-
const configuration = this.session.configuration
|
|
110
|
-
|
|
111
|
-
if (configuration) {
|
|
112
|
-
await configuration.ensureConnections({name: "Frontend model websocket broadcast"}, async () => {
|
|
113
|
-
await this._deliverBroadcast(body, meta)
|
|
114
|
-
})
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
109
|
await this._deliverBroadcast(body, meta)
|
|
119
110
|
}
|
|
120
111
|
|
|
@@ -367,6 +358,29 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
367
358
|
return controller
|
|
368
359
|
}
|
|
369
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Resolves tenant for event.
|
|
363
|
+
* @param {string | number} id - Event record id.
|
|
364
|
+
* @returns {Promise<?>} - Resolved tenant.
|
|
365
|
+
*/
|
|
366
|
+
async _resolveEventTenant(id) {
|
|
367
|
+
const configuration = this.session.configuration
|
|
368
|
+
|
|
369
|
+
return await configuration.ensureConnections({name: "Frontend model websocket event tenant resolution"}, async () => {
|
|
370
|
+
// Mirror the subscribe-time tenant resolution (`WebsocketSession._resolveTenant`):
|
|
371
|
+
// pass `subscription: {channel, params}` so resolvers that derive scope from the
|
|
372
|
+
// subscription behave the same for broadcasts as they did at `channel-subscribe`.
|
|
373
|
+
// The synthetic request forwards the subscriber's params (e.g. authenticationToken),
|
|
374
|
+
// matching this channel's ability resolution above.
|
|
375
|
+
return await configuration.resolveTenant({
|
|
376
|
+
params: {...this.params, id, model: this._modelName()},
|
|
377
|
+
request: /** @type {import("../http-server/client/request.js").default} */ (this._syntheticRequest()),
|
|
378
|
+
response: new Response({configuration}),
|
|
379
|
+
subscription: {channel: FRONTEND_MODELS_CHANNEL_NAME, params: this.params}
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
370
384
|
/**
|
|
371
385
|
* Resolves the subscriber's tenant for the broadcast record and runs `callback` inside that tenant
|
|
372
386
|
* context. Broadcast delivery runs in whatever ambient tenant context the publisher left behind. For
|
|
@@ -388,17 +402,7 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
388
402
|
return await callback()
|
|
389
403
|
}
|
|
390
404
|
|
|
391
|
-
|
|
392
|
-
// pass `subscription: {channel, params}` so resolvers that derive scope from the
|
|
393
|
-
// subscription behave the same for broadcasts as they did at `channel-subscribe`.
|
|
394
|
-
// The synthetic request forwards the subscriber's params (e.g. authenticationToken),
|
|
395
|
-
// matching this channel's ability resolution above.
|
|
396
|
-
const tenant = await configuration.resolveTenant({
|
|
397
|
-
params: {...this.params, id, model: this._modelName()},
|
|
398
|
-
request: /** @type {import("../http-server/client/request.js").default} */ (this._syntheticRequest()),
|
|
399
|
-
response: new Response({configuration}),
|
|
400
|
-
subscription: {channel: FRONTEND_MODELS_CHANNEL_NAME, params: this.params}
|
|
401
|
-
})
|
|
405
|
+
const tenant = await this._resolveEventTenant(id)
|
|
402
406
|
|
|
403
407
|
// Always enter `runWithTenant`, even when no tenant resolved. Broadcast fan-out
|
|
404
408
|
// runs in the publisher's ambient tenant context; falling back to `callback()`
|