@api-client/core 0.18.57 → 0.18.59

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 (132) hide show
  1. package/build/src/decorators/observed.d.ts.map +1 -1
  2. package/build/src/decorators/observed.js +15 -1
  3. package/build/src/decorators/observed.js.map +1 -1
  4. package/build/src/modeling/ApiModel.d.ts +7 -5
  5. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  6. package/build/src/modeling/ApiModel.js +35 -16
  7. package/build/src/modeling/ApiModel.js.map +1 -1
  8. package/build/src/modeling/ExposedEntity.d.ts +5 -2
  9. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  10. package/build/src/modeling/ExposedEntity.js +11 -8
  11. package/build/src/modeling/ExposedEntity.js.map +1 -1
  12. package/build/src/modeling/actions/Action.d.ts +41 -0
  13. package/build/src/modeling/actions/Action.d.ts.map +1 -0
  14. package/build/src/modeling/actions/Action.js +64 -0
  15. package/build/src/modeling/actions/Action.js.map +1 -0
  16. package/build/src/modeling/actions/CreateAction.d.ts +20 -0
  17. package/build/src/modeling/actions/CreateAction.d.ts.map +1 -0
  18. package/build/src/modeling/actions/CreateAction.js +43 -0
  19. package/build/src/modeling/actions/CreateAction.js.map +1 -0
  20. package/build/src/modeling/actions/DeleteAction.d.ts +36 -0
  21. package/build/src/modeling/actions/DeleteAction.d.ts.map +1 -0
  22. package/build/src/modeling/actions/DeleteAction.js +63 -0
  23. package/build/src/modeling/actions/DeleteAction.js.map +1 -0
  24. package/build/src/modeling/actions/ListAction.d.ts +39 -0
  25. package/build/src/modeling/actions/ListAction.d.ts.map +1 -0
  26. package/build/src/modeling/actions/ListAction.js +76 -0
  27. package/build/src/modeling/actions/ListAction.js.map +1 -0
  28. package/build/src/modeling/actions/ReadAction.d.ts +20 -0
  29. package/build/src/modeling/actions/ReadAction.d.ts.map +1 -0
  30. package/build/src/modeling/actions/ReadAction.js +43 -0
  31. package/build/src/modeling/actions/ReadAction.js.map +1 -0
  32. package/build/src/modeling/actions/SearchAction.d.ts +26 -0
  33. package/build/src/modeling/actions/SearchAction.d.ts.map +1 -0
  34. package/build/src/modeling/actions/SearchAction.js +53 -0
  35. package/build/src/modeling/actions/SearchAction.js.map +1 -0
  36. package/build/src/modeling/actions/UpdateAction.d.ts +29 -0
  37. package/build/src/modeling/actions/UpdateAction.d.ts.map +1 -0
  38. package/build/src/modeling/actions/UpdateAction.js +53 -0
  39. package/build/src/modeling/actions/UpdateAction.js.map +1 -0
  40. package/build/src/modeling/actions/index.d.ts +24 -0
  41. package/build/src/modeling/actions/index.d.ts.map +1 -0
  42. package/build/src/modeling/actions/index.js +9 -0
  43. package/build/src/modeling/actions/index.js.map +1 -0
  44. package/build/src/modeling/index.d.ts +12 -0
  45. package/build/src/modeling/index.d.ts.map +1 -0
  46. package/build/src/modeling/index.js +12 -0
  47. package/build/src/modeling/index.js.map +1 -0
  48. package/build/src/modeling/rules/AccessRule.d.ts +17 -0
  49. package/build/src/modeling/rules/AccessRule.d.ts.map +1 -0
  50. package/build/src/modeling/rules/AccessRule.js +19 -0
  51. package/build/src/modeling/rules/AccessRule.js.map +1 -0
  52. package/build/src/modeling/rules/AllowAuthenticated.d.ts +19 -0
  53. package/build/src/modeling/rules/AllowAuthenticated.d.ts.map +1 -0
  54. package/build/src/modeling/rules/AllowAuthenticated.js +14 -0
  55. package/build/src/modeling/rules/AllowAuthenticated.js.map +1 -0
  56. package/build/src/modeling/rules/AllowPublic.d.ts +19 -0
  57. package/build/src/modeling/rules/AllowPublic.d.ts.map +1 -0
  58. package/build/src/modeling/rules/AllowPublic.js +14 -0
  59. package/build/src/modeling/rules/AllowPublic.js.map +1 -0
  60. package/build/src/modeling/rules/MatchEmailDomain.d.ts +25 -0
  61. package/build/src/modeling/rules/MatchEmailDomain.d.ts.map +1 -0
  62. package/build/src/modeling/rules/MatchEmailDomain.js +40 -0
  63. package/build/src/modeling/rules/MatchEmailDomain.js.map +1 -0
  64. package/build/src/modeling/rules/MatchResourceOwner.d.ts +29 -0
  65. package/build/src/modeling/rules/MatchResourceOwner.d.ts.map +1 -0
  66. package/build/src/modeling/rules/MatchResourceOwner.js +40 -0
  67. package/build/src/modeling/rules/MatchResourceOwner.js.map +1 -0
  68. package/build/src/modeling/rules/MatchUserProperty.d.ts +28 -0
  69. package/build/src/modeling/rules/MatchUserProperty.d.ts.map +1 -0
  70. package/build/src/modeling/rules/MatchUserProperty.js +49 -0
  71. package/build/src/modeling/rules/MatchUserProperty.js.map +1 -0
  72. package/build/src/modeling/rules/MatchUserRole.d.ts +29 -0
  73. package/build/src/modeling/rules/MatchUserRole.d.ts.map +1 -0
  74. package/build/src/modeling/rules/MatchUserRole.js +40 -0
  75. package/build/src/modeling/rules/MatchUserRole.js.map +1 -0
  76. package/build/src/modeling/rules/RateLimitRule.d.ts +61 -0
  77. package/build/src/modeling/rules/RateLimitRule.d.ts.map +1 -0
  78. package/build/src/modeling/rules/RateLimitRule.js +101 -0
  79. package/build/src/modeling/rules/RateLimitRule.js.map +1 -0
  80. package/build/src/modeling/rules/RateLimitingConfiguration.d.ts +18 -0
  81. package/build/src/modeling/rules/RateLimitingConfiguration.d.ts.map +1 -0
  82. package/build/src/modeling/rules/RateLimitingConfiguration.js +35 -0
  83. package/build/src/modeling/rules/RateLimitingConfiguration.js.map +1 -0
  84. package/build/src/modeling/rules/index.d.ts +14 -0
  85. package/build/src/modeling/rules/index.d.ts.map +1 -0
  86. package/build/src/modeling/rules/index.js +11 -0
  87. package/build/src/modeling/rules/index.js.map +1 -0
  88. package/build/src/modeling/types.d.ts +6 -257
  89. package/build/src/modeling/types.d.ts.map +1 -1
  90. package/build/src/modeling/types.js.map +1 -1
  91. package/build/tsconfig.tsbuildinfo +1 -1
  92. package/data/models/example-generator-api.json +6 -6
  93. package/package.json +1 -1
  94. package/src/decorators/observed.ts +15 -1
  95. package/src/modeling/ApiModel.ts +21 -19
  96. package/src/modeling/ExposedEntity.ts +13 -18
  97. package/src/modeling/actions/Action.ts +64 -0
  98. package/src/modeling/actions/CreateAction.ts +38 -0
  99. package/src/modeling/actions/DeleteAction.ts +59 -0
  100. package/src/modeling/actions/ListAction.ts +66 -0
  101. package/src/modeling/actions/ReadAction.ts +40 -0
  102. package/src/modeling/actions/SearchAction.ts +46 -0
  103. package/src/modeling/actions/UpdateAction.ts +49 -0
  104. package/src/modeling/rules/AccessRule.ts +29 -0
  105. package/src/modeling/rules/AllowAuthenticated.ts +24 -0
  106. package/src/modeling/rules/AllowPublic.ts +24 -0
  107. package/src/modeling/rules/MatchEmailDomain.ts +39 -0
  108. package/src/modeling/rules/MatchResourceOwner.ts +43 -0
  109. package/src/modeling/rules/MatchUserProperty.ts +44 -0
  110. package/src/modeling/rules/MatchUserRole.ts +43 -0
  111. package/src/modeling/rules/RateLimitRule.ts +104 -0
  112. package/src/modeling/rules/RateLimitingConfiguration.ts +32 -0
  113. package/src/modeling/types.ts +6 -276
  114. package/tests/unit/decorators/observed.spec.ts +42 -0
  115. package/tests/unit/modeling/actions/Action.spec.ts +109 -0
  116. package/tests/unit/modeling/actions/CreateAction.spec.ts +65 -0
  117. package/tests/unit/modeling/actions/DeleteAction.spec.ts +78 -0
  118. package/tests/unit/modeling/actions/ListAction.spec.ts +106 -0
  119. package/tests/unit/modeling/actions/ReadAction.spec.ts +77 -0
  120. package/tests/unit/modeling/actions/SearchAction.spec.ts +73 -0
  121. package/tests/unit/modeling/actions/UpdateAction.spec.ts +73 -0
  122. package/tests/unit/modeling/api_model.spec.ts +48 -3
  123. package/tests/unit/modeling/exposed_entity.spec.ts +73 -0
  124. package/tests/unit/modeling/rules/AccessRule.spec.ts +42 -0
  125. package/tests/unit/modeling/rules/AllowAuthenticated.spec.ts +28 -0
  126. package/tests/unit/modeling/rules/AllowPublic.spec.ts +28 -0
  127. package/tests/unit/modeling/rules/MatchEmailDomain.spec.ts +52 -0
  128. package/tests/unit/modeling/rules/MatchResourceOwner.spec.ts +37 -0
  129. package/tests/unit/modeling/rules/MatchUserProperty.spec.ts +58 -0
  130. package/tests/unit/modeling/rules/MatchUserRole.spec.ts +52 -0
  131. package/tests/unit/modeling/rules/RateLimitRule.spec.ts +70 -0
  132. package/tests/unit/modeling/rules/RateLimitingConfiguration.spec.ts +61 -0
