velocious 1.0.494 → 1.0.496

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 (60) hide show
  1. package/build/database/record/index.js +2 -0
  2. package/build/database/record/validation-messages.js +50 -0
  3. package/build/database/record/validators/length.js +52 -0
  4. package/build/database/record/validators/presence.js +4 -1
  5. package/build/database/record/validators/uniqueness.js +4 -1
  6. package/build/frontend-model-resource/base-resource.js +175 -5
  7. package/build/frontend-models/resource-definition.js +49 -1
  8. package/build/src/database/record/index.d.ts.map +1 -1
  9. package/build/src/database/record/index.js +3 -1
  10. package/build/src/database/record/validation-messages.d.ts +38 -0
  11. package/build/src/database/record/validation-messages.d.ts.map +1 -0
  12. package/build/src/database/record/validation-messages.js +45 -0
  13. package/build/src/database/record/validators/length.d.ts +12 -0
  14. package/build/src/database/record/validators/length.d.ts.map +1 -0
  15. package/build/src/database/record/validators/length.js +45 -0
  16. package/build/src/database/record/validators/presence.d.ts.map +1 -1
  17. package/build/src/database/record/validators/presence.js +4 -2
  18. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  19. package/build/src/database/record/validators/uniqueness.js +4 -2
  20. package/build/src/frontend-model-resource/base-resource.d.ts +184 -5
  21. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  22. package/build/src/frontend-model-resource/base-resource.js +155 -6
  23. package/build/src/frontend-models/resource-definition.d.ts +41 -0
  24. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  25. package/build/src/frontend-models/resource-definition.js +45 -2
  26. package/build/src/sync/sync-envelope-replay-service.d.ts +166 -3
  27. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  28. package/build/src/sync/sync-envelope-replay-service.js +304 -12
  29. package/build/src/sync/sync-replay-upsert-applier.d.ts +1 -0
  30. package/build/src/sync/sync-replay-upsert-applier.d.ts.map +1 -1
  31. package/build/src/sync/sync-replay-upsert-applier.js +2 -1
  32. package/build/src/sync/sync-resource-base.d.ts +16 -32
  33. package/build/src/sync/sync-resource-base.d.ts.map +1 -1
  34. package/build/src/sync/sync-resource-base.js +31 -43
  35. package/build/src/testing/test-runner.d.ts +24 -0
  36. package/build/src/testing/test-runner.d.ts.map +1 -1
  37. package/build/src/testing/test-runner.js +99 -9
  38. package/build/sync/sync-envelope-replay-service.js +350 -10
  39. package/build/sync/sync-replay-upsert-applier.js +1 -0
  40. package/build/sync/sync-resource-base.js +32 -45
  41. package/build/testing/test-runner.js +104 -8
  42. package/build/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/scripts/run-tests.js +6 -0
  45. package/src/database/record/index.js +2 -0
  46. package/src/database/record/validation-messages.js +50 -0
  47. package/src/database/record/validators/length.js +52 -0
  48. package/src/database/record/validators/presence.js +4 -1
  49. package/src/database/record/validators/uniqueness.js +4 -1
  50. package/src/frontend-model-resource/base-resource.js +175 -5
  51. package/src/frontend-models/resource-definition.js +49 -1
  52. package/src/sync/sync-envelope-replay-service.js +350 -10
  53. package/src/sync/sync-replay-upsert-applier.js +1 -0
  54. package/src/sync/sync-resource-base.js +32 -45
  55. package/src/testing/test-runner.js +104 -8
  56. package/build/src/sync/sync-attribute-normalizer.d.ts +0 -105
  57. package/build/src/sync/sync-attribute-normalizer.d.ts.map +0 -1
  58. package/build/src/sync/sync-attribute-normalizer.js +0 -172
  59. package/build/sync/sync-attribute-normalizer.js +0 -186
  60. package/src/sync/sync-attribute-normalizer.js +0 -186
@@ -3,6 +3,7 @@
3
3
  import AuthorizationBaseResource from "../authorization/base-resource.js"
4
4
  import * as inflection from "inflection"
5
5
  import isPlainObject from "../utils/plain-object.js"
6
+ import VelociousError from "../velocious-error.js"
6
7
 
7
8
  /**
8
9
  * Built-in frontend-model resource action.
@@ -87,6 +88,34 @@ import isPlainObject from "../utils/plain-object.js"
87
88
  * @property {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration | import("../configuration-types.js").FrontendModelResourceConfiguration} [resourceConfiguration] - Optional normalized resource configuration.
88
89
  */
