@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.
- package/Testing.md +1 -1
- package/build/src/decorators/observed.d.ts.map +1 -1
- package/build/src/decorators/observed.js +91 -0
- package/build/src/decorators/observed.js.map +1 -1
- package/build/src/modeling/ApiModel.d.ts +21 -7
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +70 -29
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/DomainValidation.d.ts +1 -1
- package/build/src/modeling/DomainValidation.d.ts.map +1 -1
- package/build/src/modeling/DomainValidation.js.map +1 -1
- package/build/src/modeling/ExposedEntity.d.ts +14 -0
- package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
- package/build/src/modeling/ExposedEntity.js +59 -6
- package/build/src/modeling/ExposedEntity.js.map +1 -1
- package/build/src/modeling/actions/Action.d.ts +11 -1
- package/build/src/modeling/actions/Action.d.ts.map +1 -1
- package/build/src/modeling/actions/Action.js +21 -3
- package/build/src/modeling/actions/Action.js.map +1 -1
- package/build/src/modeling/actions/CreateAction.d.ts +2 -1
- package/build/src/modeling/actions/CreateAction.d.ts.map +1 -1
- package/build/src/modeling/actions/CreateAction.js +2 -2
- package/build/src/modeling/actions/CreateAction.js.map +1 -1
- package/build/src/modeling/actions/DeleteAction.d.ts +2 -1
- package/build/src/modeling/actions/DeleteAction.d.ts.map +1 -1
- package/build/src/modeling/actions/DeleteAction.js +2 -2
- package/build/src/modeling/actions/DeleteAction.js.map +1 -1
- package/build/src/modeling/actions/ListAction.d.ts +2 -1
- package/build/src/modeling/actions/ListAction.d.ts.map +1 -1
- package/build/src/modeling/actions/ListAction.js +2 -2
- package/build/src/modeling/actions/ListAction.js.map +1 -1
- package/build/src/modeling/actions/ReadAction.d.ts +2 -1
- package/build/src/modeling/actions/ReadAction.d.ts.map +1 -1
- package/build/src/modeling/actions/ReadAction.js +2 -2
- package/build/src/modeling/actions/ReadAction.js.map +1 -1
- package/build/src/modeling/actions/SearchAction.d.ts +2 -1
- package/build/src/modeling/actions/SearchAction.d.ts.map +1 -1
- package/build/src/modeling/actions/SearchAction.js +2 -2
- package/build/src/modeling/actions/SearchAction.js.map +1 -1
- package/build/src/modeling/actions/UpdateAction.d.ts +2 -1
- package/build/src/modeling/actions/UpdateAction.d.ts.map +1 -1
- package/build/src/modeling/actions/UpdateAction.js +2 -2
- package/build/src/modeling/actions/UpdateAction.js.map +1 -1
- package/build/src/modeling/actions/index.d.ts +2 -1
- package/build/src/modeling/actions/index.d.ts.map +1 -1
- package/build/src/modeling/actions/index.js +7 -7
- package/build/src/modeling/actions/index.js.map +1 -1
- package/build/src/modeling/index.d.ts +1 -0
- package/build/src/modeling/index.d.ts.map +1 -1
- package/build/src/modeling/index.js +1 -0
- package/build/src/modeling/index.js.map +1 -1
- package/build/src/modeling/types.d.ts +67 -0
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/src/modeling/validation/api_model_rules.d.ts +15 -0
- package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -0
- package/build/src/modeling/validation/api_model_rules.js +599 -0
- package/build/src/modeling/validation/api_model_rules.js.map +1 -0
- package/build/src/modeling/validation/association_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/association_validation.js +1 -3
- package/build/src/modeling/validation/association_validation.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +8 -8
- package/eslint.config.js +0 -1
- package/package.json +17 -122
- package/src/decorators/observed.ts +91 -0
- package/src/modeling/ApiModel.ts +73 -33
- package/src/modeling/DomainValidation.ts +1 -1
- package/src/modeling/ExposedEntity.ts +63 -9
- package/src/modeling/actions/Action.ts +25 -2
- package/src/modeling/actions/CreateAction.ts +3 -2
- package/src/modeling/actions/DeleteAction.ts +3 -2
- package/src/modeling/actions/ListAction.ts +3 -2
- package/src/modeling/actions/ReadAction.ts +3 -2
- package/src/modeling/actions/SearchAction.ts +3 -2
- package/src/modeling/actions/UpdateAction.ts +3 -2
- package/src/modeling/types.ts +70 -0
- package/src/modeling/validation/api_model_rules.ts +640 -0
- package/src/modeling/validation/api_model_validation_rules.md +58 -0
- package/src/modeling/validation/association_validation.ts +1 -3
- package/tests/unit/modeling/actions/Action.spec.ts +40 -8
- package/tests/unit/modeling/actions/CreateAction.spec.ts +5 -5
- package/tests/unit/modeling/actions/DeleteAction.spec.ts +6 -6
- package/tests/unit/modeling/actions/ListAction.spec.ts +7 -7
- package/tests/unit/modeling/actions/ReadAction.spec.ts +6 -6
- package/tests/unit/modeling/actions/SearchAction.spec.ts +6 -6
- package/tests/unit/modeling/actions/UpdateAction.spec.ts +6 -6
- package/tests/unit/modeling/api_model.spec.ts +190 -13
- package/tests/unit/modeling/api_model_expose_entity.spec.ts +43 -19
- package/tests/unit/modeling/api_model_remove_entity.spec.ts +6 -6
- package/tests/unit/modeling/exposed_entity.spec.ts +123 -3
- package/tests/unit/modeling/exposed_entity_actions.spec.ts +41 -18
- package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +1 -1
- package/tests/unit/modeling/rules/restoring_rules.spec.ts +9 -5
- package/tests/unit/modeling/validation/api_model_rules.spec.ts +324 -0
- package/tsconfig.browser.json +1 -1
- package/tsconfig.node.json +1 -1
- 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 =
|
|
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
|
|
51
|
+
actions: [action],
|
|
41
52
|
},
|
|
42
53
|
],
|
|
43
54
|
},
|
|
44
55
|
domain
|
|
45
56
|
)
|
|
46
57
|
|
|
47
|
-
const restoredAction = model.exposes[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 =
|
|
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
|
|
79
|
+
actions: [action],
|
|
67
80
|
},
|
|
68
81
|
],
|
|
69
82
|
},
|
|
70
83
|
domain
|
|
71
84
|
)
|
|
72
85
|
|
|
73
|
-
const restoredAction = model.exposes[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 =
|
|
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
|
|
107
|
+
actions: [action],
|
|
93
108
|
},
|
|
94
109
|
],
|
|
95
110
|
},
|
|
96
111
|
domain
|
|
97
112
|
)
|
|
98
113
|
|
|
99
|
-
const restoredAction = model.exposes[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 =
|
|
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
|
|
136
|
+
actions: [action],
|
|
119
137
|
},
|
|
120
138
|
],
|
|
121
139
|
},
|
|
122
140
|
domain
|
|
123
141
|
)
|
|
124
142
|
|
|
125
|
-
const restoredAction = model.exposes[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 =
|
|
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
|
|
164
|
+
actions: [action],
|
|
145
165
|
},
|
|
146
166
|
],
|
|
147
167
|
},
|
|
148
168
|
domain
|
|
149
169
|
)
|
|
150
170
|
|
|
151
|
-
const restoredAction = model.exposes[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 =
|
|
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
|
|
193
|
+
actions: [action],
|
|
171
194
|
},
|
|
172
195
|
],
|
|
173
196
|
},
|
|
174
197
|
domain
|
|
175
198
|
)
|
|
176
199
|
|
|
177
|
-
const restoredAction = model.exposes[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,
|
|
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 =
|
|
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
|
-
}
|
|
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
|
+
})
|
package/tsconfig.browser.json
CHANGED
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"incremental": true
|
|
31
31
|
},
|
|
32
32
|
"include": ["src/**/*", "test/**/*"],
|
|
33
|
-
"exclude": ["node_modules", "build", "coverage", ".tmp", "
|
|
33
|
+
"exclude": ["node_modules", "build", "coverage", ".tmp", "amf-models", "bin", "data", "docs", "scripts"],
|
|
34
34
|
}
|
package/tsconfig.node.json
CHANGED
|
@@ -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", "
|
|
35
|
+
"exclude": ["node_modules", "build", "coverage", ".tmp", "amf-models", "data", "docs", "scripts"],
|
|
36
36
|
}
|