@api-client/core 0.19.9 → 0.19.10

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 (98) hide show
  1. package/Testing.md +1 -1
  2. package/build/src/decorators/observed.d.ts.map +1 -1
  3. package/build/src/decorators/observed.js +91 -0
  4. package/build/src/decorators/observed.js.map +1 -1
  5. package/build/src/modeling/ApiModel.d.ts +21 -7
  6. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  7. package/build/src/modeling/ApiModel.js +70 -29
  8. package/build/src/modeling/ApiModel.js.map +1 -1
  9. package/build/src/modeling/DomainValidation.d.ts +1 -1
  10. package/build/src/modeling/DomainValidation.d.ts.map +1 -1
  11. package/build/src/modeling/DomainValidation.js.map +1 -1
  12. package/build/src/modeling/ExposedEntity.d.ts +14 -0
  13. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  14. package/build/src/modeling/ExposedEntity.js +59 -6
  15. package/build/src/modeling/ExposedEntity.js.map +1 -1
  16. package/build/src/modeling/actions/Action.d.ts +11 -1
  17. package/build/src/modeling/actions/Action.d.ts.map +1 -1
  18. package/build/src/modeling/actions/Action.js +21 -3
  19. package/build/src/modeling/actions/Action.js.map +1 -1
  20. package/build/src/modeling/actions/CreateAction.d.ts +2 -1
  21. package/build/src/modeling/actions/CreateAction.d.ts.map +1 -1
  22. package/build/src/modeling/actions/CreateAction.js +2 -2
  23. package/build/src/modeling/actions/CreateAction.js.map +1 -1
  24. package/build/src/modeling/actions/DeleteAction.d.ts +2 -1
  25. package/build/src/modeling/actions/DeleteAction.d.ts.map +1 -1
  26. package/build/src/modeling/actions/DeleteAction.js +2 -2
  27. package/build/src/modeling/actions/DeleteAction.js.map +1 -1
  28. package/build/src/modeling/actions/ListAction.d.ts +2 -1
  29. package/build/src/modeling/actions/ListAction.d.ts.map +1 -1
  30. package/build/src/modeling/actions/ListAction.js +2 -2
  31. package/build/src/modeling/actions/ListAction.js.map +1 -1
  32. package/build/src/modeling/actions/ReadAction.d.ts +2 -1
  33. package/build/src/modeling/actions/ReadAction.d.ts.map +1 -1
  34. package/build/src/modeling/actions/ReadAction.js +2 -2
  35. package/build/src/modeling/actions/ReadAction.js.map +1 -1
  36. package/build/src/modeling/actions/SearchAction.d.ts +2 -1
  37. package/build/src/modeling/actions/SearchAction.d.ts.map +1 -1
  38. package/build/src/modeling/actions/SearchAction.js +2 -2
  39. package/build/src/modeling/actions/SearchAction.js.map +1 -1
  40. package/build/src/modeling/actions/UpdateAction.d.ts +2 -1
  41. package/build/src/modeling/actions/UpdateAction.d.ts.map +1 -1
  42. package/build/src/modeling/actions/UpdateAction.js +2 -2
  43. package/build/src/modeling/actions/UpdateAction.js.map +1 -1
  44. package/build/src/modeling/actions/index.d.ts +2 -1
  45. package/build/src/modeling/actions/index.d.ts.map +1 -1
  46. package/build/src/modeling/actions/index.js +7 -7
  47. package/build/src/modeling/actions/index.js.map +1 -1
  48. package/build/src/modeling/index.d.ts +1 -0
  49. package/build/src/modeling/index.d.ts.map +1 -1
  50. package/build/src/modeling/index.js +1 -0
  51. package/build/src/modeling/index.js.map +1 -1
  52. package/build/src/modeling/types.d.ts +67 -0
  53. package/build/src/modeling/types.d.ts.map +1 -1
  54. package/build/src/modeling/types.js.map +1 -1
  55. package/build/src/modeling/validation/api_model_rules.d.ts +15 -0
  56. package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -0
  57. package/build/src/modeling/validation/api_model_rules.js +599 -0
  58. package/build/src/modeling/validation/api_model_rules.js.map +1 -0
  59. package/build/src/modeling/validation/association_validation.d.ts.map +1 -1
  60. package/build/src/modeling/validation/association_validation.js +1 -3
  61. package/build/src/modeling/validation/association_validation.js.map +1 -1
  62. package/build/tsconfig.tsbuildinfo +1 -1
  63. package/data/models/example-generator-api.json +8 -8
  64. package/eslint.config.js +0 -1
  65. package/package.json +17 -122
  66. package/src/decorators/observed.ts +91 -0
  67. package/src/modeling/ApiModel.ts +73 -33
  68. package/src/modeling/DomainValidation.ts +1 -1
  69. package/src/modeling/ExposedEntity.ts +63 -9
  70. package/src/modeling/actions/Action.ts +25 -2
  71. package/src/modeling/actions/CreateAction.ts +3 -2
  72. package/src/modeling/actions/DeleteAction.ts +3 -2
  73. package/src/modeling/actions/ListAction.ts +3 -2
  74. package/src/modeling/actions/ReadAction.ts +3 -2
  75. package/src/modeling/actions/SearchAction.ts +3 -2
  76. package/src/modeling/actions/UpdateAction.ts +3 -2
  77. package/src/modeling/types.ts +70 -0
  78. package/src/modeling/validation/api_model_rules.ts +640 -0
  79. package/src/modeling/validation/api_model_validation_rules.md +58 -0
  80. package/src/modeling/validation/association_validation.ts +1 -3
  81. package/tests/unit/modeling/actions/Action.spec.ts +40 -8
  82. package/tests/unit/modeling/actions/CreateAction.spec.ts +5 -5
  83. package/tests/unit/modeling/actions/DeleteAction.spec.ts +6 -6
  84. package/tests/unit/modeling/actions/ListAction.spec.ts +7 -7
  85. package/tests/unit/modeling/actions/ReadAction.spec.ts +6 -6
  86. package/tests/unit/modeling/actions/SearchAction.spec.ts +6 -6
  87. package/tests/unit/modeling/actions/UpdateAction.spec.ts +6 -6
  88. package/tests/unit/modeling/api_model.spec.ts +190 -13
  89. package/tests/unit/modeling/api_model_expose_entity.spec.ts +43 -19
  90. package/tests/unit/modeling/api_model_remove_entity.spec.ts +6 -6
  91. package/tests/unit/modeling/exposed_entity.spec.ts +123 -3
  92. package/tests/unit/modeling/exposed_entity_actions.spec.ts +41 -18
  93. package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +1 -1
  94. package/tests/unit/modeling/rules/restoring_rules.spec.ts +9 -5
  95. package/tests/unit/modeling/validation/api_model_rules.spec.ts +324 -0
  96. package/tsconfig.browser.json +1 -1
  97. package/tsconfig.node.json +1 -1
  98. package/bin/test-web.ts +0 -6