89
90
 
91
+ /**
92
+ * Normalized sync replay mutation passed to the resource sync hooks.
93
+ * @typedef {import("../sync/sync-envelope-replay-service.js").SyncReplayMutation} FrontendModelSyncMutation
94
+ */
95
+
96
+ /**
97
+ * Sync mutation authorization result.
98
+ * @typedef {object} FrontendModelSyncAuthorization
99
+ * @property {boolean} allowed - Whether the mutation may be applied.
100
+ * @property {string} [reason] - Stable failure reason code when denied.
101
+ */
102
+
103
+ /**
104
+ * Arguments for the applySync full-escape-hatch hook.
105
+ * @typedef {object} FrontendModelApplySyncArgs
106
+ * @property {Record<string, ?>} context - Replay context.
107
+ * @property {import("../database/record/index.js").default | null} existingSync - Existing sync row or null.
108
+ * @property {FrontendModelSyncMutation} mutation - Normalized replay mutation.
109
+ */
110
+
111
+ /**
112
+ * Apply result produced by routed sync mutation application.
113
+ * @typedef {object} FrontendModelSyncApplyResult
114
+ * @property {boolean} created - Whether a record was created.
115
+ * @property {boolean} [deleted] - Whether a record was deleted.
116
+ * @property {import("../database/record/index.js").default | null} record - Applied record or null.
117
+ */
118
+
90
119
  /**
91
120
  * Resolved frontend-model resource registration.
92
121
  * @typedef {object} FrontendModelResolvedResourceConfiguration
@@ -172,6 +201,16 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
172
201
  /** @type {?} */
173
202
  static SharedResource = undefined
174
203
 
204
+ /**
205
+ * Declarative writable-attribute permit list (camelCase attribute names)
206
+ * used as the default {@link FrontendModelBaseResource#permittedParams} and
207
+ * as the routed sync replay permit. Resolved through the shared resource
208
+ * like the other static resource config: an undeclared environment list
209
+ * falls back to the shared resource's list, while an explicit declaration
210
+ * (including `null`) wins.
211
+ * @type {string[] | null | undefined} */
212
+ static writableAttributes = undefined
213
+
175
214
  /**
176
215
  * Runs constructor.
177
216
  * @param {FrontendModelResourceAbilityArgs | FrontendModelResourceControllerArgs} args - Resource args.
@@ -206,7 +245,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
206
245
  /**
207
246
  * Reads a static resource config value from the environment resource first,
208
247
  * then from the shared resource.
209
- * @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "relationships" | "server" | "sync" | "translatedAttributes"} name - Static config property name.
248
+ * @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "relationships" | "server" | "sync" | "translatedAttributes" | "writableAttributes"} name - Static config property name.
210
249
  * @returns {?} - Resolved config value.
211
250
  */
212
251
  static sharedResourceStaticValue(name) {
@@ -481,9 +520,10 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
481
520
  * }
482
521
  * }
483
522
  *
484
- * Default implementation returns `[]` — nothing permitted. Subclasses
485
- * must override to enable writes. A resource that does not declare
486
- * `permittedParams` cannot accept any write.
523
+ * Default implementation returns the declared
524
+ * {@link FrontendModelBaseResource.writableAttributes} permit list, or `[]`
525
+ * nothing permitted without a declared list. Subclasses override to
526
+ * customize; an explicit override always wins.
487
527
  * @param {{action?: "create" | "update", params?: Record<string, ?>, ability?: import("../authorization/ability.js").default, locals?: Record<string, ?>}} [arg] - Request context.
488
528
  * @returns {Array<string | Record<string, ?>>} - Permit spec.
489
529
  */
@@ -491,10 +531,140 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
491
531
  return this.sharedResourceMethodOr("permittedParams", [arg], () => {
492
532
  void arg
493
533
 
494
- return []
534
+ return this.declaredWritableAttributes() ?? []
495
535
  })
496
536
  }
497
537
 
