velocious 1.0.642 → 1.0.643

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.
Files changed (50) hide show
  1. package/README.md +16 -2
  2. package/build/authorization/base-resource.js +18 -5
  3. package/build/configuration-types.js +34 -2
  4. package/build/database/record/index.js +42 -11
  5. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +17 -3
  6. package/build/frontend-model-controller.js +10 -5
  7. package/build/frontend-model-resource/base-resource.js +95 -41
  8. package/build/frontend-models/base.js +1 -1
  9. package/build/src/authorization/base-resource.d.ts +19 -6
  10. package/build/src/authorization/base-resource.d.ts.map +1 -1
  11. package/build/src/authorization/base-resource.js +16 -6
  12. package/build/src/configuration-types.d.ts +72 -5
  13. package/build/src/configuration-types.d.ts.map +1 -1
  14. package/build/src/configuration-types.js +31 -3
  15. package/build/src/database/record/index.d.ts +44 -46
  16. package/build/src/database/record/index.d.ts.map +1 -1
  17. package/build/src/database/record/index.js +39 -12
  18. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  19. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +19 -4
  20. package/build/src/frontend-model-controller.d.ts +1 -1
  21. package/build/src/frontend-model-controller.d.ts.map +1 -1
  22. package/build/src/frontend-model-controller.js +9 -6
  23. package/build/src/frontend-model-resource/base-resource.d.ts +66 -32
  24. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  25. package/build/src/frontend-model-resource/base-resource.js +89 -42
  26. package/build/src/frontend-models/base.d.ts +1 -0
  27. package/build/src/frontend-models/base.d.ts.map +1 -1
  28. package/build/src/frontend-models/base.js +2 -2
  29. package/build/src/sync/sync-api-controller.d.ts.map +1 -1
  30. package/build/src/sync/sync-api-controller.js +6 -5
  31. package/build/src/sync/sync-envelope-replay-service.d.ts +1 -1
  32. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  33. package/build/src/sync/sync-envelope-replay-service.js +3 -2
  34. package/build/src/sync/sync-resource-base.d.ts +2 -0
  35. package/build/src/sync/sync-resource-base.d.ts.map +1 -1
  36. package/build/src/sync/sync-resource-base.js +3 -1
  37. package/build/sync/sync-api-controller.js +5 -5
  38. package/build/sync/sync-envelope-replay-service.js +2 -1
  39. package/build/sync/sync-resource-base.js +3 -0
  40. package/package.json +1 -1
  41. package/src/authorization/base-resource.js +18 -5
  42. package/src/configuration-types.js +34 -2
  43. package/src/database/record/index.js +42 -11
  44. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +17 -3
  45. package/src/frontend-model-controller.js +10 -5
  46. package/src/frontend-model-resource/base-resource.js +95 -41
  47. package/src/frontend-models/base.js +1 -1
  48. package/src/sync/sync-api-controller.js +5 -5
  49. package/src/sync/sync-envelope-replay-service.js +2 -1
  50. package/src/sync/sync-resource-base.js +3 -0
package/README.md CHANGED
@@ -47,7 +47,7 @@
47
47
  * AbortSignal-driven MySQL/MariaDB query cancellation for raw, model, and cross-tenant aggregate queries (see [docs/database-query-cancellation.md](docs/database-query-cancellation.md))
48
48
  * Optional built-in debug endpoint for inspecting server and database connection state (see [docs/debug-endpoint.md](docs/debug-endpoint.md))
49
49
  * Optional built-in API manifest endpoint describing every registered frontend-model resource as human- and machine-readable JSON (see [docs/api-manifest-endpoint.md](docs/api-manifest-endpoint.md))
50
- * Backend record attachments with filesystem, S3, native callback, and bounded Node path-input persistence (see [docs/attachments.md](docs/attachments.md))
50
+ * Backend record attachments with filesystem, S3, native callback, bounded Node path-input persistence, and model-declared client sync policy (see [docs/attachments.md](docs/attachments.md))
51
51
 
52
52
  # Setup