@@ -15,6 +15,9 @@ import type {
15
15
  ExposedEntityKind,
16
16
  } from '../models/kinds.js'
17
17
  import type { DataDomain } from './DataDomain.js'
18
+ import { AccessRuleSchema } from './rules/AccessRule.js'
19
+ import { RateLimitingConfigurationSchema } from './rules/RateLimitingConfiguration.js'
20
+ import { ActionSchema } from './actions/Action.js'
18
21
 
19
22
  export interface DataDomainRemoveOptions {
20
23
  /**
@@ -488,17 +491,17 @@ export interface ExposedEntitySchema {
488
491
  /**
489
492
  * The list of enabled API actions for this exposure (List/Read/Create/etc.)
490
493
  */
491
- actions: ApiAction[]
494
+ actions: ActionSchema[]
492
495
 
493
496
  /**
494
497
  * Optional array of access rules that define the access control policies for this exposure.
495
498
  */
496
- accessRule?: AccessRule[]
499
+ accessRule?: AccessRuleSchema[]
497
500
 
498
501
  /**
499
502
  * Optional configuration for rate limiting for this exposure.
500
503
  */
501
- rateLimiting?: RateLimitingConfiguration
504
+ rateLimiting?: RateLimitingConfigurationSchema
502
505
 
503
506
  /**
504
507
  * When true, generation for this exposure hit configured limits
@@ -540,40 +543,6 @@ export interface ExposeOptions {
540
543
  maxDepth?: number
541
544
  }
542
545
 
543
- /**
544
- * Represents a specific, configurable API operation applied to a Data Entity.
545
- * Corresponds to common RESTful interactions.
546
- */
547
- export type ApiAction = ListAction | ReadAction | CreateAction | UpdateAction | DeleteAction | SearchAction
548
-
549
- /**
550
- * A base interface for common properties across all actions.
551
- */
552
- interface Action {
553
- /**
554
- * The specific kind of action (e.g., 'List', 'Read', etc.)
555
- */
556
- kind: string
557
- /**
558
- * Access control rules defining who can perform this action. It is only applied if the
559
- * authorization strategy is set to 'RBAC'.
560
- * If no rules grant access, it's denied by default making it essentially private.
561
- *
562
- * Note, the API can defined top level access rules that apply to all actions. If this property is set,
563
- * it overrides the top level access rules for this specific action.
564
- *
565
- * It is an ordered list, meaning the first rule that matches the user will be applied.
566
- * If multiple rules match, the first one in the list takes precedence.
567
- * If no rules match, the action is denied.
568
- */
569
- accessRule?: AccessRule[]
570
- /**
571
- * Optional configuration for action-wide rate limiting and throttling.
572
- * Defines rules to protect the action from overuse.
573
- */
574
- rateLimiting?: RateLimitingConfiguration
575
- }
576
-
577
546
  /**
578
547
  * Represents a pagination strategy for API actions that return collections of resources.
579
548
  * This is a base interface that can be extended for different pagination strategies.
@@ -605,245 +574,6 @@ export interface OffsetPaginationStrategy extends PaginationStrategy {
605
574
  kind: 'offset'
606
575
  }
607
576
 
608
- /**
609
- * Enables retrieving a collection of resources.
610
- * Endpoint: GET /[entity-collection-name]
611
- */
612
- export interface ListAction extends Action {
613
- kind: 'list'
614
- /**
615
- * The pagination strategy used for this action.
616
- * This defines how the results are paginated when retrieving a collection of resources.
617
- * It can be either 'cursor' or 'offset'.
618
- */
619
- pagination: PaginationStrategy
620
- /**
621
- * Fields from the entity that can be used for filtering.
622
- * Must be marked as "indexable" in the Data Model.
623
- */
624
- filterableFields: string[]
625
- /**
626
- * Fields from the entity that can be used for sorting.
627
- */
628
- sortableFields: string[]
629
- }
630
-
631
- /**
632
- * Enables retrieving a single resource by its ID.
633
- * Endpoint: GET /[entity-collection-name]/{id}
634
- */
635
- export interface ReadAction extends Action {
636
- kind: 'read'
637
- // Association handling (Link IDs vs. Embed) is defined on the
638
- // data association itself in the Data Modeler.
639
- }
640
-
641
- /**
642
- * Enables adding a new resource to a collection.
643
- * Endpoint: POST /[entity-collection-name]
644
- */
645
- export interface CreateAction extends Action {
646
- kind: 'create'
647
- }
648
-
649
- /**
650
- * Enables modifying an existing resource.
651
- * Endpoints: PUT or PATCH /[entity-collection-name]/{id}
652
- */
653
- export interface UpdateAction extends Action {
654
- kind: 'update'
655
- /**
656
- * The allowed HTTP methods for updates. Default: PATCH only.
657
- *
658
- * These two methods represent the two common ways to update a resource:
659
- * - PUT: Replaces the entire resource with the provided data.
660
- * - PATCH: Applies a partial update to the resource, allowing for specific fields to be modified.
661
- */
662
- allowedMethods: ('PUT' | 'PATCH')[]
663
- }
664
-
665
- /**
666
- * Enables removing an existing resource.
667
- * Endpoint: DELETE /[entity-collection-name]/{id}
668
- */
669
- export interface DeleteAction extends Action {
670
- kind: 'delete'
671
- /**
672
- * The strategy for deletion. Default: Soft Delete.
673
- *
674
- * @default 'soft'
675
- */
676
- strategy?: 'soft' | 'hard'
677
- /**
678
- * The data retention period (in days) for soft-deleted resources.
679
- * This defines how long the data should be kept before it is permanently deleted.
680
- *
681
- * @default 30
682
- */
683
- retentionPeriod?: number
684
- }
685
-
686
- /**
687
- * Enables keyword-based search across specified fields.
688
- * Endpoint: GET /[entity-collection-name]/search
689
- */
690
- export interface SearchAction extends Action {
691
- kind: 'search'
692
- /**
693
- * The fields within the entity to be included in the search scope.
694
- * Must be "indexable" and typically text-based.
695
- */
696
- fields: string[]
697
- }
698
-
699
- /**
700
- * Defines the access control policy for a specific API action.
701
- * Based on the predefined rule types for session-based authentication.
702
- */
703
- export type AccessRule =
704
- | AllowPublicAccessRule
705
- | AllowAuthenticatedAccessRule
706
- | MatchResourceOwnerAccessRule
707
- | MatchUserRoleAccessRule
708
- | MatchUserPropertyAccessRule
709
- | MatchEmailDomainAccessRule
710
-
711
- export interface BaseAccessRule {
712
- /**
713
- * The unique identifier for the access rule.
714
- * This is used to reference the rule in the API configuration.
715
- */
716
- type: string
717
- }
718
-
719
- /**
720
- * The action is allowed for all users, including unauthenticated ones.
721
- * This is typically used for public APIs or resources that do not require authentication.
722
- * It is the most permissive rule and should be used with caution.
723
- */
724
- export interface AllowPublicAccessRule extends BaseAccessRule {
725
- type: 'public'
726
- }
727
- /**
728
- * The action is allowed for any authenticated user.
729
- * This rule does not impose any additional restrictions based on user properties or resource ownership.
730
- * It is used for resources that should be accessible to all logged-in users.
731
- */
732
- export interface AllowAuthenticatedAccessRule extends BaseAccessRule {
733
- type: 'authenticated'
734
- }
735
- /**
736
- * The action is allowed if the authenticated user's ID matches a specific property on the resource.
737
- * This is typically used to restrict access to resources owned by the user.
738
- * For example, a user can only access their own profile or documents.
739
- */
740
- export interface MatchResourceOwnerAccessRule extends BaseAccessRule {
741
- type: 'resourceOwner'
742
- /**
743
- * The property on the resource that should match the authenticated user's ID.
744
- * This is typically the ID of the user who owns the resource.
745
- *
746
- * The domain model should annotate this property with the "ResourceOwnerIdentifier" semantic
747
- * to indicate that it is used for ownership checks.
748
- */
749
- property: string
750
- }
751
-
752
- /**
753
- * The action is allowed if the authenticated user has a specific role.
754
- * This is used to enforce role-based access control (RBAC).
755
- * For example, only users with the "admin" role can perform certain actions.
756
- */
757
- export interface MatchUserRoleAccessRule extends BaseAccessRule {
758
- type: 'matchUserRole'
759
- /**
760
- * The role that the authenticated user must have to access the resource.
761
- * This is typically a property on the user entity that defines their role.
762
- *
763
- * The domain model should annotate this property with the "UserRole" semantic
764
- * to indicate that it is used for role-based access control.
765
- */
766
- role: string[]
767
- }
768
- /**
769
- * The action is allowed if a specific property on the authenticated user matches an expected value.
770
- * This is used to enforce other user-specific restrictions.
771
- */
772
- export interface MatchUserPropertyAccessRule extends BaseAccessRule {
773
- type: 'matchUserProperty'
774
- /**
775
- * The property on the authenticated user that should match the expected value.
776
- */
777
- property: string
778
- /**
779
- * The expected value for the user property.
780
- */
781
- value: string
782
- }
783
- /**
784
- * The action is allowed if the authenticated user's email domain matches a specific domain.
785
- * This is used to restrict access based on the user's email address.
786
- * For example, only users with an email address from "my-company.com" can access certain resources.
787
- */
788
- export interface MatchEmailDomainAccessRule extends BaseAccessRule {
789
- type: 'matchEmailDomain'
790
- /**
791
- * The email domains that the authenticated user's email must match.
792
- */
793
- domains: string[]
794
- }
795
-
796
- /**
797
- * Defines the rate limiting and throttling policies for the entire API.
798
- */
799
- export interface RateLimitingConfiguration {
800
- /**
801
- * An ordered list of rules. The first rule that matches an incoming
802
- * request will be applied.
803
- */
804
- rules: RateLimitRule[]
805
- }
806
-
807
- /**
808
- * Represents a single rate limiting rule that applies to a specific
809
- * type of client, using a token bucket algorithm.
810
- */
811
- export interface RateLimitRule {
812
- /**
813
- * A human-readable description of what this rule is for.
814
- * e.g., "Limit anonymous users to 60 requests per hour."
815
- */
816
- description?: string
817
-
818
- /**
819
- * Defines how to group requests for rate limiting. This determines
820
- * who the limit applies to.
821
- *
822
- * - 'ip': Keys on the client's IP address. Best for anonymous traffic.
823
- * - 'userId': Keys on the authenticated user's ID. Best for logged-in users.
824
- * - 'role': Applies a shared limit to all users of a specific role.
825
- */
826
- key: { type: 'ip' } | { type: 'userId' } | { type: 'role'; value: string }
827
-
828
- /**
829
- * The number of requests allowed over the defined interval.
830
- * This is the rate at which tokens are added to the bucket.
831
- */
832
- rate: number
833
-
834
- /**
835
- * The time interval for the rate.
836
- */
837
- interval: 'second' | 'minute' | 'hour' | 'day'
838
-
839
- /**
840
- * The maximum number of requests that can be made in a burst.
841
- * This represents the "bucket size." A larger burst allows for
842
- * more requests to be made in a short period before throttling begins.
843
- */
844
- burst: number
845
- }
846
-
847
577
  export type DomainImpactKinds =
848
578
  | typeof DomainNamespaceKind
849
579
  | typeof DomainEntityKind
@@ -446,6 +446,48 @@ test.group('toRaw function', () => {
446
446
  const raw = toRaw(instance, obj)
447
447
  assert.equal(raw, undefined)
448
448
  }).tags(['@decorators', '@toRaw', '@core', '@unit', '@fast'])
449
+
450
+ test('should handle nested deep proxies (regression test)', ({ assert }) => {
451
+ const mockDomain = {
452
+ notifyChange() {},
453
+ }
454
+
455
+ class Child {
456
+ name = 'child'
457
+ }
458
+
459
+ class Parent {
460
+ domain = mockDomain
461
+ @observed({ deep: true }) accessor child: Child
462
+
463
+ constructor() {
464
+ this.child = new Child()
465
+ }
466
+ }
467
+
468
+ class GrandParent {
469
+ domain = mockDomain
470
+ @observed({ deep: true }) accessor parent: Parent
471
+
472
+ constructor() {
473
+ this.parent = new Parent()
474
+ }
475
+ }
476
+
477
+ const gp = new GrandParent()
478
+ // gp.parent is a proxy
479
+ // gp.parent.child is a nested proxy (proxy of a proxy)
480
+ const childProxy = gp.parent.child
481
+
482
+ const raw = toRaw(gp.parent, childProxy)
483
+
484
+ assert.isDefined(raw)
485
+ assert.equal(raw?.name, 'child')
486
+
487
+ // Should be able to structure clone it (proving it's a clean object without proxy wrappers)
488
+ const cloned = structuredClone(raw)
489
+ assert.deepEqual(cloned, { name: 'child' })
490
+ }).tags(['@decorators', '@toRaw', '@core', '@unit', '@regression'])
449
491
  })
