velocious 1.0.473 → 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 +12 -12
- 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/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.js +13 -13
- 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/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 +12 -12
- 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
|
@@ -115,6 +115,105 @@ export default class AuthorizationBaseResource {
|
|
|
115
115
|
return this.context.currentUser
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Runs current device.
|
|
120
|
+
* @returns {unknown} - Current device from context.
|
|
121
|
+
*/
|
|
122
|
+
currentDevice() {
|
|
123
|
+
return this.context.currentDevice
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Runs offline grant.
|
|
128
|
+
* @returns {unknown} - Offline grant from context.
|
|
129
|
+
*/
|
|
130
|
+
offlineGrant() {
|
|
131
|
+
return this.context.offlineGrant
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Runs now.
|
|
136
|
+
* @returns {Date} - Current time from context or the system clock.
|
|
137
|
+
*/
|
|
138
|
+
now() {
|
|
139
|
+
if (typeof this.context.now === "function") return this.context.now()
|
|
140
|
+
if (this.context.now instanceof Date) return this.context.now
|
|
141
|
+
|
|
142
|
+
return new Date()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Runs resource runtime.
|
|
147
|
+
* @returns {"backend" | "frontend" | "offline"} - Resource runtime context.
|
|
148
|
+
*/
|
|
149
|
+
resourceRuntime() {
|
|
150
|
+
if (this.context.resourceRuntime === "frontend") return "frontend"
|
|
151
|
+
if (this.context.resourceRuntime === "offline") return "offline"
|
|
152
|
+
|
|
153
|
+
return "backend"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Runs is backend.
|
|
158
|
+
* @returns {boolean} - Whether the resource is running in the backend runtime.
|
|
159
|
+
*/
|
|
160
|
+
isBackend() {
|
|
161
|
+
return this.resourceRuntime() === "backend"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Runs is frontend.
|
|
166
|
+
* @returns {boolean} - Whether the resource is running in the frontend runtime.
|
|
167
|
+
*/
|
|
168
|
+
isFrontend() {
|
|
169
|
+
return this.resourceRuntime() === "frontend"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Runs is offline.
|
|
174
|
+
* @returns {boolean} - Whether the resource is running with offline context.
|
|
175
|
+
*/
|
|
176
|
+
isOffline() {
|
|
177
|
+
return this.resourceRuntime() === "offline" || this.context.offlineGrant !== undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolves a model class from the portable resource context.
|
|
182
|
+
* @param {string} name - Model name.
|
|
183
|
+
* @returns {unknown} - Model class from registry.
|
|
184
|
+
*/
|
|
185
|
+
model(name) {
|
|
186
|
+
const registry = this.context.modelRegistry
|
|
187
|
+
|
|
188
|
+
if (registry && typeof registry === "object") {
|
|
189
|
+
if ("model" in registry && typeof registry.model === "function") {
|
|
190
|
+
const modelClass = registry.model(name)
|
|
191
|
+
|
|
192
|
+
if (modelClass) return modelClass
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (name in registry) return /** @type {Record<string, unknown>} */ (registry)[name]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const contextModel = this.context.model
|
|
199
|
+
|
|
200
|
+
if (typeof contextModel === "function") {
|
|
201
|
+
const modelClass = contextModel(name)
|
|
202
|
+
|
|
203
|
+
if (modelClass) return modelClass
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const configuration = this.context.configuration
|
|
207
|
+
|
|
208
|
+
if (configuration) {
|
|
209
|
+
const modelClasses = configuration.getModelClasses()
|
|
210
|
+
|
|
211
|
+
if (name in modelClasses) return modelClasses[name]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new Error(`${this.constructor.name} could not resolve model '${name}' from the resource context model registry.`)
|
|
215
|
+
}
|
|
216
|
+
|
|
118
217
|
/**
|
|
119
218
|
* Runs request.
|
|
120
219
|
* @returns {import("../http-server/client/request.js").default | import("../http-server/client/websocket-request.js").default | undefined} - Request from context.
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
*/
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
|
-
* @typedef {Record<string, unknown> & {configuration?: import("./configuration.js").default, currentUser?: unknown, params?: VelociousParams, request?: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default}} VelociousLooseObject
|
|
243
|
+
* @typedef {Record<string, unknown> & {configuration?: import("./configuration.js").default, currentDevice?: unknown, currentUser?: unknown, modelRegistry?: Record<string, unknown> | {model: (name: string) => unknown}, now?: Date | (() => Date), offlineGrant?: unknown, params?: VelociousParams, request?: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default, resourceRuntime?: "backend" | "frontend" | "offline"}} VelociousLooseObject
|
|
244
244
|
*/
|
|
245
245
|
|
|
246
246
|
/**
|
|
@@ -309,13 +309,14 @@
|
|
|
309
309
|
/**
|
|
310
310
|
* @typedef {object} FrontendModelResourceConfiguration
|
|
311
311
|
* @property {Array<string | FrontendModelAttributeConfiguration> | Record<string, FrontendModelAttributeConfiguration | import("./database/drivers/base-column.js").default | boolean>} attributes - Attributes to expose on the frontend model.
|
|
312
|
-
* @property {string[]} [abilities] -
|
|
312
|
+
* @property {string[]} [abilities] - Additional camelCase ability action names to expose for per-record `record.can(action)` reads. Base CRUD actions (`read`, `create`, `update`, `destroy`) are always included and must not be listed here.
|
|
313
313
|
* @property {Record<string, FrontendModelAttachmentConfiguration>} [attachments] - Attachment helpers keyed by attachment name.
|
|
314
314
|
* @property {string[]} [commands] - Legacy built-in command names (`index`, `find`, `create`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
315
315
|
* @property {Array<FrontendModelResourceCustomCommand>} [collectionCommands] - Custom collection commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
316
316
|
* @property {Array<FrontendModelResourceCustomCommand>} [memberCommands] - Custom member commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
317
317
|
* @property {string[]} [builtInCollectionCommands] - Built-in collection command names (`index`, `create`).
|
|
318
318
|
* @property {string[]} [builtInMemberCommands] - Built-in member command names (`find`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
319
|
+
* @property {string} [modelName] - Frontend model name override.
|
|
319
320
|
* @property {string[]} [relationships] - Relationship names to expose in frontend models. Type and target model are inferred from the backend model's registered relationships.
|
|
320
321
|
* @property {string} [primaryKey] - Primary key attribute name.
|
|
321
322
|
* @property {FrontendModelResourceServerConfiguration} [server] - Optional legacy backend behavior overrides for built-in frontend actions.
|
|
@@ -1439,7 +1439,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1439
1439
|
*/
|
|
1440
1440
|
frontendAttributeIsTranslated({attributeName, modelClass, resourceClass}) {
|
|
1441
1441
|
if (resourceClass) {
|
|
1442
|
-
const translatedAttributes = resourceClass.
|
|
1442
|
+
const translatedAttributes = resourceClass.translatedAttributesConfig()
|
|
1443
1443
|
|
|
1444
1444
|
if (Array.isArray(translatedAttributes) && translatedAttributes.includes(attributeName)) return true
|
|
1445
1445
|
}
|
|
@@ -2430,7 +2430,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2430
2430
|
|
|
2431
2431
|
const resource = this.frontendModelResourceInstance()
|
|
2432
2432
|
const resourceClass = /** @type {typeof import("./frontend-model-resource/base-resource.js").default} */ (resource.constructor)
|
|
2433
|
-
const translatedSet = new Set(resourceClass.
|
|
2433
|
+
const translatedSet = new Set(resourceClass.translatedAttributesConfig() || [])
|
|
2434
2434
|
let needsTranslations = false
|
|
2435
2435
|
|
|
2436
2436
|
for (const attributeName of selectedAttributes) {
|
|
@@ -2667,10 +2667,10 @@ export default class FrontendModelController extends Controller {
|
|
|
2667
2667
|
* Resource has attribute.
|
|
2668
2668
|
* @param {string} attributeName - Attribute name.
|
|
2669
2669
|
*/
|
|
2670
|
-
const
|
|
2670
|
+
const resourceAttributeMethod = (attributeName) => {
|
|
2671
2671
|
const methodName = resourceAttributeMethodName(attributeName)
|
|
2672
2672
|
|
|
2673
|
-
return resourceInstance
|
|
2673
|
+
return resourceInstance?.resourceMethod(methodName) || null
|
|
2674
2674
|
}
|
|
2675
2675
|
|
|
2676
2676
|
/**
|
|
@@ -2700,10 +2700,10 @@ export default class FrontendModelController extends Controller {
|
|
|
2700
2700
|
*/
|
|
2701
2701
|
const serializedAttributeValue = async (attributeName) => {
|
|
2702
2702
|
// Check resource instance first (virtual/computed attributes via ${name}Attribute convention)
|
|
2703
|
-
|
|
2704
|
-
const methodName = resourceAttributeMethodName(attributeName)
|
|
2703
|
+
const resourceAttribute = resourceAttributeMethod(attributeName)
|
|
2705
2704
|
|
|
2706
|
-
|
|
2705
|
+
if (resourceAttribute) {
|
|
2706
|
+
return await resourceAttribute.method.call(resourceAttribute.resource, model)
|
|
2707
2707
|
}
|
|
2708
2708
|
|
|
2709
2709
|
// Fall back to model method
|
|
@@ -2722,7 +2722,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2722
2722
|
* @param {string} attributeName - Attribute name.
|
|
2723
2723
|
*/
|
|
2724
2724
|
const attributeExists = (attributeName) => {
|
|
2725
|
-
return (attributeName in modelAttributes) || (attributeName in /** @type {Record<string, ?>} */ (model)) ||
|
|
2725
|
+
return (attributeName in modelAttributes) || (attributeName in /** @type {Record<string, ?>} */ (model)) || Boolean(resourceAttributeMethod(attributeName))
|
|
2726
2726
|
}
|
|
2727
2727
|
|
|
2728
2728
|
if (!selectedAttributes) {
|
|
@@ -3770,10 +3770,10 @@ export default class FrontendModelController extends Controller {
|
|
|
3770
3770
|
return this.frontendModelErrorPayload("Expected frontend-model custom command scope.")
|
|
3771
3771
|
}
|
|
3772
3772
|
|
|
3773
|
-
const resource =
|
|
3774
|
-
const
|
|
3773
|
+
const resource = this.frontendModelResourceInstance()
|
|
3774
|
+
const command = resource.resourceMethod(methodName)
|
|
3775
3775
|
|
|
3776
|
-
if (
|
|
3776
|
+
if (!command) {
|
|
3777
3777
|
return this.frontendModelErrorPayload(`Missing frontend-model custom command '${methodName}'.`)
|
|
3778
3778
|
}
|
|
3779
3779
|
|
|
@@ -3783,7 +3783,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3783
3783
|
// unchanged, so existing parameterless methods keep working. The args are untrusted
|
|
3784
3784
|
// client input typed only by the declared contract, so methods must still validate.
|
|
3785
3785
|
const commandArguments = this.frontendModelCustomCommandArguments(params)
|
|
3786
|
-
const responsePayload = await
|
|
3786
|
+
const responsePayload = await command.method.call(command.resource, commandArguments)
|
|
3787
3787
|
|
|
3788
3788
|
if (!responsePayload || typeof responsePayload !== "object") {
|
|
3789
3789
|
return {status: "success"}
|
|
@@ -3792,7 +3792,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3792
3792
|
return /** @type {Record<string, ?>} */ (
|
|
3793
3793
|
await this.autoSerializeFrontendModelsInPayload(
|
|
3794
3794
|
responsePayload,
|
|
3795
|
-
/** @type {{serialize: (model: ?, action: string) => Promise<Record<string, ?>>}} */ (resource),
|
|
3795
|
+
/** @type {{serialize: (model: ?, action: string) => Promise<Record<string, ?>>}} */ (command.resource),
|
|
3796
3796
|
methodName
|
|
3797
3797
|
)
|
|
3798
3798
|
)
|
|
@@ -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
|