velocious 1.0.448 → 1.0.449
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/database/record/index.js +13 -8
- package/build/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
- package/build/frontend-model-controller.js +9 -0
- package/build/frontend-model-resource/base-resource.js +266 -53
- package/build/frontend-models/base.js +241 -97
- package/build/frontend-models/preloader.js +3 -2
- package/build/src/background-jobs/job-record.d.ts +2 -1
- package/build/src/background-jobs/job-record.d.ts.map +1 -1
- package/build/src/database/query/preloader/belongs-to.d.ts +2 -2
- package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.d.ts +1 -1
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-one.d.ts +2 -2
- package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
- package/build/src/database/query/preloader.d.ts +1 -1
- package/build/src/database/query/preloader.d.ts.map +1 -1
- package/build/src/database/record/attachments/handle.d.ts +1 -1
- package/build/src/database/record/attachments/handle.d.ts.map +1 -1
- package/build/src/database/record/index.d.ts +23 -13
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +14 -9
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +2 -15
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +89 -32
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +123 -72
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +8 -1
- package/build/src/frontend-model-resource/base-resource.d.ts +203 -64
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +237 -54
- package/build/src/frontend-models/base.d.ts +173 -110
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +218 -102
- package/build/src/frontend-models/preloader.d.ts.map +1 -1
- package/build/src/frontend-models/preloader.js +4 -3
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
- package/build/src/testing/expect.d.ts +6 -0
- package/build/src/testing/expect.d.ts.map +1 -1
- package/build/src/testing/expect.js +9 -1
- package/build/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
- package/build/testing/expect.js +9 -0
- package/package.json +1 -1
- package/src/database/record/index.js +13 -8
- package/src/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
- package/src/frontend-model-controller.js +9 -0
- package/src/frontend-model-resource/base-resource.js +266 -53
- package/src/frontend-models/base.js +241 -97
- package/src/frontend-models/preloader.js +3 -2
- package/src/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
- package/src/testing/expect.js +9 -0
|
@@ -4,10 +4,29 @@ import AuthorizationBaseResource from "../authorization/base-resource.js"
|
|
|
4
4
|
import * as inflection from "inflection"
|
|
5
5
|
import isPlainObject from "../utils/plain-object.js"
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Built-in frontend-model resource action.
|
|
9
|
+
* @typedef {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} FrontendModelResourceAction
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Frontend-model controller methods used by resources.
|
|
14
|
+
* @typedef {import("../controller.js").default & {
|
|
15
|
+
* currentAbility: () => import("../authorization/ability.js").default | undefined,
|
|
16
|
+
* frontendModelAbilityAction: (action: FrontendModelResourceAction) => string,
|
|
17
|
+
* frontendModelAuthorizedQuery: (action: FrontendModelResourceAction) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
|
|
18
|
+
* frontendModelIndexQuery: () => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
|
|
19
|
+
* frontendModelParams: () => import("../configuration-types.js").VelociousParams,
|
|
20
|
+
* frontendModelPreload: () => import("../database/query/index.js").NestedPreloadRecord | null,
|
|
21
|
+
* frontendModelResourceConfigurationForModelClass: (modelClass: typeof import("../database/record/index.js").default) => FrontendModelResolvedResourceConfiguration | null,
|
|
22
|
+
* serializeFrontendModel: (model: import("../database/record/index.js").default) => Promise<Record<string, object | string | number | boolean | null>>
|
|
23
|
+
* }} FrontendModelResourceController
|
|
24
|
+
*/
|
|
25
|
+
|
|
7
26
|
/**
|
|
8
27
|
* FrontendModelResourceControllerArgs type.
|
|
9
28
|
* @typedef {object} FrontendModelResourceControllerArgs
|
|
10
|
-
* @property {
|
|
29
|
+
* @property {FrontendModelResourceController} controller - Frontend-model controller instance.
|
|
11
30
|
* @property {typeof import("../database/record/index.js").default} modelClass - Backing model class.
|
|
12
31
|
* @property {string} modelName - Model name.
|
|
13
32
|
* @property {import("../configuration-types.js").VelociousParams} params - Request params.
|
|
@@ -35,6 +54,31 @@ import isPlainObject from "../utils/plain-object.js"
|
|
|
35
54
|
* @property {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration} resourceConfiguration - Normalized resource configuration.
|
|
36
55
|
*/
|
|
37
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Transport-safe value accepted in frontend-model resource mutation payloads.
|
|
59
|
+
* Nested object/array values are intentionally opaque because TypeScript rejects
|
|
60
|
+
* recursive JSDoc typedefs for this transport payload contract.
|
|
61
|
+
* @typedef {import("../frontend-models/base.js").FrontendModelTransportValue | import("../database/record/index.js").default | Record<string, unknown> | Array<unknown>} FrontendModelResourcePayloadValue
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Attribute payload accepted by frontend-model resource mutations.
|
|
66
|
+
* @typedef {Record<string, FrontendModelResourcePayloadValue>} FrontendModelResourceAttributePayload
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Options passed while saving frontend-model resource mutations.
|
|
71
|
+
* @typedef {object} FrontendModelResourceSaveOptions
|
|
72
|
+
* @property {FrontendModelResourceAttributePayload | null} [attachments] - Uploaded attachment attributes.
|
|
73
|
+
* @property {FrontendModelResourceController | null} [controller] - Controller handling the mutation.
|
|
74
|
+
* @property {FrontendModelResourceAttributePayload | null} [nestedAttributes] - Nested attributes payload.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalized nested attributes entry.
|
|
79
|
+
* @typedef {FrontendModelResourceAttributePayload & {id?: string | number, _destroy?: boolean, attributes?: FrontendModelResourceAttributePayload, attachments?: FrontendModelResourceAttributePayload, nestedAttributes?: FrontendModelResourceAttributePayload}} FrontendModelResourceNestedEntry
|
|
80
|
+
*/
|
|
81
|
+
|
|
38
82
|
/**
|
|
39
83
|
* Base class for backend frontend-model resources.
|
|
40
84
|
* @template {typeof import("../database/record/index.js").default} [TModelClass=typeof import("../database/record/index.js").default]
|
|
@@ -108,17 +152,10 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
108
152
|
|
|
109
153
|
/**
|
|
110
154
|
* Runs typed controller instance.
|
|
111
|
-
* @returns {
|
|
112
|
-
* frontendModelAuthorizedQuery: (action: "index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url") => import("../database/query/model-class-query.js").default<TModelClass>,
|
|
113
|
-
* frontendModelAbilityAction: (action: string) => string,
|
|
114
|
-
* currentAbility: () => import("../authorization/ability.js").default | undefined,
|
|
115
|
-
* frontendModelIndexQuery: () => import("../database/query/model-class-query.js").default<TModelClass>,
|
|
116
|
-
* frontendModelPreload: () => import("../database/query/index.js").NestedPreloadRecord | null,
|
|
117
|
-
* serializeFrontendModel: (model: import("../database/record/index.js").default) => Promise<Record<string, unknown>>
|
|
118
|
-
* }} - Controller instance with frontend-model helpers.
|
|
155
|
+
* @returns {FrontendModelResourceController} - Controller instance with frontend-model helpers.
|
|
119
156
|
*/
|
|
120
157
|
typedControllerInstance() {
|
|
121
|
-
return /** Narrows the runtime value to the documented type. @type {
|
|
158
|
+
return /** Narrows the runtime value to the documented type. @type {FrontendModelResourceController} */ (this.controller)
|
|
122
159
|
}
|
|
123
160
|
|
|
124
161
|
/**
|
|
@@ -244,6 +281,118 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
244
281
|
return []
|
|
245
282
|
}
|
|
246
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Normalizes create attributes before permission filtering and saving.
|
|
286
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Incoming create attributes.
|
|
287
|
+
* @param {FrontendModelResourceSaveOptions} options - Save options.
|
|
288
|
+
* @returns {FrontendModelResourceAttributePayload | Promise<FrontendModelResourceAttributePayload>} - Normalized attributes.
|
|
289
|
+
*/
|
|
290
|
+
normalizeCreateAttributes(attributes, options) {
|
|
291
|
+
void options
|
|
292
|
+
|
|
293
|
+
return attributes
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Normalizes update attributes before permission filtering and saving.
|
|
298
|
+
* @param {import("../database/record/index.js").default} model - Existing model.
|
|
299
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Incoming update attributes.
|
|
300
|
+
* @param {FrontendModelResourceSaveOptions} options - Save options.
|
|
301
|
+
* @returns {FrontendModelResourceAttributePayload | Promise<FrontendModelResourceAttributePayload>} - Normalized attributes.
|
|
302
|
+
*/
|
|
303
|
+
normalizeUpdateAttributes(model, attributes, options) {
|
|
304
|
+
void model
|
|
305
|
+
void options
|
|
306
|
+
|
|
307
|
+
return attributes
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Runs before create.
|
|
312
|
+
* @param {import("../database/record/index.js").default} model - New model before assignment/save.
|
|
313
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Normalized create attributes.
|
|
314
|
+
* @param {FrontendModelResourceSaveOptions} options - Save options.
|
|
315
|
+
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
316
|
+
*/
|
|
317
|
+
beforeCreate(model, attributes, options) {
|
|
318
|
+
void model
|
|
319
|
+
void attributes
|
|
320
|
+
void options
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Runs after create.
|
|
325
|
+
* @param {import("../database/record/index.js").default} model - Created model.
|
|
326
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Normalized create attributes.
|
|
327
|
+
* @param {FrontendModelResourceSaveOptions} options - Save options.
|
|
328
|
+
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
329
|
+
*/
|
|
330
|
+
afterCreate(model, attributes, options) {
|
|
331
|
+
void model
|
|
332
|
+
void attributes
|
|
333
|
+
void options
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Runs before update.
|
|
338
|
+
* @param {import("../database/record/index.js").default} model - Existing model before assignment/save.
|
|
339
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Normalized update attributes.
|
|
340
|
+
* @param {FrontendModelResourceSaveOptions} options - Save options.
|
|
341
|
+
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
342
|
+
*/
|
|
343
|
+
beforeUpdate(model, attributes, options) {
|
|
344
|
+
void model
|
|
345
|
+
void attributes
|
|
346
|
+
void options
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Runs after update.
|
|
351
|
+
* @param {import("../database/record/index.js").default} model - Updated model.
|
|
352
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Normalized update attributes.
|
|
353
|
+
* @param {FrontendModelResourceSaveOptions} options - Save options.
|
|
354
|
+
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
355
|
+
*/
|
|
356
|
+
afterUpdate(model, attributes, options) {
|
|
357
|
+
void model
|
|
358
|
+
void attributes
|
|
359
|
+
void options
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Runs before destroy.
|
|
364
|
+
* @param {import("../database/record/index.js").default} model - Model before destroy.
|
|
365
|
+
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
366
|
+
*/
|
|
367
|
+
beforeDestroy(model) {
|
|
368
|
+
void model
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Runs after destroy.
|
|
373
|
+
* @param {import("../database/record/index.js").default} model - Destroyed model.
|
|
374
|
+
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
375
|
+
*/
|
|
376
|
+
afterDestroy(model) {
|
|
377
|
+
void model
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Wraps create/update/destroy resource mutations.
|
|
382
|
+
* @template Result
|
|
383
|
+
* @param {object} args - Transaction args.
|
|
384
|
+
* @param {"create" | "update" | "destroy"} args.action - Mutation action.
|
|
385
|
+
* @param {import("../database/record/index.js").default} args.model - Mutated model.
|
|
386
|
+
* @param {() => Promise<Result>} args.callback - Mutation callback.
|
|
387
|
+
* @returns {Promise<Result>} - Callback result.
|
|
388
|
+
*/
|
|
389
|
+
async runMutationTransaction({action, model, callback}) {
|
|
390
|
+
void action
|
|
391
|
+
void model
|
|
392
|
+
|
|
393
|
+
return await callback()
|
|
394
|
+
}
|
|
395
|
+
|
|
247
396
|
/**
|
|
248
397
|
* Runs primary key.
|
|
249
398
|
* @returns {string} - Primary key.
|
|
@@ -252,11 +401,12 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
252
401
|
|
|
253
402
|
/**
|
|
254
403
|
* Runs authorized query.
|
|
255
|
-
* @param {
|
|
404
|
+
* @param {FrontendModelResourceAction} action - Ability action.
|
|
256
405
|
* @returns {import("../database/query/model-class-query.js").default<TModelClass>} - Authorized query.
|
|
257
406
|
*/
|
|
258
407
|
authorizedQuery(action) {
|
|
259
|
-
|
|
408
|
+
// Narrows the controller query to this resource's model class.
|
|
409
|
+
return /** @type {import("../database/query/model-class-query.js").default<TModelClass>} */ (this.typedControllerInstance().frontendModelAuthorizedQuery(action))
|
|
260
410
|
}
|
|
261
411
|
|
|
262
412
|
|
|
@@ -329,18 +479,30 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
329
479
|
|
|
330
480
|
/**
|
|
331
481
|
* Runs create.
|
|
332
|
-
* @param {
|
|
333
|
-
* @param {
|
|
482
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Create attributes.
|
|
483
|
+
* @param {FrontendModelResourceSaveOptions} [options] - Save options.
|
|
334
484
|
* @returns {Promise<import("../database/record/index.js").default>} - Created model.
|
|
335
485
|
*/
|
|
336
486
|
async create(attributes, options = {}) {
|
|
337
|
-
const
|
|
338
|
-
const
|
|
487
|
+
const normalizedAttributes = await this.normalizeCreateAttributes(attributes, options)
|
|
488
|
+
const attachmentSplit = this._extractAttachmentAttributes(normalizedAttributes, options.attachments ?? null)
|
|
489
|
+
const permit = parsePermittedParams(this.permittedParams({action: "create", ability: this.ability, locals: this.locals, params: normalizedAttributes}))
|
|
339
490
|
const filtered = filterWritableFrontendModelAttributes(this.modelClass().prototype, attachmentSplit.attributes, this, permit.attributes)
|
|
340
491
|
const ModelClass = this.modelClass()
|
|
341
492
|
const model = new ModelClass()
|
|
342
493
|
|
|
343
|
-
return await this.
|
|
494
|
+
return await this.runMutationTransaction({
|
|
495
|
+
action: "create",
|
|
496
|
+
model,
|
|
497
|
+
callback: async () => {
|
|
498
|
+
await this.beforeCreate(model, normalizedAttributes, options)
|
|
499
|
+
const savedModel = await this._saveWithNestedAttributes({filtered, model, options: {...options, attachments: attachmentSplit.attachments}, permit})
|
|
500
|
+
|
|
501
|
+
await this.afterCreate(savedModel, normalizedAttributes, options)
|
|
502
|
+
|
|
503
|
+
return savedModel
|
|
504
|
+
}
|
|
505
|
+
})
|
|
344
506
|
}
|
|
345
507
|
|
|
346
508
|
/**
|
|
@@ -355,21 +517,33 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
355
517
|
/**
|
|
356
518
|
* Runs update.
|
|
357
519
|
* @param {import("../database/record/index.js").default} model - Existing model.
|
|
358
|
-
* @param {
|
|
359
|
-
* @param {
|
|
520
|
+
* @param {FrontendModelResourceAttributePayload} attributes - Update attributes.
|
|
521
|
+
* @param {FrontendModelResourceSaveOptions} [options] - Save options.
|
|
360
522
|
* @returns {Promise<import("../database/record/index.js").default>} - Updated model.
|
|
361
523
|
*/
|
|
362
524
|
async update(model, attributes, options = {}) {
|
|
363
|
-
const
|
|
364
|
-
const
|
|
525
|
+
const normalizedAttributes = await this.normalizeUpdateAttributes(model, attributes, options)
|
|
526
|
+
const attachmentSplit = this._extractAttachmentAttributes(normalizedAttributes, options.attachments ?? null)
|
|
527
|
+
const permit = parsePermittedParams(this.permittedParams({action: "update", ability: this.ability, locals: this.locals, params: normalizedAttributes}))
|
|
365
528
|
const filtered = filterWritableFrontendModelAttributes(model, attachmentSplit.attributes, this, permit.attributes)
|
|
366
529
|
|
|
367
|
-
return await this.
|
|
530
|
+
return await this.runMutationTransaction({
|
|
531
|
+
action: "update",
|
|
532
|
+
model,
|
|
533
|
+
callback: async () => {
|
|
534
|
+
await this.beforeUpdate(model, normalizedAttributes, options)
|
|
535
|
+
const savedModel = await this._saveWithNestedAttributes({filtered, model, options: {...options, attachments: attachmentSplit.attachments}, permit})
|
|
536
|
+
|
|
537
|
+
await this.afterUpdate(savedModel, normalizedAttributes, options)
|
|
538
|
+
|
|
539
|
+
return savedModel
|
|
540
|
+
}
|
|
541
|
+
})
|
|
368
542
|
}
|
|
369
543
|
|
|
370
544
|
/**
|
|
371
545
|
* Saves a model and applies nested attributes in one transaction.
|
|
372
|
-
* @param {{filtered: Record<string, ?>, model: import("../database/record/index.js").default, options:
|
|
546
|
+
* @param {{filtered: Record<string, ?>, model: import("../database/record/index.js").default, options: FrontendModelResourceSaveOptions, permit: {attributes: string[], nested: Record<string, ?>}}} args - Save arguments.
|
|
373
547
|
* @returns {Promise<import("../database/record/index.js").default>} - Saved model.
|
|
374
548
|
*/
|
|
375
549
|
async _saveWithNestedAttributes({filtered, model, options, permit}) {
|
|
@@ -516,7 +690,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
516
690
|
* Sets a translated attribute on a model via the translations relationship.
|
|
517
691
|
* @param {import("../database/record/index.js").default} model - Model instance.
|
|
518
692
|
* @param {string} name - Attribute name.
|
|
519
|
-
* @param {
|
|
693
|
+
* @param {FrontendModelResourcePayloadValue} value - Attribute value.
|
|
520
694
|
* @returns {Promise<void>}
|
|
521
695
|
*/
|
|
522
696
|
async _setTranslatedAttributeOnModel(model, name, value) {
|
|
@@ -569,7 +743,15 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
569
743
|
* @returns {Promise<void>} - No return value.
|
|
570
744
|
*/
|
|
571
745
|
async destroy(model) {
|
|
572
|
-
await
|
|
746
|
+
await this.runMutationTransaction({
|
|
747
|
+
action: "destroy",
|
|
748
|
+
model,
|
|
749
|
+
callback: async () => {
|
|
750
|
+
await this.beforeDestroy(model)
|
|
751
|
+
await model.destroy()
|
|
752
|
+
await this.afterDestroy(model)
|
|
753
|
+
}
|
|
754
|
+
})
|
|
573
755
|
}
|
|
574
756
|
|
|
575
757
|
/**
|
|
@@ -589,10 +771,10 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
589
771
|
* @param {object} args - Nested relationship inputs.
|
|
590
772
|
* @param {import("../database/record/index.js").default} args.parent - Parent model instance.
|
|
591
773
|
* @param {string} args.relationshipName - Relationship receiving nested attributes.
|
|
592
|
-
* @param {
|
|
774
|
+
* @param {FrontendModelResourcePayloadValue} args.rawEntries - Raw nested entries from the request payload.
|
|
593
775
|
* @param {{attributes: string[], nested: Record<string, ?>}} args.childPermit - Parsed child permit.
|
|
594
|
-
* @param {
|
|
595
|
-
* @returns {{ability: import("../authorization/ability.js").default | undefined, childResource: FrontendModelBaseResource, childResourceConfig: FrontendModelResolvedResourceConfiguration, childWritableAttributes: string[], destroyPermitted: boolean, entries: Array<
|
|
776
|
+
* @param {FrontendModelResourceController | null | undefined} args.controller - Controller instance for child resource lookup.
|
|
777
|
+
* @returns {{ability: import("../authorization/ability.js").default | undefined, childResource: FrontendModelBaseResource, childResourceConfig: FrontendModelResolvedResourceConfiguration, childWritableAttributes: string[], destroyPermitted: boolean, entries: Array<FrontendModelResourceNestedEntry>, relationship: import("../database/record/relationships/base.js").default, targetModelClass: typeof import("../database/record/index.js").default}} Nested relationship context.
|
|
596
778
|
*/
|
|
597
779
|
_nestedRelationshipContext({parent, relationshipName, rawEntries, childPermit, controller}) {
|
|
598
780
|
const parentModelClass = parent.getModelClass()
|
|
@@ -631,7 +813,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
631
813
|
|
|
632
814
|
const childResource = new childResourceConfig.resourceClass({
|
|
633
815
|
ability: this.ability,
|
|
634
|
-
controller,
|
|
816
|
+
controller: controller || undefined,
|
|
635
817
|
context: this.context || {},
|
|
636
818
|
locals: this.locals || {},
|
|
637
819
|
modelClass: targetModelClass,
|
|
@@ -663,10 +845,10 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
663
845
|
/**
|
|
664
846
|
* Normalizes nested entries for collection and singular relationships.
|
|
665
847
|
* @param {object} args - Nested entries inputs.
|
|
666
|
-
* @param {
|
|
848
|
+
* @param {FrontendModelResourcePayloadValue} args.rawEntries - Raw nested entries value.
|
|
667
849
|
* @param {string} args.relationshipName - Relationship name.
|
|
668
850
|
* @param {string} args.relationshipType - Relationship type.
|
|
669
|
-
* @returns {Array<
|
|
851
|
+
* @returns {Array<FrontendModelResourceNestedEntry>} Normalized nested entry objects.
|
|
670
852
|
*/
|
|
671
853
|
_nestedRelationshipEntries({rawEntries, relationshipName, relationshipType}) {
|
|
672
854
|
if (relationshipType === "hasMany") {
|
|
@@ -677,7 +859,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
677
859
|
return rawEntries.map((entry) => {
|
|
678
860
|
if (!isPlainObject(entry)) throw new Error(`nestedAttributes['${relationshipName}'] entries must be objects.`)
|
|
679
861
|
|
|
680
|
-
|
|
862
|
+
// Narrows the plain-object payload to a normalized nested-entry object.
|
|
863
|
+
return /** @type {FrontendModelResourceNestedEntry} */ (entry)
|
|
681
864
|
})
|
|
682
865
|
}
|
|
683
866
|
|
|
@@ -686,14 +869,16 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
686
869
|
return rawEntries.map((entry) => {
|
|
687
870
|
if (!isPlainObject(entry)) throw new Error(`nestedAttributes['${relationshipName}'] entries must be objects.`)
|
|
688
871
|
|
|
689
|
-
|
|
872
|
+
// Narrows the plain-object payload to a normalized nested-entry object.
|
|
873
|
+
return /** @type {FrontendModelResourceNestedEntry} */ (entry)
|
|
690
874
|
})
|
|
691
875
|
}
|
|
692
876
|
if (!isPlainObject(rawEntries)) {
|
|
693
877
|
throw new Error(`Expected object for nestedAttributes['${relationshipName}'] but got: ${typeof rawEntries}`)
|
|
694
878
|
}
|
|
695
879
|
|
|
696
|
-
|
|
880
|
+
// Narrows the plain-object payload to a normalized nested-entry object.
|
|
881
|
+
return [/** @type {FrontendModelResourceNestedEntry} */ (rawEntries)]
|
|
697
882
|
}
|
|
698
883
|
|
|
699
884
|
/**
|
|
@@ -702,25 +887,38 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
702
887
|
* fields (`{name, file, commentsAttributes}`).
|
|
703
888
|
* @param {object} args - Normalization inputs.
|
|
704
889
|
* @param {{attributes: string[], nested: Record<string, ?>}} args.childPermit - Parsed child permit spec.
|
|
705
|
-
* @param {
|
|
890
|
+
* @param {FrontendModelResourceNestedEntry} args.entry - Raw nested entry.
|
|
706
891
|
* @param {string} args.relationshipName - Relationship name for error messages.
|
|
707
892
|
* @param {typeof import("../database/record/index.js").default} args.targetModelClass - Child model class.
|
|
708
|
-
* @returns {
|
|
893
|
+
* @returns {FrontendModelResourceNestedEntry} Normalized nested entry.
|
|
709
894
|
*/
|
|
710
895
|
_normalizeNestedRelationshipEntry({childPermit, entry, relationshipName, targetModelClass}) {
|
|
711
|
-
/** @type {
|
|
896
|
+
/** @type {FrontendModelResourceAttributePayload} */
|
|
712
897
|
const attributes = {}
|
|
713
|
-
/** @type {
|
|
898
|
+
/** @type {FrontendModelResourceAttributePayload} */
|
|
714
899
|
const attachments = {}
|
|
715
|
-
/** @type {
|
|
900
|
+
/** @type {FrontendModelResourceAttributePayload} */
|
|
716
901
|
const nestedAttributes = {}
|
|
717
|
-
/** @type {
|
|
902
|
+
/** @type {FrontendModelResourceNestedEntry} */
|
|
718
903
|
const normalized = {}
|
|
719
904
|
const attachmentDefinitions = targetModelClass.getAttachmentsMap?.() || {}
|
|
720
905
|
|
|
721
906
|
for (const [attributeName, value] of Object.entries(entry)) {
|
|
722
|
-
if (attributeName === "id"
|
|
723
|
-
|
|
907
|
+
if (attributeName === "id") {
|
|
908
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
909
|
+
throw new Error(`nestedAttributes['${relationshipName}'] entry id must be a string or number.`)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
normalized.id = value
|
|
913
|
+
continue
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (attributeName === "_destroy") {
|
|
917
|
+
if (typeof value !== "boolean") {
|
|
918
|
+
throw new Error(`nestedAttributes['${relationshipName}'] entry _destroy must be a boolean.`)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
normalized._destroy = value
|
|
724
922
|
continue
|
|
725
923
|
}
|
|
726
924
|
|
|
@@ -771,8 +969,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
771
969
|
/**
|
|
772
970
|
* Applies belongs-to nested attributes before the parent save so the parent FK can be set.
|
|
773
971
|
* @param {import("../database/record/index.js").default} parent - Parent model instance.
|
|
774
|
-
* @param {
|
|
775
|
-
* @param {
|
|
972
|
+
* @param {FrontendModelResourceAttributePayload} nestedAttributes - Nested-attribute payload keyed by relationship name.
|
|
973
|
+
* @param {FrontendModelResourceController | null | undefined} controller - Controller instance for resource resolution and authorization.
|
|
776
974
|
* @param {{attributes: string[], nested: Record<string, ?>} | null} [parentPermit] - Parsed parent permit spec.
|
|
777
975
|
* @returns {Promise<void>}
|
|
778
976
|
*/
|
|
@@ -802,13 +1000,15 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
802
1000
|
if (!context.destroyPermitted) {
|
|
803
1001
|
throw new Error(`nestedAttributes['${relationshipName}'] entry requested _destroy but "_destroy" is not in the permit for this relationship.`)
|
|
804
1002
|
}
|
|
805
|
-
|
|
1003
|
+
const id = entry.id
|
|
1004
|
+
|
|
1005
|
+
if (id == undefined) throw new Error(`nestedAttributes['${relationshipName}'] _destroy entry is missing an id.`)
|
|
806
1006
|
|
|
807
1007
|
const existing = await this._findNestedRecord({
|
|
808
1008
|
ability: context.ability,
|
|
809
1009
|
action: "destroy",
|
|
810
1010
|
childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
|
|
811
|
-
id
|
|
1011
|
+
id,
|
|
812
1012
|
relationshipName,
|
|
813
1013
|
targetModelClass: context.targetModelClass
|
|
814
1014
|
})
|
|
@@ -818,12 +1018,13 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
818
1018
|
continue
|
|
819
1019
|
}
|
|
820
1020
|
|
|
821
|
-
const
|
|
1021
|
+
const id = entry.id
|
|
1022
|
+
const child = id != undefined
|
|
822
1023
|
? await this._findNestedRecord({
|
|
823
1024
|
ability: context.ability,
|
|
824
1025
|
action: "update",
|
|
825
1026
|
childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
|
|
826
|
-
id
|
|
1027
|
+
id,
|
|
827
1028
|
relationshipName,
|
|
828
1029
|
targetModelClass: context.targetModelClass
|
|
829
1030
|
})
|
|
@@ -837,7 +1038,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
837
1038
|
await context.childResource._applyBelongsToNestedAttributes(child, entry.nestedAttributes || {}, controller, childPermit)
|
|
838
1039
|
await child.save()
|
|
839
1040
|
|
|
840
|
-
if (
|
|
1041
|
+
if (id == undefined) {
|
|
841
1042
|
await this._authorizeCreatedChild({
|
|
842
1043
|
ability: context.ability,
|
|
843
1044
|
child,
|
|
@@ -869,8 +1070,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
869
1070
|
* (allowDestroy, limit, rejectIf) come from the MODEL's
|
|
870
1071
|
* `acceptedNestedAttributesFor(name)` declaration.
|
|
871
1072
|
* @param {import("../database/record/index.js").default} parent - Parent model instance.
|
|
872
|
-
* @param {
|
|
873
|
-
* @param {
|
|
1073
|
+
* @param {FrontendModelResourceAttributePayload} nestedAttributes - Nested-attribute payload keyed by relationship name.
|
|
1074
|
+
* @param {FrontendModelResourceController | null | undefined} controller - Controller instance for resource resolution and authorization.
|
|
874
1075
|
* @param {{attributes: string[], nested: Record<string, ?>} | null} [parentPermit] - Parsed parent permit spec.
|
|
875
1076
|
* @returns {Promise<void>}
|
|
876
1077
|
*/
|
|
@@ -922,11 +1123,17 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
922
1123
|
}
|
|
923
1124
|
|
|
924
1125
|
for (const entry of destroyEntries) {
|
|
1126
|
+
const id = entry.id
|
|
1127
|
+
|
|
1128
|
+
if (id == undefined) {
|
|
1129
|
+
throw new Error(`nestedAttributes['${relationshipName}'] _destroy entry is missing an id.`)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
925
1132
|
const existing = await this._findScopedChild({
|
|
926
1133
|
ability: context.ability,
|
|
927
1134
|
action: "destroy",
|
|
928
1135
|
childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
|
|
929
|
-
id
|
|
1136
|
+
id,
|
|
930
1137
|
parent,
|
|
931
1138
|
parentLinkAttributes,
|
|
932
1139
|
relationshipName,
|
|
@@ -937,11 +1144,17 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
937
1144
|
}
|
|
938
1145
|
|
|
939
1146
|
for (const entry of updateEntries) {
|
|
1147
|
+
const id = entry.id
|
|
1148
|
+
|
|
1149
|
+
if (id == undefined) {
|
|
1150
|
+
throw new Error(`nestedAttributes['${relationshipName}'] update entry is missing an id.`)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
940
1153
|
const existing = await this._findScopedChild({
|
|
941
1154
|
ability: context.ability,
|
|
942
1155
|
action: "update",
|
|
943
1156
|
childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
|
|
944
|
-
id
|
|
1157
|
+
id,
|
|
945
1158
|
parent,
|
|
946
1159
|
parentLinkAttributes,
|
|
947
1160
|
relationshipName,
|