450
492
 
451
493
  test.group('edge cases and error conditions', () => {
@@ -0,0 +1,109 @@
1
+ import { test } from '@japa/runner'
2
+ import { Action, AccessRule, RateLimitingConfiguration } from '../../../../src/modeling/index.js'
3
+ import { type ActionSchema } from '../../../../src/modeling/actions/Action.js'
4
+
5
+ test.group('Action', () => {
6
+ test('initializes with default values', ({ assert }) => {
7
+ const action = new Action()
8
+ assert.equal(action.kind, '')
9
+ assert.deepEqual(action.accessRule, [])
10
+ assert.isUndefined(action.rateLimiting)
11
+ }).tags(['@modeling', '@action'])
12
+
13
+ test('initializes with provided values', ({ assert }) => {
14
+ const schema: ActionSchema = {
15
+ kind: 'read',
16
+ accessRule: [{ type: 'public' }],
17
+ rateLimiting: { rules: [] },
18
+ }
19
+ const action = new Action(schema)
20
+ assert.equal(action.kind, 'read')
21
+ assert.lengthOf(action.accessRule, 1)
22
+ assert.instanceOf(action.accessRule[0], AccessRule)
23
+ assert.equal(action.accessRule[0].type, 'public')
24
+ assert.instanceOf(action.rateLimiting, RateLimitingConfiguration)
25
+ }).tags(['@modeling', '@action'])
26
+
27
+ test('serializes to JSON', ({ assert }) => {
28
+ const action = new Action({
29
+ kind: 'write',
30
+ accessRule: [{ type: 'admin' }],
31
+ rateLimiting: { rules: [] },
32
+ })
33
+ const json = action.toJSON()
34
+
35
+ assert.equal(json.kind, 'write')
36
+ assert.deepEqual(json.accessRule, [{ type: 'admin' }])
37
+ assert.deepEqual(json.rateLimiting, { rules: [] })
38
+ }).tags(['@modeling', '@action'])
39
+
40
+ test('notifies change when kind changes', async ({ assert }) => {
41
+ const action = new Action({ kind: 'read' })
42
+ let notified = false
43
+ action.addEventListener('change', () => {
44
+ notified = true
45
+ })
46
+
47
+ action.kind = 'write'
48
+ // allow microtask to process the change
49
+ await Promise.resolve()
50
+
51
+ assert.isTrue(notified)
52
+ }).tags(['@modeling', '@action', '@observed'])
53
+
54
+ test('notifies change when accessRule array is replaced', async ({ assert }) => {
55
+ const action = new Action()
56
+ let notified = false
57
+ action.addEventListener('change', () => {
58
+ notified = true
59
+ })
60
+
61
+ action.accessRule = [new AccessRule({ type: 'public' })]
62
+ await Promise.resolve()
63
+ assert.isTrue(notified)
64
+ }).tags(['@modeling', '@action', '@observed'])
65
+
66
+ test('notifies change when rateLimiting is replaced', async ({ assert }) => {
67
+ const action = new Action()
68
+ let notified = false
69
+ action.addEventListener('change', () => {
70
+ notified = true
71
+ })
72
+
73
+ action.rateLimiting = new RateLimitingConfiguration()
74
+ await Promise.resolve()
75
+ assert.isTrue(notified)
76
+ }).tags(['@modeling', '@action', '@observed'])
77
+
78
+ test('constructor copies accessRule array (immutability)', ({ assert }) => {
79
+ const rules = [{ type: 'public' }]
80
+ const action = new Action({ kind: 'read', accessRule: rules })
81
+
82
+ // Modify original array
83
+ rules.push({ type: 'admin' })
84
+ rules[0].type = 'changed'
85
+
86
+ assert.lengthOf(action.accessRule, 1)
87
+ assert.equal(action.accessRule[0].type, 'public')
88
+ }).tags(['@modeling', '@action', '@immutability'])
89
+
90
+ test('toJSON returns safe copy', ({ assert }) => {
91
+ const action = new Action({
92
+ kind: 'read',
93
+ accessRule: [{ type: 'public' }],
94
+ rateLimiting: { rules: [] },
95
+ })
96
+ const json = action.toJSON()
97
+
98
+ // Modify JSON
99
+ json.kind = 'write'
100
+ if (json.accessRule) {
101
+ json.accessRule[0].type = 'changed'
102
+ json.accessRule.push({ type: 'new' })
103
+ }
104
+
105
+ assert.equal(action.kind, 'read')
106
+ assert.lengthOf(action.accessRule, 1)
107
+ assert.equal(action.accessRule[0].type, 'public')
108
+ }).tags(['@modeling', '@action', '@immutability'])
109
+ })
@@ -0,0 +1,65 @@
1
+ import { test } from '@japa/runner'
2
+ import { CreateAction } from '../../../../src/modeling/actions/CreateAction.js'
3
+ import { AccessRule } from '../../../../src/modeling/rules/index.js'
4
+
5
+ test.group('CreateAction', () => {
6
+ test('initializes with default values', ({ assert }) => {
7
+ const action = new CreateAction()
8
+ assert.equal(action.kind, 'create')
9
+ assert.isEmpty(action.accessRule) // Inherited from Action
10
+ }).tags(['@modeling', '@action', '@create-action'])
11
+
12
+ test('initializes with inherited values', ({ assert }) => {
13
+ const action = new CreateAction({
14
+ accessRule: [{ type: 'public' }],
15
+ })
16
+
17
+ assert.equal(action.kind, 'create')
18
+ assert.lengthOf(action.accessRule, 1)
19
+ assert.equal(action.accessRule[0].type, 'public')
20
+ }).tags(['@modeling', '@action', '@create-action'])
21
+
22
+ test('constructor copies arrays (immutability)', ({ assert }) => {
23
+ const rules = [{ type: 'public' }]
24
+
25
+ const action = new CreateAction({
26
+ accessRule: rules,
27
+ })
28
+
29
+ // Modify original source
30
+ rules.push({ type: 'admin' })
31
+ rules[0].type = 'changed'
32
+
33
+ assert.lengthOf(action.accessRule, 1)
34
+ assert.equal(action.accessRule[0].type, 'public')
35
+ }).tags(['@modeling', '@action', '@create-action', '@immutability'])
36
+
37
+ test('toJSON returns valid schema', ({ assert }) => {
38
+ const action = new CreateAction({
39
+ accessRule: [{ type: 'public' }],
40
+ })
41
+
42
+ const json = action.toJSON()
43
+
44
+ assert.equal(json.kind, 'create')
45
+ if (json.accessRule) {
46
+ assert.lengthOf(json.accessRule, 1)
47
+ assert.equal(json.accessRule[0].type, 'public')
48
+ } else {
49
+ assert.fail('accessRule should be present in JSON')
50
+ }
51
+ }).tags(['@modeling', '@action', '@create-action', '@serialization'])
52
+
53
+ test('notifies change when inherited property changes', async ({ assert }) => {
54
+ const action = new CreateAction()
55
+ let notified = false
56
+ action.addEventListener('change', () => {
57
+ notified = true
58
+ })
59
+
60
+ // Modify inherited property
61
+ action.accessRule = [new AccessRule({ type: 'admin' })]
62
+ await Promise.resolve()
63
+ assert.isTrue(notified)
64
+ }).tags(['@modeling', '@action', '@create-action', '@observed'])
65
+ })
@@ -0,0 +1,78 @@
1
+ import { test } from '@japa/runner'
2
+ import { DeleteAction } from '../../../../src/modeling/actions/DeleteAction.js'
3
+
4
+ test.group('DeleteAction', () => {
5
+ test('initializes with default values', ({ assert }) => {
6
+ const action = new DeleteAction()
7
+ assert.equal(action.kind, 'delete')
8
+ assert.equal(action.strategy, 'soft')
9
+ assert.equal(action.retentionPeriod, 30)
10
+ assert.isEmpty(action.accessRule) // Inherited from Action
11
+ }).tags(['@modeling', '@action', '@delete-action'])
12
+
13
+ test('initializes with provided values', ({ assert }) => {
14
+ const action = new DeleteAction({
15
+ strategy: 'hard',
16
+ retentionPeriod: 0,
17
+ accessRule: [{ type: 'admin' }],
18
+ })
19
+
20
+ assert.equal(action.kind, 'delete')
21
+ assert.equal(action.strategy, 'hard')
22
+ assert.equal(action.retentionPeriod, 0)
23
+ assert.lengthOf(action.accessRule, 1)
24
+ assert.equal(action.accessRule[0].type, 'admin')
25
+ }).tags(['@modeling', '@action', '@delete-action'])
26
+
27
+ test('constructor copies arrays (immutability)', ({ assert }) => {
28
+ const rules = [{ type: 'public' }]
29
+
30
+ const action = new DeleteAction({
31
+ accessRule: rules,
32
+ })
33
+
34
+ // Modify original source
35
+ rules.push({ type: 'admin' })
36
+ rules[0].type = 'changed'
37
+
38
+ assert.lengthOf(action.accessRule, 1)
39
+ assert.equal(action.accessRule[0].type, 'public')
40
+ }).tags(['@modeling', '@action', '@delete-action', '@immutability'])
41
+
42
+ test('toJSON returns valid schema', ({ assert }) => {
43
+ const action = new DeleteAction({
44
+ strategy: 'hard',
45
+ retentionPeriod: 0,
46
+ })
47
+
48
+ const json = action.toJSON()
49
+
50
+ assert.equal(json.kind, 'delete')
51
+ assert.equal(json.strategy, 'hard')
52
+ assert.equal(json.retentionPeriod, 0)
53
+ }).tags(['@modeling', '@action', '@delete-action', '@serialization'])
54
+
55
+ test('notifies change when strategy changes', async ({ assert }) => {
56
+ const action = new DeleteAction()
57
+ let notified = false
58
+ action.addEventListener('change', () => {
59
+ notified = true
60
+ })
61
+
62
+ action.strategy = 'hard'
63
+ await Promise.resolve()
64
+ assert.isTrue(notified)
65
+ }).tags(['@modeling', '@action', '@delete-action', '@observed'])
66
+
67
+ test('notifies change when retentionPeriod changes', async ({ assert }) => {
68
+ const action = new DeleteAction()
69
+ let notified = false
70
+ action.addEventListener('change', () => {
71
+ notified = true
72
+ })
73
+
74
+ action.retentionPeriod = 60
75
+ await Promise.resolve()
76
+ assert.isTrue(notified)
77
+ }).tags(['@modeling', '@action', '@delete-action', '@observed'])
78
+ })