538
+ /**
539
+ * Resolves the declared writable-attribute permit list from the environment
540
+ * resource first, then the shared resource — mirroring how the other
541
+ * static resource config resolves. An explicit environment declaration
542
+ * (including `null`) wins over the shared resource's list.
543
+ * @returns {string[] | null} Declared permit list or null when undeclared.
544
+ */
545
+ declaredWritableAttributes() {
546
+ const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
547
+ const permittedAttributes = /** @type {string[] | null | undefined} */ (ResourceClass.sharedResourceStaticValue("writableAttributes"))
548
+
549
+ return permittedAttributes ?? null
550
+ }
551
+
552
+ /**
553
+ * Builds the client-safe error thrown for a failed writable-attribute validation.
554
+ * @param {string} message - Human-readable validation message.
555
+ * @param {{cause?: Error, code: string}} details - Stable machine-readable code and optional cause.
556
+ * @returns {Error} Client-safe error.
557
+ */
558
+ writableAttributeError(message, {cause, code}) {
559
+ return VelociousError.safe(message, cause ? {cause, code} : {code})
560
+ }
561
+
562
+ /**
563
+ * Authorizes one routed sync replay mutation before it is applied.
564
+ * Defaults to allowing every mutation; record-level authorization still
565
+ * applies through {@link FrontendModelBaseResource#findSyncRecord} scoping
566
+ * and the create membership check.
567
+ * @param {object} args - Options.
568
+ * @param {Record<string, ?>} args.context - Replay context.
569
+ * @param {FrontendModelSyncMutation} args.mutation - Normalized replay mutation.
570
+ * @returns {FrontendModelSyncAuthorization | Promise<FrontendModelSyncAuthorization>} Authorization result.
571
+ */
572
+ authorizeSyncMutation({context, mutation}) {
573
+ void context
574
+ void mutation
575
+
576
+ return {allowed: true}
577
+ }
578
+
579
+ /**
580
+ * Returns the per-sync failure reason reported when a routed sync mutation
581
+ * fails record-level authorization. Defaults to null, which reports the
582
+ * generic "access-denied" reason.
583
+ * @param {object} args - Options.
584
+ * @param {"create" | "destroy" | "update"} args.action - Denied action.
585
+ * @param {FrontendModelSyncMutation} args.mutation - Normalized replay mutation.
586
+ * @returns {string | null} Stable failure reason code or null for the generic default.
587
+ */
588
+ syncAuthorizationFailureReason({action, mutation}) {
589
+ void action
590
+ void mutation
591
+
592
+ return null
593
+ }
594
+
595
+ /**
596
+ * Finds the existing record targeted by a routed sync replay mutation.
597
+ * Defaults to an `accessibleFor` lookup by primary key through the
598
+ * resource's normalized ability action for update (or destroy for delete
599
+ * mutations), falling back to an unscoped lookup without an ability.
600
+ * @param {object} args - Options.
601
+ * @param {import("../authorization/ability.js").default} [args.ability] - Ability override. Defaults to the resource ability.
602
+ * @param {boolean} [args.forDelete] - Whether the lookup is for a delete mutation.
603
+ * @param {FrontendModelSyncMutation} args.mutation - Normalized replay mutation.
604
+ * @returns {Promise<import("../database/record/index.js").default | null>} Existing record or null.
605
+ */
606
+ async findSyncRecord({ability = this.ability, forDelete = false, mutation}) {
607
+ const ModelClass = this.modelClass()
608
+ const primaryKey = ModelClass.primaryKey()
609
+ const query = ability
610
+ ? ModelClass.accessibleFor(this.syncAbilityAction(forDelete ? "destroy" : "update"), ability)
611
+ : ModelClass.where({})
612
+
613
+ return await query.findBy({[primaryKey]: mutation.resourceId})
614
+ }
615
+
616
+ /**
617
+ * Maps a raw sync action to the resource's normalized ability action when
618
+ * the resource configuration declares an abilities mapping, otherwise the
619
+ * raw action name is used directly.
620
+ * @param {"create" | "destroy" | "update"} action - Raw sync action.
621
+ * @returns {string} Ability action.
622
+ */
623
+ syncAbilityAction(action) {
624
+ const abilities = this.resourceConfigurationValue?.abilities
625
+
626
+ if (abilities && typeof abilities == "object" && !Array.isArray(abilities)) {
627
+ const abilityAction = /** @type {Record<string, ?>} */ (abilities)[action]
628
+
629
+ if (typeof abilityAction == "string" && abilityAction.length > 0) return abilityAction
630
+ }
631
+
632
+ return action
633
+ }
634
+
635
+ /**
636
+ * Full escape hatch for routed sync mutation application. Returning a
637
+ * non-null result replaces the whole default apply flow (authorization,
638
+ * record lookup, normalization and save) with the returned apply result.
639
+ * @param {FrontendModelApplySyncArgs} args - Apply args.
640
+ * @returns {FrontendModelSyncApplyResult | null | Promise<FrontendModelSyncApplyResult | null>} Apply result or null for the default flow.
641
+ */
642
+ applySync(args) {
643
+ void args
644
+
645
+ return null
646
+ }
647
+
648
+ /**
649
+ * Runs after a routed sync mutation was applied. Returned entries are
650
+ * merged into the apply result, reaching persistExtraAttributes and
651
+ * broadcasts.
652
+ * @param {object} args - Options.
653
+ * @param {Record<string, ?>} args.context - Replay context.
654
+ * @param {boolean} args.created - Whether the record was created.
655
+ * @param {FrontendModelSyncMutation} args.mutation - Normalized replay mutation.
656
+ * @param {import("../database/record/index.js").default | null} args.record - Applied record or null.
657
+ * @returns {Record<string, ?> | Promise<Record<string, ?>>} Extra apply-result entries.
658
+ */
659
+ afterSyncApply({context, created, mutation, record}) {
660
+ void context
661
+ void created
662
+ void mutation
663
+ void record
664
+
665
+ return {}
666
+ }
667
+
498
668
  /**
499
669
  * Normalizes create attributes before permission filtering and saving.
500
670
  * @param {FrontendModelResourceAttributePayload} attributes - Incoming create attributes.
@@ -40,11 +40,14 @@ const RESOURCE_STATIC_CONFIG_KEYS = new Set([
40
40
  "modelName",
41
41
  "ModelClass",
42
42
  "primaryKey",
43
+ "quickSearchColumns",
43
44
  "relationships",
45
+ "ReplayServiceClass",
44
46
  "server",
45
47
  "SharedResource",
46
48
  "sync",
47
- "translatedAttributes"
49
+ "translatedAttributes",
50
+ "writableAttributes"
48
51
  ])
49
52
 
50
53
  /**
@@ -879,3 +882,48 @@ function normalizeFrontendModelResourcePathForMatch(path) {
879
882
 
880
883
  return withLeadingSlash
881
884
  }
885
+
886
+ /**
887
+ * Resolved frontend-model resource registration for a replay resource type.
888
+ * @typedef {object} FrontendModelResolvedResourceRegistration
889
+ * @property {string} modelName - Effective frontend model name (modelName override or registry key).
890
+ * @property {import("../configuration-types.js").FrontendModelResourceClassType} resourceClass - Registered resource class.
891
+ * @property {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration} resourceConfiguration - Normalized resource configuration.
892
+ */
893
+
894
+ /**
895
+ * Resolves the registered frontend-model resource class for a resource type
896
+ * across all backend projects. A resource's effective name is its
897
+ * `modelName` override when declared, otherwise its registry key — matching
898
+ * {@link frontendModelSyncManifestForBackendProjects}. A registry key shadowed
899
+ * by a `modelName` override does not resolve.
900
+ * @param {object} args - Options.
901
+ * @param {{getBackendProjects: () => import("../configuration-types.js").BackendProjectConfiguration[]}} args.configuration - Configuration exposing the backend projects.
902
+ * @param {string} args.resourceType - Frontend model name to resolve.
903
+ * @returns {FrontendModelResolvedResourceRegistration | null} Resolved registration or null when the resource type is not registered.
904
+ */
905
+ export function resolveFrontendModelResourceClass({configuration, resourceType}) {
906
+ for (const backendProject of configuration.getBackendProjects()) {
907
+ const resources = frontendModelResourcesForBackendProject(backendProject)
908
+
909
+ for (const configuredModelName of Object.keys(resources)) {
910
+ const resourceDefinition = resources[configuredModelName]
911
+ const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
912
+
913
+ if (!resourceClass) continue
914
+
915
+ // Cheap direct-key mismatch skip: only normalize configurations for the
916
+ // matching key or when a modelName override could rename the resource.
917
+ if (configuredModelName !== resourceType && !resourceClass.sharedResourceStaticValue("modelName")) continue
918
+
919
+ const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
920
+
921
+ if (!resourceConfiguration) continue
922
+ if ((resourceConfiguration.modelName || configuredModelName) !== resourceType) continue
923
+
924
+ return {modelName: resourceType, resourceClass, resourceConfiguration}
925
+ }
926
+ }
927
+
928
+ return null
929
+ }