53
53
 
@@ -701,6 +701,13 @@ For backend models, you can declare attachment helpers directly:
701
701
  Task.hasManyAttachments("files")
702
702
  Task.hasOneAttachment("descriptionFile")
703
703
  Task.hasOneAttachment("archivedPdf", {driver: "s3"})
704
+ User.hasOneAttachment("profilePicture", {
705
+ sync: {
706
+ fetch: "eager",
707
+ offlineRequirement: "optional",
708
+ retention: "evictable"
709
+ }
710
+ })
704
711
  ```
705
712
 
706
713
  `db:migrate` provisions the framework-owned attachment table before runtime
@@ -815,7 +822,9 @@ behavior. See
815
822
  [docs/attachments.md](docs/attachments.md#normalized-storage-driver-input) for
816
823
  the normalized input passed to custom drivers.
817
824
 
818
- For frontend models, configure `resourceConfig().attachments` and use:
825
+ For a resource with a backing model, the model attachment declaration
826
+ automatically generates `resourceConfig().attachments`; do not repeat it on the
827
+ resource. Use the generated attachment handles normally:
819
828
 
820
829
  ```js
821
830
  await frontendTask.update({descriptionFile: file})
@@ -828,6 +837,11 @@ await frontendTask.attach(file)
828
837
 
829
838
  Frontend model attachment input does not support `{path: ...}`.
830
839
  Use `File`/`Blob`/bytes/`contentBase64` payloads instead.
840
+ The optional model-level `sync` block is client-safe policy metadata for asset
841
+ cache adapters. It distinguishes eager/on-demand fetching,
842
+ durable/evictable retention, and optional/required offline availability.
843
+ Required offline assets must be durable. Backend driver configuration never
844
+ appears in generated frontend models or API manifests.
831
845
  Attachment metadata is exposed through the built-in `VelociousAttachment` frontend model with safe fields only: `id`, `recordType`, `recordId`, `name`, `position`, `filename`, `contentType`, `byteSize`, `createdAt`, and `updatedAt`. Storage internals such as `driver`, `storageKey`, and `contentBase64` remain hidden and non-queryable. Direct metadata queries require owner filters: `recordType`, `recordId`, and `name`.
832
846
 
833
847
  When your frontend app calls a backend on another host/port (or under a path prefix), configure transport once:
@@ -1,10 +1,15 @@
1
1
  // @ts-check
2
2
 
3
+ /**
4
+ * Model class supported by authorization and shared frontend-model resources.
5
+ * @typedef {{new (): import("../database/record/index.js").default | import("../frontend-models/base.js").default, getModelName: () => string}} AuthorizationResourceModelClass
6
+ */
7
+
3
8
  /** Base class for authorization resources defining abilities for a model. */
