@api-client/core 0.18.57 → 0.18.58

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 (127) hide show
  1. package/build/src/modeling/ApiModel.d.ts +7 -5
  2. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  3. package/build/src/modeling/ApiModel.js +35 -16
  4. package/build/src/modeling/ApiModel.js.map +1 -1
  5. package/build/src/modeling/ExposedEntity.d.ts +5 -2
  6. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  7. package/build/src/modeling/ExposedEntity.js +11 -8
  8. package/build/src/modeling/ExposedEntity.js.map +1 -1
  9. package/build/src/modeling/actions/Action.d.ts +41 -0
  10. package/build/src/modeling/actions/Action.d.ts.map +1 -0
  11. package/build/src/modeling/actions/Action.js +64 -0
  12. package/build/src/modeling/actions/Action.js.map +1 -0
  13. package/build/src/modeling/actions/CreateAction.d.ts +18 -0
  14. package/build/src/modeling/actions/CreateAction.d.ts.map +1 -0
  15. package/build/src/modeling/actions/CreateAction.js +37 -0
  16. package/build/src/modeling/actions/CreateAction.js.map +1 -0
  17. package/build/src/modeling/actions/DeleteAction.d.ts +34 -0
  18. package/build/src/modeling/actions/DeleteAction.d.ts.map +1 -0
  19. package/build/src/modeling/actions/DeleteAction.js +57 -0
  20. package/build/src/modeling/actions/DeleteAction.js.map +1 -0
  21. package/build/src/modeling/actions/ListAction.d.ts +37 -0
  22. package/build/src/modeling/actions/ListAction.d.ts.map +1 -0
  23. package/build/src/modeling/actions/ListAction.js +70 -0
  24. package/build/src/modeling/actions/ListAction.js.map +1 -0
  25. package/build/src/modeling/actions/ReadAction.d.ts +18 -0
  26. package/build/src/modeling/actions/ReadAction.d.ts.map +1 -0
  27. package/build/src/modeling/actions/ReadAction.js +37 -0
  28. package/build/src/modeling/actions/ReadAction.js.map +1 -0
  29. package/build/src/modeling/actions/SearchAction.d.ts +24 -0
  30. package/build/src/modeling/actions/SearchAction.d.ts.map +1 -0
  31. package/build/src/modeling/actions/SearchAction.js +47 -0
  32. package/build/src/modeling/actions/SearchAction.js.map +1 -0
  33. package/build/src/modeling/actions/UpdateAction.d.ts +27 -0
  34. package/build/src/modeling/actions/UpdateAction.d.ts.map +1 -0
  35. package/build/src/modeling/actions/UpdateAction.js +47 -0
  36. package/build/src/modeling/actions/UpdateAction.js.map +1 -0
  37. package/build/src/modeling/actions/index.d.ts +24 -0
  38. package/build/src/modeling/actions/index.d.ts.map +1 -0
  39. package/build/src/modeling/actions/index.js +9 -0
  40. package/build/src/modeling/actions/index.js.map +1 -0
  41. package/build/src/modeling/index.d.ts +12 -0
  42. package/build/src/modeling/index.d.ts.map +1 -0
  43. package/build/src/modeling/index.js +12 -0
  44. package/build/src/modeling/index.js.map +1 -0
  45. package/build/src/modeling/rules/AccessRule.d.ts +17 -0
  46. package/build/src/modeling/rules/AccessRule.d.ts.map +1 -0
  47. package/build/src/modeling/rules/AccessRule.js +19 -0
  48. package/build/src/modeling/rules/AccessRule.js.map +1 -0
  49. package/build/src/modeling/rules/AllowAuthenticated.d.ts +19 -0
  50. package/build/src/modeling/rules/AllowAuthenticated.d.ts.map +1 -0
  51. package/build/src/modeling/rules/AllowAuthenticated.js +14 -0
  52. package/build/src/modeling/rules/AllowAuthenticated.js.map +1 -0
  53. package/build/src/modeling/rules/AllowPublic.d.ts +19 -0
  54. package/build/src/modeling/rules/AllowPublic.d.ts.map +1 -0
  55. package/build/src/modeling/rules/AllowPublic.js +14 -0
  56. package/build/src/modeling/rules/AllowPublic.js.map +1 -0
  57. package/build/src/modeling/rules/MatchEmailDomain.d.ts +25 -0
  58. package/build/src/modeling/rules/MatchEmailDomain.d.ts.map +1 -0
  59. package/build/src/modeling/rules/MatchEmailDomain.js +40 -0
  60. package/build/src/modeling/rules/MatchEmailDomain.js.map +1 -0
  61. package/build/src/modeling/rules/MatchResourceOwner.d.ts +29 -0
  62. package/build/src/modeling/rules/MatchResourceOwner.d.ts.map +1 -0
  63. package/build/src/modeling/rules/MatchResourceOwner.js +40 -0
  64. package/build/src/modeling/rules/MatchResourceOwner.js.map +1 -0
  65. package/build/src/modeling/rules/MatchUserProperty.d.ts +28 -0
  66. package/build/src/modeling/rules/MatchUserProperty.d.ts.map +1 -0
  67. package/build/src/modeling/rules/MatchUserProperty.js +49 -0
  68. package/build/src/modeling/rules/MatchUserProperty.js.map +1 -0
  69. package/build/src/modeling/rules/MatchUserRole.d.ts +29 -0
  70. package/build/src/modeling/rules/MatchUserRole.d.ts.map +1 -0
  71. package/build/src/modeling/rules/MatchUserRole.js +40 -0
  72. package/build/src/modeling/rules/MatchUserRole.js.map +1 -0
  73. package/build/src/modeling/rules/RateLimitRule.d.ts +61 -0
  74. package/build/src/modeling/rules/RateLimitRule.d.ts.map +1 -0
  75. package/build/src/modeling/rules/RateLimitRule.js +101 -0
  76. package/build/src/modeling/rules/RateLimitRule.js.map +1 -0
  77. package/build/src/modeling/rules/RateLimitingConfiguration.d.ts +18 -0
  78. package/build/src/modeling/rules/RateLimitingConfiguration.d.ts.map +1 -0
  79. package/build/src/modeling/rules/RateLimitingConfiguration.js +35 -0
  80. package/build/src/modeling/rules/RateLimitingConfiguration.js.map +1 -0
  81. package/build/src/modeling/rules/index.d.ts +13 -0
  82. package/build/src/modeling/rules/index.d.ts.map +1 -0
  83. package/build/src/modeling/rules/index.js +10 -0
  84. package/build/src/modeling/rules/index.js.map +1 -0
  85. package/build/src/modeling/types.d.ts +6 -257
  86. package/build/src/modeling/types.d.ts.map +1 -1
  87. package/build/src/modeling/types.js.map +1 -1
  88. package/build/tsconfig.tsbuildinfo +1 -1
  89. package/data/models/example-generator-api.json +26 -26
  90. package/package.json +1 -1
  91. package/src/modeling/ApiModel.ts +21 -19
  92. package/src/modeling/ExposedEntity.ts +13 -18
  93. package/src/modeling/actions/Action.ts +64 -0
  94. package/src/modeling/actions/CreateAction.ts +30 -0
  95. package/src/modeling/actions/DeleteAction.ts +51 -0
  96. package/src/modeling/actions/ListAction.ts +58 -0
  97. package/src/modeling/actions/ReadAction.ts +32 -0
  98. package/src/modeling/actions/SearchAction.ts +38 -0
  99. package/src/modeling/actions/UpdateAction.ts +41 -0
  100. package/src/modeling/rules/AccessRule.ts +29 -0
  101. package/src/modeling/rules/AllowAuthenticated.ts +24 -0
  102. package/src/modeling/rules/AllowPublic.ts +24 -0
  103. package/src/modeling/rules/MatchEmailDomain.ts +39 -0
  104. package/src/modeling/rules/MatchResourceOwner.ts +43 -0
  105. package/src/modeling/rules/MatchUserProperty.ts +44 -0
  106. package/src/modeling/rules/MatchUserRole.ts +43 -0
  107. package/src/modeling/rules/RateLimitRule.ts +104 -0
  108. package/src/modeling/rules/RateLimitingConfiguration.ts +32 -0
  109. package/src/modeling/types.ts +6 -276
  110. package/tests/unit/modeling/actions/Action.spec.ts +109 -0
  111. package/tests/unit/modeling/actions/CreateAction.spec.ts +65 -0
  112. package/tests/unit/modeling/actions/DeleteAction.spec.ts +78 -0
  113. package/tests/unit/modeling/actions/ListAction.spec.ts +106 -0
  114. package/tests/unit/modeling/actions/ReadAction.spec.ts +77 -0
  115. package/tests/unit/modeling/actions/SearchAction.spec.ts +73 -0
  116. package/tests/unit/modeling/actions/UpdateAction.spec.ts +73 -0
  117. package/tests/unit/modeling/api_model.spec.ts +48 -3
  118. package/tests/unit/modeling/exposed_entity.spec.ts +73 -0
  119. package/tests/unit/modeling/rules/AccessRule.spec.ts +42 -0
  120. package/tests/unit/modeling/rules/AllowAuthenticated.spec.ts +28 -0
  121. package/tests/unit/modeling/rules/AllowPublic.spec.ts +28 -0
  122. package/tests/unit/modeling/rules/MatchEmailDomain.spec.ts +52 -0
  123. package/tests/unit/modeling/rules/MatchResourceOwner.spec.ts +37 -0
  124. package/tests/unit/modeling/rules/MatchUserProperty.spec.ts +58 -0
  125. package/tests/unit/modeling/rules/MatchUserRole.spec.ts +52 -0
  126. package/tests/unit/modeling/rules/RateLimitRule.spec.ts +70 -0
  127. package/tests/unit/modeling/rules/RateLimitingConfiguration.spec.ts +61 -0