@@ -2,13 +2,19 @@ import { test } from '@japa/runner'
2
2
  import {
3
3
  ApiModel,
4
4
  CreateAction,
5
+ type CreateActionSchema,
5
6
  DataDomain,
6
7
  DeleteAction,
8
+ type DeleteActionSchema,
7
9
  DomainEntity,
8
10
  ListAction,
11
+ type ListActionSchema,
9
12
  ReadAction,
13
+ type ReadActionSchema,
10
14
  SearchAction,
15
+ type SearchActionSchema,
11
16
  UpdateAction,
17
+ type UpdateActionSchema,
12
18
  } from '../../../src/modeling/index.js'
13
19
  import { ExposedEntityKind } from '../../../src/models/kinds.js'
14
20
  import { nanoid } from '../../../src/nanoid.js'
@@ -25,7 +31,12 @@ test.group('ExposedEntity::actions', (group) => {
25
31
  })
26
32
 
27
33
  test('restores a list acton', ({ assert }) => {
28
- const action = new ListAction()
34
+ const action: ListActionSchema = {
35
+ kind: 'list',
36
+ pagination: { kind: '' },
37
+ sortableFields: [],
38
+ filterableFields: [],
39
+ }
29
40
  const model = new ApiModel(
30
41
  {
31
42
  exposes: [
@@ -37,21 +48,23 @@ test.group('ExposedEntity::actions', (group) => {
37
48
  collectionPath: '/things',
38
49
  resourcePath: '/things/{id}',
39
50
  isRoot: true,
40
- actions: [action.toJSON()],
51
+ actions: [action],
41
52
  },
42
53
  ],
43
54
  },
44
55
  domain
45
56
  )
46
57
 
47
- const restoredAction = model.exposes[0].actions[0]
58
+ const restoredAction = Array.from(model.exposes.values())[0]!.actions[0]
48
59
  assert.ok(restoredAction, 'Action should be restored')
49
60
  assert.equal(restoredAction.kind, 'list', 'Action kind should be list')
50
61
  assert.instanceOf(restoredAction, ListAction, 'Action should be an instance of ListAction')
51
62
  }).tags(['@modeling', '@action', '@restoring'])
52
63
 
53
64
  test('restores a create acton', ({ assert }) => {
54
- const action = new CreateAction()
65
+ const action: CreateActionSchema = {
66
+ kind: 'create',
67
+ }
55
68
  const model = new ApiModel(
56
69
  {
57
70
  exposes: [
@@ -63,21 +76,23 @@ test.group('ExposedEntity::actions', (group) => {
63
76
  collectionPath: '/things',
64
77
  resourcePath: '/things/{id}',
65
78
  isRoot: true,
66
- actions: [action.toJSON()],
79
+ actions: [action],
67
80
  },
68
81
  ],
69
82
  },
70
83
  domain
71
84
  )
72
85
 
73
- const restoredAction = model.exposes[0].actions[0]
86
+ const restoredAction = Array.from(model.exposes.values())[0]!.actions[0]
74
87
  assert.ok(restoredAction, 'Action should be restored')
75
88
  assert.equal(restoredAction.kind, 'create', 'Action kind should be create')
76
89
  assert.instanceOf(restoredAction, CreateAction, 'Action should be an instance of CreateAction')
77
90
  }).tags(['@modeling', '@action', '@restoring'])
78
91
 
79
92
  test('restores a read acton', ({ assert }) => {
80
- const action = new ReadAction()
93
+ const action: ReadActionSchema = {
94
+ kind: 'read',
95
+ }
81
96
  const model = new ApiModel(
82
97
  {
83
98
  exposes: [
@@ -89,21 +104,24 @@ test.group('ExposedEntity::actions', (group) => {
89
104
  collectionPath: '/things',
90
105
  resourcePath: '/things/{id}',
91
106
  isRoot: true,
92
- actions: [action.toJSON()],
107
+ actions: [action],
93
108
  },
94
109
  ],
95
110
  },
96
111
  domain
97
112
  )
98
113
 
99
- const restoredAction = model.exposes[0].actions[0]
114
+ const restoredAction = Array.from(model.exposes.values())[0]!.actions[0]
100
115
  assert.ok(restoredAction, 'Action should be restored')
101
116
  assert.equal(restoredAction.kind, 'read', 'Action kind should be read')
102
117
  assert.instanceOf(restoredAction, ReadAction, 'Action should be an instance of ReadAction')
103
118
  }).tags(['@modeling', '@action', '@restoring'])
104
119
 
105
120
  test('restores a update acton', ({ assert }) => {
106
- const action = new UpdateAction()
121
+ const action: UpdateActionSchema = {
122
+ kind: 'update',
123
+ allowedMethods: [],
124
+ }
107
125
  const model = new ApiModel(
108
126
  {
109
127
  exposes: [
@@ -115,21 +133,23 @@ test.group('ExposedEntity::actions', (group) => {
115
133
  collectionPath: '/things',
116
134
  resourcePath: '/things/{id}',
117
135
  isRoot: true,
118
- actions: [action.toJSON()],
136
+ actions: [action],
119
137
  },
120
138
  ],
121
139
  },
122
140
  domain
123
141
  )
124
142
 
125
- const restoredAction = model.exposes[0].actions[0]
143
+ const restoredAction = Array.from(model.exposes.values())[0]!.actions[0]
126
144
  assert.ok(restoredAction, 'Action should be restored')
127
145
  assert.equal(restoredAction.kind, 'update', 'Action kind should be update')
128
146
  assert.instanceOf(restoredAction, UpdateAction, 'Action should be an instance of UpdateAction')
129
147
  }).tags(['@modeling', '@action', '@restoring'])
130
148
 
131
149
  test('restores a delete acton', ({ assert }) => {
132
- const action = new DeleteAction()
150
+ const action: DeleteActionSchema = {
151
+ kind: 'delete',
152
+ }
133
153
  const model = new ApiModel(
134
154
  {
135
155
  exposes: [
@@ -141,21 +161,24 @@ test.group('ExposedEntity::actions', (group) => {
141
161
  collectionPath: '/things',
142
162
  resourcePath: '/things/{id}',
143
163
  isRoot: true,
144
- actions: [action.toJSON()],
164
+ actions: [action],
145
165
  },
146
166
  ],
147
167
  },
148
168
  domain
149
169
  )
150
170
 
151
- const restoredAction = model.exposes[0].actions[0]
171
+ const restoredAction = Array.from(model.exposes.values())[0]!.actions[0]
152
172
  assert.ok(restoredAction, 'Action should be restored')
153
173
  assert.equal(restoredAction.kind, 'delete', 'Action kind should be delete')
154
174
  assert.instanceOf(restoredAction, DeleteAction, 'Action should be an instance of DeleteAction')
155
175
  }).tags(['@modeling', '@action', '@restoring'])
156
176
 
157
177
  test('restores a search acton', ({ assert }) => {
158
- const action = new SearchAction()
178
+ const action: SearchActionSchema = {
179
+ kind: 'search',
180
+ fields: [],
181
+ }
159
182
  const model = new ApiModel(
160
183
  {
161
184
  exposes: [
@@ -167,14 +190,14 @@ test.group('ExposedEntity::actions', (group) => {
167
190
  collectionPath: '/things',
168
191
  resourcePath: '/things/{id}',
169
192
  isRoot: true,
170
- actions: [action.toJSON()],
193
+ actions: [action],
171
194
  },
172
195
  ],
173
196
  },
174
197
  domain
175
198
  )
176
199
 
177
- const restoredAction = model.exposes[0].actions[0]
200
+ const restoredAction = Array.from(model.exposes.values())[0]!.actions[0]
178
201
  assert.ok(restoredAction, 'Action should be restored')
179
202
  assert.equal(restoredAction.kind, 'search', 'Action kind should be search')
180
203
  assert.instanceOf(restoredAction, SearchAction, 'Action should be an instance of SearchAction')
@@ -84,7 +84,7 @@ test.group('ExposedEntity Path Setter Validation', () => {
84
84
 
85
85
  // expose A -> B
86
86
  const rootExp = model.exposeEntity({ key: eA.key }, { followAssociations: true })
87
- const nestedExp = model.exposes.find((e) => !e.isRoot)
87
+ const nestedExp = Array.from(model.exposes.values()).find((e) => !e.isRoot)
88
88
 
89
89
  assert.isDefined(nestedExp)
90
90
  // Assuming root entity has collection path /as
@@ -1,5 +1,5 @@
1
1
  import { test } from '@japa/runner'
2
- import { ApiModel, DataDomain, DomainEntity, ListAction } from '../../../../src/modeling/index.js'
2
+ import { ApiModel, DataDomain, DomainEntity, type ListActionSchema } from '../../../../src/modeling/index.js'
3
3
  import {
4
4
  AllowAuthenticatedAccessRule,
5
5
  AllowPublicAccessRule,
@@ -71,7 +71,7 @@ test.group('restoring actions', (group) => {
71
71
  domain
72
72
  )
73
73
 
74
- const ex = model.exposes[0]!
74
+ const ex = Array.from(model.exposes.values())[0]!
75
75
  assert.lengthOf(ex.accessRule!, 6)
76
76
  assert.instanceOf(ex.accessRule![0], AllowAuthenticatedAccessRule)
77
77
  assert.instanceOf(ex.accessRule![1], AllowPublicAccessRule)
@@ -88,9 +88,13 @@ test.group('restoring actions', (group) => {
88
88
  const r4 = new MatchResourceOwnerAccessRule()
89
89
  const r5 = new MatchUserPropertyAccessRule()
90
90
  const r6 = new MatchUserRoleAccessRule()
91
- const action = new ListAction({
91
+ const action: ListActionSchema = {
92
+ kind: 'list',
93
+ pagination: { kind: '' },
94
+ sortableFields: [],
95
+ filterableFields: [],
92
96
  accessRule: [r1.toJSON(), r2.toJSON(), r3.toJSON(), r4.toJSON(), r5.toJSON(), r6.toJSON()],
93
- }).toJSON()
97
+ }
94
98
  const model = new ApiModel(
95
99
  {
96
100
  exposes: [
@@ -109,7 +113,7 @@ test.group('restoring actions', (group) => {
109
113
  domain
110
114
  )
111
115
 
112
- const ex = model.exposes[0]!.actions[0]!
116
+ const ex = Array.from(model.exposes.values())[0]!.actions[0]!
113
117
  assert.lengthOf(ex.accessRule!, 6)
114
118
  assert.instanceOf(ex.accessRule![0], AllowAuthenticatedAccessRule)
115
119
  assert.instanceOf(ex.accessRule![1], AllowPublicAccessRule)
@@ -0,0 +1,324 @@
1
+ import { test } from '@japa/runner'
2
+ import { ApiModel } from '../../../../src/modeling/ApiModel.js'
3
+ import { DataDomain } from '../../../../src/modeling/DataDomain.js'
4
+
5
+ import {
6
+ validateApiModelInfo,
7
+ validateApiModelDependency,
8
+ validateApiModelSecurity,
9
+ validateApiModelMetadata,
10
+ validateExposedEntity,
11
+ validateAction,
12
+ validateApiModel,
13
+ } from '../../../../src/modeling/validation/api_model_rules.js'
14
+ import { ExposedEntity } from '../../../../src/modeling/ExposedEntity.js'
15
+ import { ListAction } from '../../../../src/modeling/actions/ListAction.js'
16
+ import { DeleteAction } from '../../../../src/modeling/actions/DeleteAction.js'
17
+ import { ReadAction } from '../../../../src/modeling/actions/ReadAction.js'
18
+ import { SearchAction } from '../../../../src/modeling/actions/SearchAction.js'
19
+ import { UpdateAction } from '../../../../src/modeling/actions/UpdateAction.js'
20
+ import { RolesBasedAccessControl, UsernamePasswordConfiguration } from '../../../../src/modeling/types.js'
21
+ import { SemanticType } from '../../../../src/modeling/Semantics.js'
22
+
23
+ test.group('ApiModel Validation', () => {
24
+ test('validateApiModelInfo - success', ({ assert }) => {
25
+ const model = new ApiModel({
26
+ info: { name: 'Valid API', description: 'A test definition' },
27
+ })
28
+ const issues = validateApiModelInfo(model)
29
+ assert.isEmpty(issues)
30
+ })
31
+
32
+ test('validateApiModelInfo - missing required', ({ assert }) => {
33
+ const model = new ApiModel({
34
+ info: { name: '' },
35
+ })
36
+ const issues = validateApiModelInfo(model)
37
+ // Expect missing name and missing description
38
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_NAME'))
39
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_DESCRIPTION' && i.severity === 'warning'))
40
+ })
41
+
42
+ test('validateApiModelDependency - missing domain', ({ assert }) => {
43
+ const model = new ApiModel()
44
+ const issues = validateApiModelDependency(model)
45
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_DOMAIN'))
46
+ })
47
+
48
+ test('validateApiModelDependency - valid domain', ({ assert }) => {
49
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
50
+ const model = new ApiModel({}, domain)
51
+ const issues = validateApiModelDependency(model)
52
+ assert.isEmpty(issues)
53
+ })
54
+
55
+ test('validateApiModelSecurity - success', ({ assert }) => {
56
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
57
+ const modelNode = domain.addModel({ key: 'users' })
58
+ const userEntity = domain.addEntity(modelNode.key, { info: { name: 'User' } })
59
+ userEntity.addProperty({ key: 'pass', semantics: [{ id: SemanticType.Password }] })
60
+ userEntity.addProperty({ key: 'role', semantics: [{ id: SemanticType.UserRole }] })
61
+ userEntity.addProperty({ key: 'name', semantics: [{ id: SemanticType.Username }] })
62
+
63
+ const model = new ApiModel(
64
+ {
65
+ authentication: { strategy: 'UsernamePassword', passwordKey: 'pass' } as UsernamePasswordConfiguration,
66
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
67
+ session: {
68
+ secret: 'super-secret',
69
+ properties: ['id', 'role'],
70
+ cookie: {
71
+ enabled: true,
72
+ kind: 'cookie',
73
+ lifetime: '7d',
74
+ httpOnly: true,
75
+ secure: true,
76
+ sameSite: 'none',
77
+ name: 'as',
78
+ },
79
+ },
80
+ user: { key: userEntity.key, domain: domain.key },
81
+ },
82
+ domain
83
+ )
84
+
85
+ const issues = validateApiModelSecurity(model)
86
+ assert.isEmpty(issues)
87
+ })
88
+
89
+ test('validateApiModelSecurity - missing session properties', ({ assert }) => {
90
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
91
+ const model = new ApiModel(
92
+ {
93
+ authentication: { strategy: 'UsernamePassword', passwordKey: 'pass' } as UsernamePasswordConfiguration,
94
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
95
+ session: {
96
+ secret: 'super-secret',
97
+ properties: [],
98
+ cookie: {
99
+ enabled: true,
100
+ kind: 'cookie',
101
+ lifetime: '7d',
102
+ httpOnly: true,
103
+ secure: true,
104
+ sameSite: 'none',
105
+ name: 'as',
106
+ },
107
+ },
108
+ user: { key: 'nonexistent', domain: domain.key },
109
+ },
110
+ domain
111
+ )
112
+
113
+ const issues = validateApiModelSecurity(model)
114
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_SESSION_PROPERTIES'))
115
+ assert.isTrue(issues.some((i) => i.code === 'API_INVALID_USER_ENTITY'))
116
+ })
117
+
118
+ test('validateApiModelSecurity - missing password semantic', ({ assert }) => {
119
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
120
+ const modelNode = domain.addModel({ key: 'users' })
121
+ const userEntity = domain.addEntity(modelNode.key, { info: { name: 'User' } })
122
+ userEntity.addProperty({ key: 'pass' }) // missing password semantic
123
+ userEntity.addProperty({ key: 'role', semantics: [{ id: SemanticType.UserRole }] })
124
+ userEntity.addProperty({ key: 'name', semantics: [{ id: SemanticType.Username }] })
125
+
126
+ const model = new ApiModel(
127
+ {
128
+ authentication: { strategy: 'UsernamePassword', passwordKey: 'pass' } as UsernamePasswordConfiguration,
129
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
130
+ session: { secret: 'secret', properties: [] },
131
+ user: { key: userEntity.key, domain: domain.key },
132
+ },
133
+ domain
134
+ )
135
+
136
+ const issues = validateApiModelSecurity(model)
137
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_PASSWORD_SEMANTIC'))
138
+ })
139
+
140
+ test('validateApiModelSecurity - missing username semantic', ({ assert }) => {
141
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
142
+ const modelNode = domain.addModel({ key: 'users' })
143
+ const userEntity = domain.addEntity(modelNode.key, { info: { name: 'User' } })
144
+ userEntity.addProperty({ key: 'pass', semantics: [{ id: SemanticType.Password }] })
145
+ userEntity.addProperty({ key: 'role', semantics: [{ id: SemanticType.UserRole }] })
146
+ userEntity.addProperty({ key: 'name' }) // no username semantic
147
+
148
+ const model = new ApiModel(
149
+ {
150
+ authentication: { strategy: 'UsernamePassword', passwordKey: 'pass' } as UsernamePasswordConfiguration,
151
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
152
+ session: { secret: 'secret', properties: [] },
153
+ user: { key: userEntity.key, domain: domain.key },
154
+ },
155
+ domain
156
+ )
157
+
158
+ const issues = validateApiModelSecurity(model)
159
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_USERNAME_SEMANTIC'))
160
+ })
161
+
162
+ test('validateApiModelSecurity - missing role semantic', ({ assert }) => {
163
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
164
+ const modelNode = domain.addModel({ key: 'users' })
165
+ const userEntity = domain.addEntity(modelNode.key, { info: { name: 'User' } })
166
+ userEntity.addProperty({ key: 'pass', semantics: [{ id: SemanticType.Password }] })
167
+ userEntity.addProperty({ key: 'role' }) // missing role semantic
168
+ userEntity.addProperty({ key: 'name', semantics: [{ id: SemanticType.Username }] })
169
+
170
+ const model = new ApiModel(
171
+ {
172
+ authentication: { strategy: 'UsernamePassword', passwordKey: 'pass' } as UsernamePasswordConfiguration,
173
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
174
+ session: { secret: 'secret', properties: [] },
175
+ user: { key: userEntity.key, domain: domain.key },
176
+ },
177
+ domain
178
+ )
179
+
180
+ const issues = validateApiModelSecurity(model)
181
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_ROLE_SEMANTIC'))
182
+ })
183
+
184
+ test('validateApiModelMetadata - contact warnings', ({ assert }) => {
185
+ const model = new ApiModel()
186
+ const issues = validateApiModelMetadata(model)
187
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_CONTACT' && i.severity === 'info'))
188
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_LICENSE' && i.severity === 'info'))
189
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_TOS' && i.severity === 'info'))
190
+ })
191
+
192
+ test('validateApiModelMetadata - invalid urls and emails', ({ assert }) => {
193
+ const model = new ApiModel({
194
+ contact: { email: 'invalid-email', url: 'invalid-url' },
195
+ license: { name: 'MIT', url: 'invalid-url' },
196
+ })
197
+ const issues = validateApiModelMetadata(model)
198
+ assert.isTrue(issues.some((i) => i.code === 'API_INVALID_CONTACT_EMAIL'))
199
+ assert.isTrue(issues.some((i) => i.code === 'API_INVALID_CONTACT_URL'))
200
+ assert.isTrue(issues.some((i) => i.code === 'API_INVALID_LICENSE_URL'))
201
+ })
202
+
203
+ test('validateExposedEntity - missing actions', ({ assert }) => {
204
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
205
+ const model = new ApiModel({}, domain)
206
+ const exposure = new ExposedEntity(model, {
207
+ entity: { key: 'fake' },
208
+ collectionPath: '/items',
209
+ resourcePath: '/items/{id}',
210
+ hasCollection: true,
211
+ })
212
+ exposure.actions = [] // Override default behaviour
213
+
214
+ // override collectionPath/resourcePath defaults since exposure sets it if missing
215
+ exposure.collectionPath = '/items'
216
+ exposure.resourcePath = '/items/{id}'
217
+
218
+ const issues = validateExposedEntity(exposure, model)
219
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_MISSING_ACTIONS'))
220
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_INVALID_ENTITY_REF')) // because fake entity doesn't exist
221
+ })
222
+
223
+ test('validateExposedEntity - path integrity (has collection)', ({ assert }) => {
224
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
225
+ const model = new ApiModel({}, domain)
226
+ const exposure = new ExposedEntity(model, {
227
+ entity: { key: 'fake' },
228
+ hasCollection: true,
229
+ })
230
+ exposure.collectionPath = 'invalid_no_slash'
231
+ exposure.resourcePath = '/invalid_no_slash_resource'
232
+ exposure.actions = [new ReadAction(exposure)]
233
+ const issues = validateExposedEntity(exposure, model)
234
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_INVALID_COLLECTION_PATH'))
235
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_INVALID_RESOURCE_PATH_FORMAT'))
236
+ })
237
+
238
+ test('validateAction - List needs pagination', ({ assert }) => {
239
+ const model = new ApiModel()
240
+ const exposure = new ExposedEntity(model, { entity: { key: '1' } })
241
+ const action = new ListAction(exposure)
242
+ // @ts-expect-error testing undefined pagination
243
+ action.pagination = undefined
244
+
245
+ const issues = validateAction(action, exposure, model.key)
246
+ assert.isTrue(issues.some((i) => i.code === 'ACTION_LIST_MISSING_PAGINATION'))
247
+ })
248
+
249
+ test('validateAction - Search needs fields', ({ assert }) => {
250
+ const model = new ApiModel()
251
+ const exposure = new ExposedEntity(model, { entity: { key: '1' } })
252
+ const action = new SearchAction(exposure)
253
+ action.fields = []
254
+
255
+ const issues = validateAction(action, exposure, model.key)
256
+ assert.isTrue(issues.some((i) => i.code === 'ACTION_SEARCH_MISSING_FIELDS'))
257
+ })
258
+
259
+ test('validateAction - Delete needs strategy', ({ assert }) => {
260
+ const model = new ApiModel()
261
+ const exposure = new ExposedEntity(model, { entity: { key: '1' } })
262
+ const action = new DeleteAction(exposure)
263
+ // @ts-expect-error testing undefined strategy
264
+ action.strategy = undefined
265
+
266
+ const issues = validateAction(action, exposure, model.key)
267
+ assert.isTrue(issues.some((i) => i.code === 'ACTION_DELETE_MISSING_STRATEGY'))
268
+ })
269
+
270
+ test('validateAction - Update needs methods', ({ assert }) => {
271
+ const model = new ApiModel()
272
+ const exposure = new ExposedEntity(model, { entity: { key: '1' } })
273
+ const action = new UpdateAction(exposure)
274
+ action.allowedMethods = []
275
+
276
+ const issues = validateAction(action, exposure, model.key)
277
+ assert.isTrue(issues.some((i) => i.code === 'ACTION_UPDATE_MISSING_METHODS'))
278
+ })
279
+ test('validateApiModel - success aggregates without issues', ({ assert }) => {
280
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
281
+ const modelNode = domain.addModel({ key: 'users' })
282
+ const userEntity = domain.addEntity(modelNode.key, { info: { name: 'User' } })
283
+ userEntity.addProperty({ key: 'pass', semantics: [{ id: SemanticType.Password }] })
284
+ userEntity.addProperty({ key: 'role', semantics: [{ id: SemanticType.UserRole }] })
285
+ userEntity.addProperty({ key: 'name', semantics: [{ id: SemanticType.Username }] })
286
+ const model = new ApiModel(
287
+ {
288
+ info: { name: 'Valid API', description: 'A test definition' },
289
+ authentication: { strategy: 'UsernamePassword', passwordKey: 'pass' } as UsernamePasswordConfiguration,
290
+ authorization: { strategy: 'RBAC', roleKey: 'role' } as RolesBasedAccessControl,
291
+ session: {
292
+ secret: 'super-secret',
293
+ properties: ['id', 'role'],
294
+ cookie: {
295
+ enabled: true,
296
+ kind: 'cookie',
297
+ lifetime: '7d',
298
+ httpOnly: true,
299
+ secure: true,
300
+ sameSite: 'none',
301
+ name: 'as',
302
+ },
303
+ },
304
+ user: { key: userEntity.key, domain: domain.key },
305
+ },
306
+ domain
307
+ )
308
+ const result = validateApiModel(model)
309
+ assert.isTrue(result.isValid)
310
+ assert.isEmpty(result.issues.filter((i) => i.severity === 'error'))
311
+ })
312
+
313
+ test('validateApiModel - fails on multiple errors', ({ assert }) => {
314
+ const model = new ApiModel({
315
+ info: { name: '' },
316
+ })
317
+ // Now it's missing name, missing domain, missing security context, missing contact.
318
+ const result = validateApiModel(model)
319
+ assert.isFalse(result.isValid)
320
+ assert.isTrue(result.issues.some((i) => i.code === 'API_MISSING_NAME'))
321
+ assert.isTrue(result.issues.some((i) => i.code === 'API_MISSING_DOMAIN'))
322
+ assert.isTrue(result.issues.some((i) => i.code === 'API_MISSING_AUTHENTICATION'))
323
+ })
324
+ })
@@ -30,5 +30,5 @@
30
30
  "incremental": true
31
31
  },
32
32
  "include": ["src/**/*", "test/**/*"],
33
- "exclude": ["node_modules", "build", "coverage", ".tmp", ".wireit", "amf-models", "bin", "data", "docs", "scripts"],
33
+ "exclude": ["node_modules", "build", "coverage", ".tmp", "amf-models", "bin", "data", "docs", "scripts"],
34
34
  }
@@ -32,5 +32,5 @@
32
32
  "incremental": true
33
33
  },
34
34
  "include": ["src/**/*", "tests/**/*", "bin/test.ts"],
35
- "exclude": ["node_modules", "build", "coverage", ".tmp", ".wireit", "amf-models", "data", "docs", "scripts"],
35
+ "exclude": ["node_modules", "build", "coverage", ".tmp", "amf-models", "data", "docs", "scripts"],
36
36
  }
package/bin/test-web.ts DELETED
@@ -1,6 +0,0 @@
1
- import { startTestRunner } from '@web/test-runner'
2
-
3
- await startTestRunner({
4
- autoExitProcess: true,
5
- readCliArgs: true,
6
- })