4
9
  export default class AuthorizationBaseResource {
5
10
  /**
6
11
  * Model class.
7
- * @type {typeof import("../database/record/index.js").default | undefined} */
12
+ * @type {AuthorizationResourceModelClass | undefined} */
8
13
  static ModelClass = undefined
9
14
 
10
15
  /**
@@ -22,7 +27,9 @@ export default class AuthorizationBaseResource {
22
27
 
23
28
  /**
24
29
  * Runs model class.
25
- * @returns {typeof import("../database/record/index.js").default} - Model class handled by this resource.
30
+ * @template {AuthorizationResourceModelClass} TModelClass
31
+ * @this {{ModelClass: TModelClass | undefined, name: string}}
32
+ * @returns {TModelClass} - Model class handled by this resource.
26
33
  */
27
34
  static modelClass() {
28
35
  if (!this.ModelClass) {
@@ -41,7 +48,10 @@ export default class AuthorizationBaseResource {
41
48
  */
42
49
  can(actions, conditions) {
43
50
  this.assertResourceConditionsSignature({conditions, methodName: "can"})
44
- this.requiredAbility().can(actions, this.requiredModelClass(), /** @type {import("./ability.js").AbilityConditionsType<typeof import("../database/record/index.js").default> | undefined} */ (conditions))
51
+ // Authorization query rules are backend-only even when a shared resource is bound to a frontend model.
52
+ const modelClass = /** @type {typeof import("../database/record/index.js").default} */ (this.requiredModelClass())
53
+
54
+ this.requiredAbility().can(actions, modelClass, /** @type {import("./ability.js").AbilityConditionsType<typeof import("../database/record/index.js").default> | undefined} */ (conditions))
45
55
  }
46
56
 
47
57
  /**
@@ -53,7 +63,10 @@ export default class AuthorizationBaseResource {
53
63
  */
54
64
  cannot(actions, conditions) {
55
65
  this.assertResourceConditionsSignature({conditions, methodName: "cannot"})
56
- this.requiredAbility().cannot(actions, this.requiredModelClass(), /** @type {import("./ability.js").AbilityConditionsType<typeof import("../database/record/index.js").default> | undefined} */ (conditions))
66
+ // Authorization query rules are backend-only even when a shared resource is bound to a frontend model.
67
+ const modelClass = /** @type {typeof import("../database/record/index.js").default} */ (this.requiredModelClass())
68
+
69
+ this.requiredAbility().cannot(actions, modelClass, /** @type {import("./ability.js").AbilityConditionsType<typeof import("../database/record/index.js").default> | undefined} */ (conditions))
57
70
  }
58
71
 
59
72
  /**
@@ -70,7 +83,7 @@ export default class AuthorizationBaseResource {
70
83
 
71
84
  /**
72
85
  * Runs required model class.
73
- * @returns {typeof import("../database/record/index.js").default} - Model class handled by this resource.
86
+ * @returns {AuthorizationResourceModelClass} - Model class handled by this resource.
74
87
  */
75
88
  requiredModelClass() {
76
89
  const ResourceClass = /** @type {typeof AuthorizationBaseResource} */ (this.constructor)
@@ -375,6 +375,22 @@
375
375
  * @property {Record<string, ReturnType<typeof JSON.parse>>} [instance] - Optional custom attachment driver instance.
376
376
  */
377
377
 
378
+ /**
379
+ * Client-safe synchronization policy declared with a model attachment.
380
+ * @typedef {object} AttachmentSyncConfiguration
381
+ * @property {"eager" | "on-demand"} fetch - Whether clients prefetch the attachment or wait until it is requested.
382
+ * @property {"optional" | "required"} offlineRequirement - Whether an offline-ready scope requires the attachment bytes.
383
+ * @property {"durable" | "evictable"} retention - Whether clients may evict the attachment under storage pressure.
384
+ */
385
+
386
+ /**
387
+ * Model attachment declaration retained by the record class.
388
+ * @typedef {object} RecordAttachmentConfiguration
389
+ * @property {string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>} [driver] - Attachment driver name, class, or instance.
390
+ * @property {AttachmentSyncConfiguration} [sync] - Client-safe synchronized asset policy.
391
+ * @property {"hasOne" | "hasMany"} type - Attachment cardinality.
392
+ */
393
+
378
394
  /**
379
395
  * @typedef {object} AttachmentsConfiguration
380
396
  * @property {string} [defaultDriver] - Default attachment storage driver name.
@@ -418,6 +434,7 @@
418
434
 
419
435
  /**
420
436
  * @typedef {object} FrontendModelAttachmentConfiguration
437
+ * @property {AttachmentSyncConfiguration} [sync] - Client-side synchronized asset policy.
421
438
  * @property {"hasOne" | "hasMany"} type - Attachment cardinality.
422
439
  */
423
440
 
@@ -574,11 +591,26 @@
574
591
  */
575
592
 
576
593
  /**
577
- * @typedef {Omit<typeof import("./frontend-model-resource/base-resource.js").default, never> & {new (args: import("./frontend-model-resource/base-resource.js").FrontendModelResourceAbilityArgs | import("./frontend-model-resource/base-resource.js").FrontendModelResourceControllerArgs): import("./frontend-model-resource/base-resource.js").default<typeof import("./database/record/index.js").default>}} FrontendModelResourceClassType
594
+ * Unbound resource class used by model-agnostic registries.
595
+ * @typedef {Omit<typeof import("./frontend-model-resource/base-resource.js").default, "modelClass"> & {modelClass: () => typeof import("./database/record/index.js").default, new (args: never): import("./frontend-model-resource/base-resource.js").default<typeof import("./database/record/index.js").default>}} UnboundFrontendModelResourceClassType
596
+ */
597
+
598
+ /**
599
+ * Resource class bound to a specific model class.
600
+ * @template {import("./frontend-model-resource/base-resource.js").FrontendModelResourceModelClass} TModelClass
601
+ * @template {typeof import("./database/record/index.js").default} TDatabaseModelClass
602
+ * @typedef {Omit<typeof import("./frontend-model-resource/base-resource.js").default, "ModelClass" | "modelClass"> & {ModelClass: TModelClass | undefined, modelClass: () => TModelClass, new (args: import("./frontend-model-resource/base-resource.js").FrontendModelResourceAbilityArgs<TModelClass> | import("./frontend-model-resource/base-resource.js").FrontendModelResourceControllerArgs<TDatabaseModelClass>): import("./frontend-model-resource/base-resource.js").default<TModelClass, TDatabaseModelClass>}} BoundFrontendModelResourceClassType
603
+ */
604
+
605
+ /**
606
+ * @template {import("./frontend-model-resource/base-resource.js").FrontendModelResourceModelClass} [TModelClass=never]
607
+ * @template {typeof import("./database/record/index.js").default} [TDatabaseModelClass=Extract<TModelClass, typeof import("./database/record/index.js").default>]
608
+ * @typedef {[TModelClass] extends [never] ? UnboundFrontendModelResourceClassType : BoundFrontendModelResourceClassType<TModelClass, TDatabaseModelClass>} FrontendModelResourceClassType
578
609
  */
579
610
 
580
611
  /**
581
- * @typedef {FrontendModelResourceClassType} FrontendModelResourceDefinition
612
+ * @template {import("./frontend-model-resource/base-resource.js").FrontendModelResourceModelClass} [TModelClass=never]
613
+ * @typedef {FrontendModelResourceClassType<TModelClass>} FrontendModelResourceDefinition
582
614
  */
583
615
 
584
616
  /**
@@ -71,6 +71,8 @@ import UUID from "pure-uuid"
71
71
  * AttachmentDriverConstructor type.
72
72
  * @typedef {import("../../configuration-types.js").AttachmentDriverConstructor} AttachmentDriverConstructor
73
73
  */
74
+ /** @typedef {import("../../configuration-types.js").AttachmentSyncConfiguration} AttachmentSyncConfiguration */
75
+ /** @typedef {import("../../configuration-types.js").RecordAttachmentConfiguration} RecordAttachmentConfiguration */
74
76
 
75
77
  /** Stored values that a declared `"boolean"` cast reads back as `true`. */
76
78
  const declaredBooleanTruthyValues = new Set([1, true, "1"])
@@ -238,7 +240,7 @@ class VelociousDatabaseRecord {
238
240
  static _lifecycleCallbacks = undefined
239
241
  /** @type {Record<string, typeof import("./validators/base.js").default> | undefined} */
240
242
  static _validatorTypes = undefined
241
- /** @type {Record<string, {driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>, type: "hasOne" | "hasMany"}> | undefined} */
243
+ /** @type {Record<string, RecordAttachmentConfiguration> | undefined} */
242
244
  static _attachmentsMap = undefined
243
245
  /** @type {Record<string, import("./relationships/base.js").default> | undefined} */
244
246
  static _relationships = undefined
@@ -494,13 +496,13 @@ class VelociousDatabaseRecord {
494
496
 
495
497
  /**
496
498
  * Runs get attachments map.
497
- * @returns {Record<string, {driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>, type: "hasOne" | "hasMany"}>} - Attachment definitions keyed by name.
499
+ * @returns {Record<string, RecordAttachmentConfiguration>} - Attachment definitions keyed by name.
498
500
  */
499
501
  static getAttachmentsMap() {
500
502
  if (!this._attachmentsMap) {
501
503
  /**
502
504
  * Narrows the runtime value to the documented type.
503
- * @type {Record<string, {driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>, type: "hasOne" | "hasMany"}>} */
505
+ * @type {Record<string, RecordAttachmentConfiguration>} */
504
506
  this._attachmentsMap = {}
505
507
  }
506
508
 
@@ -1165,16 +1167,25 @@ class VelociousDatabaseRecord {
1165
1167
 
1166
1168
  /**
1167
1169
  * Runs get attachments.
1168
- * @returns {Record<string, {driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>, type: "hasOne" | "hasMany"}>} - Attachment definitions.
1170
+ * @returns {Record<string, RecordAttachmentConfiguration>} - Attachment definitions.
1169
1171
  */
1170
1172
  static getAttachments() {
1171
1173
  return this.getAttachmentsMap()
1172
1174
  }
1173
1175
 
1176
+ /**
1177
+ * Returns attachment definitions through the model contract shared with
1178
+ * frontend model classes.
1179
+ * @returns {Record<string, RecordAttachmentConfiguration>} - Attachment definitions.
1180
+ */
1181
+ static attachmentDefinitions() {
1182
+ return this.getAttachmentsMap()
1183
+ }
1184
+
1174
1185
  /**
1175
1186
  * Runs get attachment by name.
1176
1187
  * @param {string} attachmentName - Attachment name.
1177
- * @returns {{driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>, type: "hasOne" | "hasMany"}} - Attachment definition.
1188
+ * @returns {RecordAttachmentConfiguration} - Attachment definition.
1178
1189
  */
1179
1190
  static getAttachmentByName(attachmentName) {
1180
1191
  const definition = this.getAttachmentsMap()[attachmentName]
@@ -1443,14 +1454,34 @@ class VelociousDatabaseRecord {
1443
1454
  * @param {string} attachmentName - Attachment name.
1444
1455
  * @param {object} args - Attachment args.
1445
1456
  * @param {string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>} [args.driver] - Attachment driver name, class, or instance.
1457
+ * @param {AttachmentSyncConfiguration} [args.sync] - Client-safe synchronized asset policy.
1446
1458
  * @param {"hasOne" | "hasMany"} args.type - Attachment type.
1447
1459
  * @returns {void} - No return value.
1448
1460
  */
1449
- static _defineAttachment(attachmentName, {driver, type}) {
1461
+ static _defineAttachment(attachmentName, {driver, sync, type}) {
1450
1462
  if (!attachmentName || typeof attachmentName !== "string") throw new Error(`Invalid attachment name: ${attachmentName}`)
1451
1463
  if (attachmentName in this.getAttachmentsMap()) throw new Error(`Attachment ${attachmentName} already exists`)
1452
1464
 
1453
- this.getAttachmentsMap()[attachmentName] = {driver, type}
1465
+ if (sync) {
1466
+ const {fetch, offlineRequirement, retention, ...restSync} = sync
1467
+
1468
+ restArgsError(restSync)
1469
+
1470
+ if (fetch !== "eager" && fetch !== "on-demand") {
1471
+ throw new Error(`Attachment ${attachmentName} sync fetch must be eager or on-demand`)
1472
+ }
1473
+ if (offlineRequirement !== "optional" && offlineRequirement !== "required") {
1474
+ throw new Error(`Attachment ${attachmentName} offline requirement must be optional or required`)
1475
+ }
1476
+ if (retention !== "durable" && retention !== "evictable") {
1477
+ throw new Error(`Attachment ${attachmentName} sync retention must be durable or evictable`)
1478
+ }
1479
+ if (offlineRequirement === "required" && retention !== "durable") {
1480
+ throw new Error(`Attachment ${attachmentName} required offline assets must use durable retention`)
1481
+ }
1482
+ }
1483
+
1484
+ this.getAttachmentsMap()[attachmentName] = {driver, sync, type}
1454
1485
 
1455
1486
  const prototype = /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (/** @type {ReturnType<typeof JSON.parse>} */ (this.prototype))
1456
1487
 
@@ -1467,21 +1498,21 @@ class VelociousDatabaseRecord {
1467
1498
  /**
1468
1499
  * Adds a single attachment helper to the model.
1469
1500
  * @param {string} attachmentName - Attachment name.
1470
- * @param {{driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>}} [args] - Attachment options.
1501
+ * @param {{driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>, sync?: AttachmentSyncConfiguration}} [args] - Attachment options.
1471
1502
  * @returns {void} - No return value.
1472
1503
  */
1473
1504
  static hasOneAttachment(attachmentName, args = {}) {
1474
- this._defineAttachment(attachmentName, {driver: args.driver, type: "hasOne"})
1505
+ this._defineAttachment(attachmentName, {driver: args.driver, sync: args.sync, type: "hasOne"})
1475
1506
  }
1476
1507
 
1477
1508
  /**
1478
1509
  * Adds a collection attachment helper to the model.
1479
1510
  * @param {string} attachmentName - Attachment name.
1480
- * @param {{driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>}} [args] - Attachment options.
1511
+ * @param {{driver?: string | AttachmentDriverConstructor | Record<string, ReturnType<typeof JSON.parse>>, sync?: AttachmentSyncConfiguration}} [args] - Attachment options.
1481
1512
  * @returns {void} - No return value.
1482
1513
  */
1483
1514
  static hasManyAttachments(attachmentName, args = {}) {
1484
- this._defineAttachment(attachmentName, {driver: args.driver, type: "hasMany"})
1515
+ this._defineAttachment(attachmentName, {driver: args.driver, sync: args.sync, type: "hasMany"})
1485
1516
  }
1486
1517
 
1487
1518
  /**
@@ -5,6 +5,7 @@ import path from "node:path"
5
5
  import * as inflection from "inflection"
6
6
  import {frontendModelResourceIsBuiltIn, frontendModelResourcesWithBuiltInsForBackendProject} from "../../../../../frontend-models/built-in-resources.js"
7
7
  import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition} from "../../../../../frontend-models/resource-definition.js"
8
+ import {frontendModelResourceInternalConstructor} from "../../../../../frontend-model-resource/base-resource.js"
8
9
 
9
10
  /**
10
11
  * Attribute metadata used for generated frontend-model JSDoc.
@@ -381,7 +382,18 @@ export default class DbGenerateFrontendModels extends BaseCommand {
381
382
  ? "hasMany"
382
383
  : "hasOne"
383
384
 
384
- fileContent += ` ${attachmentName}: {type: ${JSON.stringify(attachmentType)}},\n`
385
+ if (attachmentConfig.sync) {
386
+ fileContent += ` ${attachmentName}: {\n`
387
+ fileContent += " sync: {\n"
388
+ fileContent += ` fetch: ${JSON.stringify(attachmentConfig.sync.fetch)},\n`
389
+ fileContent += ` offlineRequirement: ${JSON.stringify(attachmentConfig.sync.offlineRequirement)},\n`
390
+ fileContent += ` retention: ${JSON.stringify(attachmentConfig.sync.retention)},\n`
391
+ fileContent += " },\n"
392
+ fileContent += ` type: ${JSON.stringify(attachmentType)}\n`
393
+ fileContent += " },\n"
394
+ } else {
395
+ fileContent += ` ${attachmentName}: {type: ${JSON.stringify(attachmentType)}},\n`
396
+ }
385
397
  }
386
398
  fileContent += " },\n"
387
399
  }
@@ -857,7 +869,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
857
869
  try {
858
870
  const modelClass = resourceClass.modelClass()
859
871
 
860
- const instance = new resourceClass({
872
+ const ResourceClass = frontendModelResourceInternalConstructor(resourceClass)
873
+ const instance = new ResourceClass({
861
874
  ability: undefined,
862
875
  context: {},
863
876
  locals: {},
@@ -894,7 +907,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
894
907
  try {
895
908
  const modelClass = resourceClass.modelClass()
896
909
 
897
- const instance = new resourceClass({
910
+ const ResourceClass = frontendModelResourceInternalConstructor(resourceClass)
911
+ const instance = new ResourceClass({
898
912
  ability: undefined,
899
913
  context: {},
900
914
  locals: {},
@@ -3,7 +3,7 @@
3
3
  import {randomUUID} from "node:crypto"
4
4
  import * as inflection from "inflection"
5
5
  import Controller from "./controller.js"
6
- import FrontendModelBaseResource from "./frontend-model-resource/base-resource.js"
6
+ import FrontendModelBaseResource, {frontendModelResourceInternalConstructor} from "./frontend-model-resource/base-resource.js"
7
7
  import Response from "./http-server/client/response.js"
8
8
  import {frontendModelResourcesWithBuiltInsForBackendProject} from "./frontend-models/built-in-resources.js"
9
9
  import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject, frontendModelSyncManifestForBackendProjects} from "./frontend-models/resource-definition.js"
@@ -70,7 +70,7 @@ import {RansackQueryError, normalizeRansackGroup, parseRansackSort} from "./util
70
70
  * @typedef {object} FrontendModelIndexQueryOptions
71
71
  * @property {boolean} [includePagination] - Whether frontend-model pagination params should be applied.
72
72
  * @property {boolean} [includeSort] - Whether frontend-model sort params should be applied.
73
- * @property {import("./frontend-model-resource/base-resource.js").default} [resource] - Resource providing query hooks.
73
+ * @property {Pick<import("./frontend-model-resource/base-resource.js").default<import("./frontend-model-resource/base-resource.js").FrontendModelResourceModelClass>, "applyFrontendModelIndexPagination" | "applyFrontendModelIndexSearch" | "applyFrontendModelIndexSort">} [resource] - Resource providing query hooks.
74
74
  */
75
75
  /** @typedef {import("./database/query/model-class-query.js").default & Record<symbol, Set<string> | undefined>} FrontendModelQueryMetadata */
76
76
  /**
@@ -1040,7 +1040,9 @@ export default class FrontendModelController extends Controller {
1040
1040
  resourceConfiguration: frontendModelResource.resourceConfiguration
1041
1041
  }
1042
1042
 
1043
- return new frontendModelResource.resourceClass(resourceArgs)
1043
+ const ResourceClass = frontendModelResourceInternalConstructor(frontendModelResource.resourceClass)
1044
+
1045
+ return new ResourceClass(resourceArgs)
1044
1046
  }
1045
1047
 
1046
1048
  /**
@@ -2908,7 +2910,9 @@ export default class FrontendModelController extends Controller {
2908
2910
  const resourceClass = resourceDefinition ? frontendModelResourceClassFromDefinition(resourceDefinition) : null
2909
2911
 
2910
2912
  if (resourceClass) {
2911
- resource = new resourceClass({
2913
+ const ResourceClass = frontendModelResourceInternalConstructor(resourceClass)
2914
+
2915
+ resource = new ResourceClass({
2912
2916
  ability: this.currentAbility(),
2913
2917
  // Propagate the controller so a related/preloaded model's serialization
2914
2918
  // resource can use request context (e.g. `requestBaseUrl()` for signed
@@ -3985,7 +3989,8 @@ export default class FrontendModelController extends Controller {
3985
3989
 
3986
3990
  if (!frontendModelResource) throw frontendSyncReplaySafeError(`Sync replay model is not enabled: ${mutation.model}`)
3987
3991
 
3988
- const resource = new frontendModelResource.resourceClass({
3992
+ const ResourceClass = frontendModelResourceInternalConstructor(frontendModelResource.resourceClass)
3993
+ const resource = new ResourceClass({
3989
3994
  ability: this.currentAbility(),
3990
3995
  controller: this,
3991
3996
  context: {