@@ -0,0 +1,77 @@
1
+ import { test } from '@japa/runner'
2
+ import { ReadAction } from '../../../../src/modeling/actions/ReadAction.js'
3
+ import { AccessRule } from '../../../../src/modeling/rules/index.js'
4
+
5
+ test.group('ReadAction', () => {
6
+ test('initializes with default values', ({ assert }) => {
7
+ const action = new ReadAction()
8
+ assert.equal(action.kind, 'read')
9
+ assert.isEmpty(action.accessRule) // Inherited from Action
10
+ }).tags(['@modeling', '@action', '@read-action'])
11
+
12
+ test('initializes with inherited values', ({ assert }) => {
13
+ const action = new ReadAction({
14
+ accessRule: [{ type: 'public' }],
15
+ })
16
+
17
+ assert.equal(action.kind, 'read')
18
+ assert.lengthOf(action.accessRule, 1)
19
+ assert.equal(action.accessRule[0].type, 'public')
20
+ }).tags(['@modeling', '@action', '@read-action'])
21
+
22
+ test('constructor copies arrays (immutability)', ({ assert }) => {
23
+ const rules = [{ type: 'public' }]
24
+
25
+ const action = new ReadAction({
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', '@read-action', '@immutability'])
36
+
37
+ test('toJSON returns valid schema', ({ assert }) => {
38
+ const action = new ReadAction({
39
+ accessRule: [{ type: 'public' }],
40
+ })
41
+
42
+ const json = action.toJSON()
43
+
44
+ assert.equal(json.kind, 'read')
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', '@read-action', '@serialization'])
52
+
53
+ test('notifies change when inherited property changes', async ({ assert }) => {
54
+ const action = new ReadAction()
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', '@read-action', '@observed'])
65
+
66
+ test('notifies change when accessRule value change', async ({ assert }) => {
67
+ const action = new ReadAction()
68
+ let notified = false
69
+ action.addEventListener('change', () => {
70
+ notified = true
71
+ })
72
+
73
+ action.accessRule.push(new AccessRule({ type: 'admin' }))
74
+ await Promise.resolve()
75
+ assert.isTrue(notified)
76
+ }).tags(['@modeling', '@action', '@read-action', '@observed'])
77
+ })
@@ -0,0 +1,73 @@
1
+ import { test } from '@japa/runner'
2
+ import { SearchAction } from '../../../../src/modeling/actions/SearchAction.js'
3
+
4
+ test.group('SearchAction', () => {
5
+ test('initializes with default values', ({ assert }) => {
6
+ const action = new SearchAction()
7
+ assert.equal(action.kind, 'search')
8
+ assert.deepEqual(action.fields, [])
9
+ assert.isEmpty(action.accessRule)
10
+ }).tags(['@modeling', '@action', '@search-action'])
11
+
12
+ test('initializes with provided values', ({ assert }) => {
13
+ const fields = ['name', 'description']
14
+ const action = new SearchAction({
15
+ fields,
16
+ })
17
+
18
+ assert.equal(action.kind, 'search')
19
+ assert.deepEqual(action.fields, fields)
20
+ }).tags(['@modeling', '@action', '@search-action'])
21
+
22
+ test('constructor copies arrays (immutability)', ({ assert }) => {
23
+ const fields = ['name']
24
+
25
+ const action = new SearchAction({
26
+ fields,
27
+ })
28
+
29
+ // Modify original source
30
+ fields.push('age')
31
+
32
+ assert.deepEqual(action.fields, ['name'])
33
+ }).tags(['@modeling', '@action', '@search-action', '@immutability'])
34
+
35
+ test('toJSON returns valid schema', ({ assert }) => {
36
+ const action = new SearchAction({
37
+ fields: ['name'],
38
+ })
39
+
40
+ const json = action.toJSON()
41
+
42
+ // Modify JSON
43
+ json.fields.push('new')
44
+
45
+ assert.equal(json.kind, 'search')
46
+ assert.lengthOf(action.fields, 1) // Ensure immutability
47
+ assert.equal(action.fields[0], 'name')
48
+ }).tags(['@modeling', '@action', '@search-action', '@serialization', '@immutability'])
49
+
50
+ test('notifies change when fields changes', async ({ assert }) => {
51
+ const action = new SearchAction()
52
+ let notified = false
53
+ action.addEventListener('change', () => {
54
+ notified = true
55
+ })
56
+
57
+ action.fields = ['name']
58
+ await Promise.resolve()
59
+ assert.isTrue(notified)
60
+ }).tags(['@modeling', '@action', '@search-action', '@observed'])
61
+
62
+ test('notifies change when fields value change', async ({ assert }) => {
63
+ const action = new SearchAction()
64
+ let notified = false
65
+ action.addEventListener('change', () => {
66
+ notified = true
67
+ })
68
+
69
+ action.fields.push('name')
70
+ await Promise.resolve()
71
+ assert.isTrue(notified)
72
+ }).tags(['@modeling', '@action', '@search-action', '@observed'])
73
+ })
@@ -0,0 +1,73 @@
1
+ import { test } from '@japa/runner'
2
+ import { UpdateAction } from '../../../../src/modeling/actions/UpdateAction.js'
3
+
4
+ test.group('UpdateAction', () => {
5
+ test('initializes with default values', ({ assert }) => {
6
+ const action = new UpdateAction()
7
+ assert.equal(action.kind, 'update')
8
+ assert.deepEqual(action.allowedMethods, ['PATCH'])
9
+ assert.isEmpty(action.accessRule)
10
+ }).tags(['@modeling', '@action', '@update-action'])
11
+
12
+ test('initializes with provided values', ({ assert }) => {
13
+ const methods: ('PUT' | 'PATCH')[] = ['PUT', 'PATCH']
14
+ const action = new UpdateAction({
15
+ allowedMethods: methods,
16
+ })
17
+
18
+ assert.equal(action.kind, 'update')
19
+ assert.deepEqual(action.allowedMethods, methods)
20
+ }).tags(['@modeling', '@action', '@update-action'])
21
+
22
+ test('constructor copies arrays (immutability)', ({ assert }) => {
23
+ const methods: ('PUT' | 'PATCH')[] = ['PUT']
24
+
25
+ const action = new UpdateAction({
26
+ allowedMethods: methods,
27
+ })
28
+
29
+ // Modify original source
30
+ methods.push('PATCH')
31
+
32
+ assert.deepEqual(action.allowedMethods, ['PUT'])
33
+ }).tags(['@modeling', '@action', '@update-action', '@immutability'])
34
+
35
+ test('toJSON returns valid schema', ({ assert }) => {
36
+ const action = new UpdateAction({
37
+ allowedMethods: ['PUT'],
38
+ })
39
+
40
+ const json = action.toJSON()
41
+
42
+ // Modify JSON
43
+ json.allowedMethods.push('PATCH')
44
+
45
+ assert.equal(json.kind, 'update')
46
+ assert.lengthOf(action.allowedMethods, 1) // Ensure immutability
47
+ assert.equal(action.allowedMethods[0], 'PUT')
48
+ }).tags(['@modeling', '@action', '@update-action', '@serialization', '@immutability'])
49
+
50
+ test('notifies change when allowedMethods changes', async ({ assert }) => {
51
+ const action = new UpdateAction()
52
+ let notified = false
53
+ action.addEventListener('change', () => {
54
+ notified = true
55
+ })
56
+
57
+ action.allowedMethods = ['PUT', 'PATCH']
58
+ await Promise.resolve()
59
+ assert.isTrue(notified)
60
+ }).tags(['@modeling', '@action', '@update-action', '@observed'])
61
+
62
+ test('notifies change when allowedMethods value change', async ({ assert }) => {
63
+ const action = new UpdateAction()
64
+ let notified = false
65
+ action.addEventListener('change', () => {
66
+ notified = true
67
+ })
68
+
69
+ action.allowedMethods.push('PUT')
70
+ await Promise.resolve()
71
+ assert.isTrue(notified)
72
+ }).tags(['@modeling', '@action', '@update-action', '@observed'])
73
+ })
@@ -11,6 +11,9 @@ import {
11
11
  ExposedEntityKind,
12
12
  ExposedEntity,
13
13
  } from '../../../src/index.js'
14
+ import { AllowPublicAccessRule } from '../../../src/modeling/rules/AllowPublic.js'
15
+ import { RateLimitingConfiguration } from '../../../src/modeling/rules/RateLimitingConfiguration.js'
16
+ import { Action } from '../../../src/modeling/index.js'
14
17
 
15
18
  test.group('ApiModel.createSchema()', () => {
16
19
  test('creates a schema with default values', ({ assert }) => {
@@ -99,7 +102,7 @@ test.group('ApiModel.constructor()', () => {
99
102
  assert.isUndefined(model.authentication)
100
103
  assert.isUndefined(model.authorization)
101
104
  assert.isUndefined(model.session)
102
- assert.isUndefined(model.accessRule)
105
+ assert.isEmpty(model.accessRule)
103
106
  assert.isUndefined(model.rateLimiting)
104
107
  assert.isUndefined(model.termsOfService)
105
108
  assert.isUndefined(model.contact)
@@ -144,8 +147,8 @@ test.group('ApiModel.constructor()', () => {
144
147
  assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
145
148
  assert.deepEqual(model.authorization, { strategy: 'RBAC', roleKey: 'role' })
146
149
  assert.deepEqual(model.session, { secret: 'secret', properties: ['email'] })
147
- assert.deepEqual(model.accessRule, [{ type: 'public' }])
148
- assert.deepEqual(model.rateLimiting, { rules: [] })
150
+ assert.deepEqual(model.accessRule, [new AllowPublicAccessRule()])
151
+ assert.deepEqual(model.rateLimiting, new RateLimitingConfiguration())
149
152
  assert.equal(model.termsOfService, 'https://example.com/terms')
150
153
  assert.deepEqual(model.contact, { name: 'John Doe', email: 'john.doe@example.com' })
151
154
  assert.deepEqual(model.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
@@ -193,6 +196,30 @@ test.group('ApiModel.constructor()', () => {
193
196
  await Promise.resolve() // Allow microtask to run
194
197
  assert.isTrue(notified)
195
198
  }).tags(['@modeling', '@api', '@creation'])
199
+
200
+ test('notifies change when nested action changes', async ({ assert }) => {
201
+ const model = new ApiModel({
202
+ exposes: [
203
+ {
204
+ kind: ExposedEntityKind,
205
+ key: 'e1',
206
+ entity: { key: 'e1' },
207
+ resourcePath: '/e1',
208
+ hasCollection: false,
209
+ actions: [{ kind: 'read' }],
210
+ },
211
+ ],
212
+ })
213
+
214
+ let notified = false
215
+ model.addEventListener('change', () => {
216
+ notified = true
217
+ })
218
+
219
+ model.exposes[0].actions[0].kind = 'write'
220
+ await Promise.resolve()
221
+ assert.isTrue(notified)
222
+ }).tags(['@modeling', '@api', '@observed'])
196
223
  })
197
224
 
198
225
  test.group('ApiModel.toJSON()', () => {
@@ -260,6 +287,24 @@ test.group('ApiModel.toJSON()', () => {
260
287
  assert.deepEqual(json.contact, { name: 'John Doe', email: 'john.doe@example.com' })
261
288
  assert.deepEqual(json.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
262
289
  }).tags(['@modeling', '@api', '@serialization'])
290
+
291
+ test('actions are immutable', ({ assert }) => {
292
+ const model = new ApiModel()
293
+ const action = new Action({ kind: 'read' })
294
+ const entityKey = 'get-entity'
295
+ const exposed: ExposedEntitySchema = {
296
+ key: entityKey,
297
+ actions: [action.toJSON()],
298
+ hasCollection: true,
299
+ kind: ExposedEntityKind,
300
+ resourcePath: '/',
301
+ entity: { key: entityKey },
302
+ }
303
+ model.exposes.push(new ExposedEntity(model, exposed))
304
+ const json = model.toJSON()
305
+ json.exposes[0].actions[0].kind = 'write'
306
+ assert.equal(model.exposes[0].actions[0].kind, 'read')
307
+ }).tags(['@modeling', '@api', '@serialization'])
263
308
  })
264
309
 
265
310
  test.group('ApiModel.getExposedEntity()', () => {
@@ -152,4 +152,77 @@ test.group('ExposedEntity', () => {
152
152
  await Promise.resolve()
153
153
  assert.isAtLeast(notified, 1)
154
154
  }).tags(['@modeling', '@exposed-entity', '@observed'])
155
+
156
+ test('restores actions and rules from schema', ({ assert }) => {
157
+ const model = new ApiModel()
158
+ const ex = new ExposedEntity(model, {
159
+ actions: [{ kind: 'read', accessRule: [{ type: 'public' }] }],
160
+ accessRule: [{ type: 'admin' }],
161
+ })
162
+
163
+ assert.lengthOf(ex.actions, 1)
164
+ assert.equal(ex.actions[0].kind, 'read')
165
+ assert.lengthOf(ex.actions[0].accessRule, 1)
166
+ assert.equal(ex.actions[0].accessRule[0].type, 'public')
167
+
168
+ assert.lengthOf(ex.accessRule!, 1)
169
+ assert.equal(ex.accessRule![0].type, 'admin')
170
+ }).tags(['@modeling', '@exposed-entity'])
171
+
172
+ test('serializes actions and rules', ({ assert }) => {
173
+ const model = new ApiModel()
174
+ const ex = new ExposedEntity(model, {
175
+ actions: [{ kind: 'read' }],
176
+ accessRule: [{ type: 'admin' }],
177
+ })
178
+
179
+ const json = ex.toJSON()
180
+ assert.deepInclude(json.actions[0], { kind: 'read' })
181
+ assert.deepInclude(json.accessRule![0], { type: 'admin' })
182
+ }).tags(['@modeling', '@exposed-entity'])
183
+
184
+ test('notifies change when action property changes', async ({ assert }) => {
185
+ const model = new ApiModel()
186
+ const ex = new ExposedEntity(model, {
187
+ actions: [{ kind: 'read' }],
188
+ })
189
+
190
+ let notified = false
191
+ ex.addEventListener('change', () => {
192
+ notified = true
193
+ })
194
+
195
+ ex.actions[0].kind = 'write'
196
+ await Promise.resolve()
197
+ assert.isTrue(notified)
198
+ }).tags(['@modeling', '@exposed-entity', '@observed'])
199
+
200
+ test('constructor copies actions array (immutability)', ({ assert }) => {
201
+ const model = new ApiModel()
202
+ const actions = [{ kind: 'read' }]
203
+ const ex = new ExposedEntity(model, { actions })
204
+
205
+ // Modify original array
206
+ actions.push({ kind: 'write' })
207
+ actions[0].kind = 'changed'
208
+
209
+ assert.lengthOf(ex.actions, 1)
210
+ assert.equal(ex.actions[0].kind, 'read')
211
+ }).tags(['@modeling', '@exposed-entity', '@immutability'])
212
+
213
+ test('constructor copies accessRule array (immutability)', ({ assert }) => {
214
+ const model = new ApiModel()
215
+ const rules = [{ type: 'public' }]
216
+ const ex = new ExposedEntity(model, {
217
+ actions: [],
218
+ accessRule: rules,
219
+ })
220
+
221
+ // Modify original array
222
+ rules.push({ type: 'admin' })
223
+ rules[0].type = 'changed'
224
+
225
+ assert.lengthOf(ex.accessRule!, 1)
226
+ assert.equal(ex.accessRule![0].type, 'public')
227
+ }).tags(['@modeling', '@exposed-entity', '@immutability'])
155
228
  })
@@ -0,0 +1,42 @@
1
+ import { test } from '@japa/runner'
2
+ import { AccessRule, type AccessRuleSchema } from '../../../../src/modeling/index.js'
3
+
4
+ test.group('AccessRule', () => {
5
+ test('initializes with default values', ({ assert }) => {
6
+ const rule = new AccessRule()
7
+ assert.equal(rule.type, '')
8
+ }).tags(['@modeling', '@access-rule'])
9
+
10
+ test('initializes with provided values', ({ assert }) => {
11
+ const schema: AccessRuleSchema = { type: 'public' }
12
+ const rule = new AccessRule(schema)
13
+ assert.equal(rule.type, 'public')
14
+ }).tags(['@modeling', '@access-rule'])
15
+
16
+ test('serializes to JSON', ({ assert }) => {
17
+ const rule = new AccessRule({ type: 'authenticated' })
18
+ const json = rule.toJSON()
19
+ assert.deepEqual(json, { type: 'authenticated' })
20
+ }).tags(['@modeling', '@access-rule'])
21
+
22
+ test('notifies change', async ({ assert }) => {
23
+ const rule = new AccessRule({ type: 'public' })
24
+ let notified = false
25
+ rule.addEventListener('change', () => {
26
+ notified = true
27
+ })
28
+
29
+ rule.notifyChange()
30
+ assert.isTrue(notified)
31
+ }).tags(['@modeling', '@access-rule'])
32
+
33
+ test('toJSON returns safe copy (immutability)', ({ assert }) => {
34
+ const rule = new AccessRule({ type: 'public' })
35
+ const json = rule.toJSON()
36
+
37
+ // Modify JSON (simulate runtime mutation)
38
+ json.type = 'changed'
39
+
40
+ assert.equal(rule.type, 'public')
41
+ }).tags(['@modeling', '@access-rule', '@immutability'])
42
+ })
@@ -0,0 +1,28 @@
1
+ import { test } from '@japa/runner'
2
+ import { AllowAuthenticatedAccessRule } from '../../../../src/modeling/rules/AllowAuthenticated.js'
3
+
4
+ test.group('AllowAuthenticatedAccessRule', () => {
5
+ test('initializes with correct type', ({ assert }) => {
6
+ const rule = new AllowAuthenticatedAccessRule()
7
+ assert.equal(rule.type, 'authenticated')
8
+ }).tags(['@modeling', '@rule', '@allow-authenticated'])
9
+
10
+ test('toJSON returns valid schema', ({ assert }) => {
11
+ const rule = new AllowAuthenticatedAccessRule()
12
+ const json = rule.toJSON()
13
+
14
+ assert.equal(json.type, 'authenticated')
15
+ }).tags(['@modeling', '@rule', '@allow-authenticated', '@serialization'])
16
+
17
+ test('notifies change', async ({ assert }) => {
18
+ const rule = new AllowAuthenticatedAccessRule()
19
+ let notified = false
20
+ rule.addEventListener('change', () => {
21
+ notified = true
22
+ })
23
+
24
+ rule.notifyChange()
25
+ await Promise.resolve()
26
+ assert.isTrue(notified)
27
+ }).tags(['@modeling', '@rule', '@allow-authenticated', '@observed'])
28
+ })
@@ -0,0 +1,28 @@
1
+ import { test } from '@japa/runner'
2
+ import { AllowPublicAccessRule } from '../../../../src/modeling/rules/AllowPublic.js'
3
+
4
+ test.group('AllowPublicAccessRule', () => {
5
+ test('initializes with correct type', ({ assert }) => {
6
+ const rule = new AllowPublicAccessRule()
7
+ assert.equal(rule.type, 'public')
8
+ }).tags(['@modeling', '@rule', '@allow-public'])
9
+
10
+ test('toJSON returns valid schema', ({ assert }) => {
11
+ const rule = new AllowPublicAccessRule()
12
+ const json = rule.toJSON()
13
+
14
+ assert.equal(json.type, 'public')
15
+ }).tags(['@modeling', '@rule', '@allow-public', '@serialization'])
16
+
17
+ test('notifies change', async ({ assert }) => {
18
+ const rule = new AllowPublicAccessRule()
19
+ let notified = false
20
+ rule.addEventListener('change', () => {
21
+ notified = true
22
+ })
23
+
24
+ rule.notifyChange()
25
+ await Promise.resolve()
26
+ assert.isTrue(notified)
27
+ }).tags(['@modeling', '@rule', '@allow-public', '@observed'])
28
+ })
@@ -0,0 +1,52 @@
1
+ import { test } from '@japa/runner'
2
+ import { MatchEmailDomainAccessRule } from '../../../../src/modeling/rules/MatchEmailDomain.js'
3
+
4
+ test.group('MatchEmailDomainAccessRule', () => {
5
+ test('initializes with default values', ({ assert }) => {
6
+ const rule = new MatchEmailDomainAccessRule()
7
+ assert.equal(rule.type, 'matchEmailDomain')
8
+ assert.deepEqual(rule.domains, [])
9
+ }).tags(['@modeling', '@rule', '@match-email-domain'])
10
+
11
+ test('initializes with provided values', ({ assert }) => {
12
+ const domains = ['example.com', 'test.org']
13
+ const rule = new MatchEmailDomainAccessRule({ domains })
14
+
15
+ assert.equal(rule.type, 'matchEmailDomain')
16
+ assert.deepEqual(rule.domains, domains)
17
+ }).tags(['@modeling', '@rule', '@match-email-domain'])
18
+
19
+ test('constructor copies arrays (immutability)', ({ assert }) => {
20
+ const domains = ['example.com']
21
+ const rule = new MatchEmailDomainAccessRule({ domains })
22
+
23
+ // Modify original source
24
+ domains.push('hacker.com')
25
+
26
+ assert.deepEqual(rule.domains, ['example.com'])
27
+ }).tags(['@modeling', '@rule', '@match-email-domain', '@immutability'])
28
+
29
+ test('toJSON returns valid schema', ({ assert }) => {
30
+ const rule = new MatchEmailDomainAccessRule({ domains: ['example.com'] })
31
+ const json = rule.toJSON()
32
+
33
+ // Modify JSON
34
+ json.domains.push('hacker.com')
35
+
36
+ assert.equal(json.type, 'matchEmailDomain')
37
+ assert.lengthOf(rule.domains, 1) // Ensure immutability
38
+ assert.equal(rule.domains[0], 'example.com')
39
+ }).tags(['@modeling', '@rule', '@match-email-domain', '@serialization', '@immutability'])
40
+
41
+ test('notifies change when domains changes', async ({ assert }) => {
42
+ const rule = new MatchEmailDomainAccessRule()
43
+ let notified = false
44
+ rule.addEventListener('change', () => {
45
+ notified = true
46
+ })
47
+
48
+ rule.domains = ['example.com']
49
+ await Promise.resolve()
50
+ assert.isTrue(notified)
51
+ }).tags(['@modeling', '@rule', '@match-email-domain', '@observed'])
52
+ })
@@ -0,0 +1,37 @@
1
+ import { test } from '@japa/runner'
2
+ import { MatchResourceOwnerAccessRule } from '../../../../src/modeling/rules/MatchResourceOwner.js'
3
+
4
+ test.group('MatchResourceOwnerAccessRule', () => {
5
+ test('initializes with default values', ({ assert }) => {
6
+ const rule = new MatchResourceOwnerAccessRule()
7
+ assert.equal(rule.type, 'resourceOwner')
8
+ assert.equal(rule.property, '')
9
+ }).tags(['@modeling', '@rule', '@match-resource-owner'])
10
+
11
+ test('initializes with provided values', ({ assert }) => {
12
+ const rule = new MatchResourceOwnerAccessRule({ property: 'creatorId' })
13
+
14
+ assert.equal(rule.type, 'resourceOwner')
15
+ assert.equal(rule.property, 'creatorId')
16
+ }).tags(['@modeling', '@rule', '@match-resource-owner'])
17
+
18
+ test('toJSON returns valid schema', ({ assert }) => {
19
+ const rule = new MatchResourceOwnerAccessRule({ property: 'creatorId' })
20
+ const json = rule.toJSON()
21
+
22
+ assert.equal(json.type, 'resourceOwner')
23
+ assert.equal(json.property, 'creatorId')
24
+ }).tags(['@modeling', '@rule', '@match-resource-owner', '@serialization'])
25
+
26
+ test('notifies change when property changes', async ({ assert }) => {
27
+ const rule = new MatchResourceOwnerAccessRule()
28
+ let notified = false
29
+ rule.addEventListener('change', () => {
30
+ notified = true
31
+ })
32
+
33
+ rule.property = 'ownerId'
34
+ await Promise.resolve()
35
+ assert.isTrue(notified)
36
+ }).tags(['@modeling', '@rule', '@match-resource-owner', '@observed'])
37
+ })
@@ -0,0 +1,58 @@
1
+ import { test } from '@japa/runner'
2
+ import { MatchUserPropertyAccessRule } from '../../../../src/modeling/rules/MatchUserProperty.js'
3
+
4
+ test.group('MatchUserPropertyAccessRule', () => {
5
+ test('initializes with default values', ({ assert }) => {
6
+ const rule = new MatchUserPropertyAccessRule()
7
+ assert.equal(rule.type, 'matchUserProperty')
8
+ assert.equal(rule.property, '')
9
+ assert.equal(rule.value, '')
10
+ }).tags(['@modeling', '@rule', '@match-user-property'])
11
+
12
+ test('initializes with provided values', ({ assert }) => {
13
+ const rule = new MatchUserPropertyAccessRule({
14
+ property: 'department',
15
+ value: 'engineering',
16
+ })
17
+
18
+ assert.equal(rule.type, 'matchUserProperty')
19
+ assert.equal(rule.property, 'department')
20
+ assert.equal(rule.value, 'engineering')
21
+ }).tags(['@modeling', '@rule', '@match-user-property'])
22
+
23
+ test('toJSON returns valid schema', ({ assert }) => {
24
+ const rule = new MatchUserPropertyAccessRule({
25
+ property: 'department',
26
+ value: 'engineering',
27
+ })
28
+ const json = rule.toJSON()
29
+
30
+ assert.equal(json.type, 'matchUserProperty')
31
+ assert.equal(json.property, 'department')
32
+ assert.equal(json.value, 'engineering')
33
+ }).tags(['@modeling', '@rule', '@match-user-property', '@serialization'])
34
+
35
+ test('notifies change when property changes', async ({ assert }) => {
36
+ const rule = new MatchUserPropertyAccessRule()
37
+ let notified = false
38
+ rule.addEventListener('change', () => {
39
+ notified = true
40
+ })
41
+
42
+ rule.property = 'dept'
43
+ await Promise.resolve()
44
+ assert.isTrue(notified)
45
+ }).tags(['@modeling', '@rule', '@match-user-property', '@observed'])
46
+
47
+ test('notifies change when value changes', async ({ assert }) => {
48
+ const rule = new MatchUserPropertyAccessRule()
49
+ let notified = false
50
+ rule.addEventListener('change', () => {
51
+ notified = true
52
+ })
53
+
54
+ rule.value = 'sales'
55
+ await Promise.resolve()
56
+ assert.isTrue(notified)
57
+ }).tags(['@modeling', '@rule', '@match-user-property', '@observed'])
58
+ })