@api-client/core 0.18.31 → 0.18.33
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/build/src/browser.d.ts +1 -0
- package/build/src/browser.d.ts.map +1 -1
- package/build/src/browser.js +1 -0
- package/build/src/browser.js.map +1 -1
- package/build/src/index.d.ts +1 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +1 -0
- package/build/src/index.js.map +1 -1
- package/build/src/modeling/ApiModel.d.ts +4 -3
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +25 -22
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/ExposedEntity.d.ts +124 -0
- package/build/src/modeling/ExposedEntity.d.ts.map +1 -0
- package/build/src/modeling/ExposedEntity.js +364 -0
- package/build/src/modeling/ExposedEntity.js.map +1 -0
- package/build/src/modeling/helpers/endpointHelpers.d.ts +11 -0
- package/build/src/modeling/helpers/endpointHelpers.d.ts.map +1 -1
- package/build/src/modeling/helpers/endpointHelpers.js +21 -0
- package/build/src/modeling/helpers/endpointHelpers.js.map +1 -1
- package/build/src/modeling/types.d.ts +12 -15
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/src/models/kinds.d.ts +1 -0
- package/build/src/models/kinds.d.ts.map +1 -1
- package/build/src/models/kinds.js +1 -0
- package/build/src/models/kinds.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +6 -6
- package/package.json +1 -1
- package/src/modeling/ApiModel.ts +22 -26
- package/src/modeling/ExposedEntity.ts +358 -0
- package/src/modeling/helpers/endpointHelpers.ts +22 -0
- package/src/modeling/types.ts +12 -16
- package/src/models/kinds.ts +1 -0
- package/tests/unit/modeling/api_model.spec.ts +49 -10
- package/tests/unit/modeling/api_model_expose_entity.spec.ts +2 -4
- package/tests/unit/modeling/api_model_remove_entity.spec.ts +1 -2
- package/tests/unit/modeling/exposed_entity.spec.ts +155 -0
|
@@ -5,9 +5,11 @@ import {
|
|
|
5
5
|
DataDomain,
|
|
6
6
|
type RolesBasedAccessControl,
|
|
7
7
|
type ApiModelSchema,
|
|
8
|
-
type
|
|
8
|
+
type ExposedEntitySchema,
|
|
9
9
|
type ApiContact,
|
|
10
10
|
type ApiLicense,
|
|
11
|
+
ExposedEntityKind,
|
|
12
|
+
ExposedEntity,
|
|
11
13
|
} from '../../../src/index.js'
|
|
12
14
|
|
|
13
15
|
test.group('ApiModel.createSchema()', () => {
|
|
@@ -34,7 +36,16 @@ test.group('ApiModel.createSchema()', () => {
|
|
|
34
36
|
const input: Partial<ApiModelSchema> = {
|
|
35
37
|
key: 'test-api',
|
|
36
38
|
info: { name: 'Test API', description: 'A test API' },
|
|
37
|
-
exposes: [
|
|
39
|
+
exposes: [
|
|
40
|
+
{
|
|
41
|
+
key: 'entity1',
|
|
42
|
+
actions: [],
|
|
43
|
+
hasCollection: true,
|
|
44
|
+
kind: ExposedEntityKind,
|
|
45
|
+
resourcePath: '/',
|
|
46
|
+
entity: { key: 'entity1' },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
38
49
|
user: { key: 'user-entity' },
|
|
39
50
|
dependencyList: [{ key: 'domain1', version: '1.0.0' }],
|
|
40
51
|
authentication: { strategy: 'UsernamePassword' },
|
|
@@ -51,7 +62,8 @@ test.group('ApiModel.createSchema()', () => {
|
|
|
51
62
|
assert.equal(schema.kind, ApiModelKind)
|
|
52
63
|
assert.equal(schema.key, 'test-api')
|
|
53
64
|
assert.deepInclude(schema.info, { name: 'Test API', description: 'A test API' })
|
|
54
|
-
assert.
|
|
65
|
+
assert.lengthOf(schema.exposes, 1, 'should have one exposed entity')
|
|
66
|
+
assert.equal(schema.exposes[0].key, 'entity1', 'exposed entity should have correct key')
|
|
55
67
|
assert.deepEqual(schema.user, { key: 'user-entity' })
|
|
56
68
|
assert.deepEqual(schema.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
|
|
57
69
|
assert.deepEqual(schema.authentication, { strategy: 'UsernamePassword' })
|
|
@@ -100,7 +112,16 @@ test.group('ApiModel.constructor()', () => {
|
|
|
100
112
|
kind: ApiModelKind,
|
|
101
113
|
key: 'test-api',
|
|
102
114
|
info: { name: 'Test API', description: 'A test API' },
|
|
103
|
-
exposes: [
|
|
115
|
+
exposes: [
|
|
116
|
+
{
|
|
117
|
+
key: 'entity1',
|
|
118
|
+
actions: [],
|
|
119
|
+
resourcePath: '/',
|
|
120
|
+
entity: { key: 'entity1' },
|
|
121
|
+
hasCollection: true,
|
|
122
|
+
kind: ExposedEntityKind,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
104
125
|
user: { key: 'user-entity' },
|
|
105
126
|
dependencyList: [{ key: 'domain1', version: '1.0.0' }],
|
|
106
127
|
authentication: { strategy: 'UsernamePassword' },
|
|
@@ -116,7 +137,8 @@ test.group('ApiModel.constructor()', () => {
|
|
|
116
137
|
|
|
117
138
|
assert.equal(model.key, 'test-api')
|
|
118
139
|
assert.equal(model.info.name, 'Test API')
|
|
119
|
-
assert.
|
|
140
|
+
assert.lengthOf(model.exposes, 1, 'should have one exposed entity')
|
|
141
|
+
assert.equal(model.exposes[0].key, 'entity1', 'exposed entity should have correct key')
|
|
120
142
|
assert.deepEqual(model.user, { key: 'user-entity' })
|
|
121
143
|
assert.deepEqual(model.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
|
|
122
144
|
assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
|
|
@@ -179,7 +201,16 @@ test.group('ApiModel.toJSON()', () => {
|
|
|
179
201
|
kind: ApiModelKind,
|
|
180
202
|
key: 'test-api',
|
|
181
203
|
info: { name: 'Test API', description: 'A test API' },
|
|
182
|
-
exposes: [
|
|
204
|
+
exposes: [
|
|
205
|
+
{
|
|
206
|
+
key: 'entity1',
|
|
207
|
+
actions: [],
|
|
208
|
+
resourcePath: '/',
|
|
209
|
+
entity: { key: 'entity1' },
|
|
210
|
+
hasCollection: true,
|
|
211
|
+
kind: ExposedEntityKind,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
183
214
|
user: { key: 'user-entity' },
|
|
184
215
|
dependencyList: [{ key: 'domain1', version: '1.0.0' }],
|
|
185
216
|
authentication: { strategy: 'UsernamePassword' },
|
|
@@ -196,7 +227,8 @@ test.group('ApiModel.toJSON()', () => {
|
|
|
196
227
|
|
|
197
228
|
assert.equal(json.key, 'test-api')
|
|
198
229
|
assert.deepInclude(json.info, { name: 'Test API', description: 'A test API' })
|
|
199
|
-
assert.
|
|
230
|
+
assert.lengthOf(json.exposes, 1, 'should have one exposed entity')
|
|
231
|
+
assert.equal(json.exposes[0].key, 'entity1', 'exposed entity should have correct key')
|
|
200
232
|
assert.deepEqual(json.user, { key: 'user-entity' })
|
|
201
233
|
assert.deepEqual(json.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
|
|
202
234
|
assert.deepEqual(json.authentication, { strategy: 'UsernamePassword' })
|
|
@@ -214,11 +246,18 @@ test.group('ApiModel.getExposedEntity()', () => {
|
|
|
214
246
|
test('returns an existing exposed entity', ({ assert }) => {
|
|
215
247
|
const model = new ApiModel()
|
|
216
248
|
const entityKey = 'get-entity'
|
|
217
|
-
const exposed:
|
|
218
|
-
|
|
249
|
+
const exposed: ExposedEntitySchema = {
|
|
250
|
+
key: entityKey,
|
|
251
|
+
actions: [],
|
|
252
|
+
hasCollection: true,
|
|
253
|
+
kind: ExposedEntityKind,
|
|
254
|
+
resourcePath: '/',
|
|
255
|
+
entity: { key: entityKey },
|
|
256
|
+
}
|
|
257
|
+
model.exposes.push(new ExposedEntity(model, exposed))
|
|
219
258
|
|
|
220
259
|
const retrievedEntity = model.getExposedEntity({ key: entityKey })
|
|
221
|
-
assert.deepEqual(retrievedEntity, exposed)
|
|
260
|
+
assert.deepEqual(retrievedEntity?.toJSON(), exposed)
|
|
222
261
|
}).tags(['@modeling', '@api'])
|
|
223
262
|
|
|
224
263
|
test('returns undefined if entity is not exposed', ({ assert }) => {
|
|
@@ -15,7 +15,6 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
15
15
|
assert.typeOf(exposedEntity.key, 'string')
|
|
16
16
|
assert.deepEqual(exposedEntity.entity, { key: e1.key })
|
|
17
17
|
assert.deepEqual(exposedEntity.actions, [])
|
|
18
|
-
assert.includeDeepMembers(model.exposes, [exposedEntity])
|
|
19
18
|
}).tags(['@modeling', '@api'])
|
|
20
19
|
|
|
21
20
|
test('returns an existing entity if already exposed', ({ assert }) => {
|
|
@@ -28,7 +27,7 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
28
27
|
const initialExposedEntity = model.exposeEntity({ key: e1.key })
|
|
29
28
|
const retrievedExposedEntity = model.exposeEntity({ key: e1.key })
|
|
30
29
|
|
|
31
|
-
assert.
|
|
30
|
+
assert.deepEqual(retrievedExposedEntity.toJSON(), initialExposedEntity.toJSON())
|
|
32
31
|
assert.lengthOf(model.exposes, 1)
|
|
33
32
|
}).tags(['@modeling', '@api'])
|
|
34
33
|
|
|
@@ -81,8 +80,7 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
81
80
|
const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
|
|
82
81
|
assert.isDefined(nestedB)
|
|
83
82
|
assert.deepEqual(nestedB?.parent?.key, exposedA.key)
|
|
84
|
-
assert.strictEqual(nestedB?.
|
|
85
|
-
assert.strictEqual(nestedB?.absoluteCollectionPath, '/as/{id}/entitybs')
|
|
83
|
+
assert.strictEqual(nestedB?.collectionPath, '/entitybs')
|
|
86
84
|
})
|
|
87
85
|
|
|
88
86
|
test('does not infinitely expose circular associations', ({ assert }) => {
|
|
@@ -45,10 +45,9 @@ test.group('ApiModel.removeEntity()', () => {
|
|
|
45
45
|
const model = new ApiModel()
|
|
46
46
|
model.attachDataDomain(domain)
|
|
47
47
|
model.exposeEntity({ key: e1.key })
|
|
48
|
-
const initialExposes = [...model.exposes]
|
|
49
48
|
|
|
50
49
|
model.removeEntity({ key: 'non-existing-entity' })
|
|
51
|
-
assert.
|
|
50
|
+
assert.lengthOf(model.exposes, 1, 'exposes count should remain unchanged')
|
|
52
51
|
}).tags(['@modeling', '@api'])
|
|
53
52
|
|
|
54
53
|
test('notifies change when an entity is removed', async ({ assert }) => {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { ApiModel, ExposedEntity, type ExposedEntitySchema } from '../../../src/index.js'
|
|
3
|
+
import { ExposedEntityKind } from '../../../src/models/kinds.js'
|
|
4
|
+
|
|
5
|
+
test.group('ExposedEntity', () => {
|
|
6
|
+
test('setCollectionPath normalizes and preserves resource param', ({ assert }) => {
|
|
7
|
+
const model = new ApiModel()
|
|
8
|
+
const ex = new ExposedEntity(model, {
|
|
9
|
+
hasCollection: true,
|
|
10
|
+
collectionPath: '/items',
|
|
11
|
+
resourcePath: '/items/{customId}',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
ex.setCollectionPath('products')
|
|
15
|
+
|
|
16
|
+
assert.equal(ex.collectionPath, '/products')
|
|
17
|
+
assert.equal(ex.resourcePath, '/products/{customId}')
|
|
18
|
+
}).tags(['@modeling', '@exposed-entity'])
|
|
19
|
+
|
|
20
|
+
test('setResourcePath with collection allows only parameter name change', ({ assert }) => {
|
|
21
|
+
const model = new ApiModel()
|
|
22
|
+
const ex = new ExposedEntity(model, {
|
|
23
|
+
hasCollection: true,
|
|
24
|
+
collectionPath: '/products',
|
|
25
|
+
resourcePath: '/products/{id}',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// valid: same collection segment, different param name
|
|
29
|
+
ex.setResourcePath('/products/{productId}')
|
|
30
|
+
assert.equal(ex.resourcePath, '/products/{productId}')
|
|
31
|
+
|
|
32
|
+
// invalid: different first segment
|
|
33
|
+
assert.throws(() => ex.setResourcePath('/wrong/{id}'))
|
|
34
|
+
|
|
35
|
+
// invalid: second segment not a parameter
|
|
36
|
+
assert.throws(() => ex.setResourcePath('/products/notParam'))
|
|
37
|
+
}).tags(['@modeling', '@exposed-entity'])
|
|
38
|
+
|
|
39
|
+
test('setResourcePath without collection must have exactly two segments', ({ assert }) => {
|
|
40
|
+
const model = new ApiModel()
|
|
41
|
+
const ex = new ExposedEntity(model, {
|
|
42
|
+
hasCollection: false,
|
|
43
|
+
resourcePath: '/profile/{id}',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
ex.setResourcePath('settings/secret')
|
|
47
|
+
assert.equal(ex.resourcePath, '/settings/secret')
|
|
48
|
+
|
|
49
|
+
assert.throws(() => ex.setResourcePath('onlyone'))
|
|
50
|
+
}).tags(['@modeling', '@exposed-entity'])
|
|
51
|
+
|
|
52
|
+
test('computes absolute resource and collection paths along parent chain', ({ assert }) => {
|
|
53
|
+
const model = new ApiModel()
|
|
54
|
+
|
|
55
|
+
// Build exposure schemas
|
|
56
|
+
const rootSchema: Partial<ExposedEntitySchema> = {
|
|
57
|
+
key: 'root',
|
|
58
|
+
entity: { key: 'user' },
|
|
59
|
+
hasCollection: true,
|
|
60
|
+
collectionPath: '/users',
|
|
61
|
+
resourcePath: '/users/{userId}',
|
|
62
|
+
isRoot: true,
|
|
63
|
+
actions: [],
|
|
64
|
+
}
|
|
65
|
+
const childSchema: Partial<ExposedEntitySchema> = {
|
|
66
|
+
key: 'child',
|
|
67
|
+
entity: { key: 'post' },
|
|
68
|
+
hasCollection: true,
|
|
69
|
+
collectionPath: '/posts',
|
|
70
|
+
resourcePath: '/posts/{postId}',
|
|
71
|
+
parent: { key: 'root', association: { key: 'toPosts' } },
|
|
72
|
+
actions: [],
|
|
73
|
+
}
|
|
74
|
+
const grandSchema: Partial<ExposedEntitySchema> = {
|
|
75
|
+
key: 'grand',
|
|
76
|
+
entity: { key: 'details' },
|
|
77
|
+
hasCollection: false,
|
|
78
|
+
resourcePath: '/details',
|
|
79
|
+
parent: { key: 'child', association: { key: 'toDetails' } },
|
|
80
|
+
actions: [],
|
|
81
|
+
}
|
|
82
|
+
// Instantiate instances bound to the model
|
|
83
|
+
const rootEx = new ExposedEntity(model, rootSchema)
|
|
84
|
+
const childEx = new ExposedEntity(model, childSchema)
|
|
85
|
+
const grandEx = new ExposedEntity(model, grandSchema)
|
|
86
|
+
// attach to model as instances
|
|
87
|
+
model.exposes = [rootEx, childEx, grandEx]
|
|
88
|
+
|
|
89
|
+
// root
|
|
90
|
+
assert.equal(rootEx.getAbsoluteCollectionPath(), '/users')
|
|
91
|
+
assert.equal(rootEx.getAbsoluteResourcePath(), '/users/{userId}')
|
|
92
|
+
|
|
93
|
+
// child
|
|
94
|
+
assert.equal(childEx.getAbsoluteCollectionPath(), '/users/{userId}/posts')
|
|
95
|
+
assert.equal(childEx.getAbsoluteResourcePath(), '/users/{userId}/posts/{postId}')
|
|
96
|
+
|
|
97
|
+
// grand (no collection)
|
|
98
|
+
assert.isUndefined(grandEx.getAbsoluteCollectionPath())
|
|
99
|
+
assert.equal(grandEx.getAbsoluteResourcePath(), '/users/{userId}/posts/{postId}/details')
|
|
100
|
+
}).tags(['@modeling', '@exposed-entity'])
|
|
101
|
+
|
|
102
|
+
test('ApiModel notifies when nested ExposedEntity collection path changes', async ({ assert }) => {
|
|
103
|
+
const model = new ApiModel({
|
|
104
|
+
exposes: [
|
|
105
|
+
{
|
|
106
|
+
kind: ExposedEntityKind,
|
|
107
|
+
key: 'e1',
|
|
108
|
+
entity: { key: 'e1' },
|
|
109
|
+
hasCollection: true,
|
|
110
|
+
collectionPath: '/things',
|
|
111
|
+
resourcePath: '/things/{id}',
|
|
112
|
+
isRoot: true,
|
|
113
|
+
actions: [],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
let notified = 0
|
|
119
|
+
model.addEventListener('change', () => {
|
|
120
|
+
notified += 1
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const ex = model.exposes[0]
|
|
124
|
+
ex.setCollectionPath('items')
|
|
125
|
+
await Promise.resolve() // allow ApiModel.notifyChange microtask to run
|
|
126
|
+
assert.isAtLeast(notified, 1)
|
|
127
|
+
}).tags(['@modeling', '@exposed-entity', '@observed'])
|
|
128
|
+
|
|
129
|
+
test('ApiModel notifies when nested ExposedEntity resource path changes', async ({ assert }) => {
|
|
130
|
+
const model = new ApiModel({
|
|
131
|
+
exposes: [
|
|
132
|
+
{
|
|
133
|
+
kind: ExposedEntityKind,
|
|
134
|
+
key: 'e1',
|
|
135
|
+
entity: { key: 'e1' },
|
|
136
|
+
hasCollection: true,
|
|
137
|
+
collectionPath: '/products',
|
|
138
|
+
resourcePath: '/products/{id}',
|
|
139
|
+
isRoot: true,
|
|
140
|
+
actions: [],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
let notified = 0
|
|
146
|
+
model.addEventListener('change', () => {
|
|
147
|
+
notified += 1
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const ex = model.exposes[0]
|
|
151
|
+
ex.setResourcePath('/products/{productId}')
|
|
152
|
+
await Promise.resolve()
|
|
153
|
+
assert.isAtLeast(notified, 1)
|
|
154
|
+
}).tags(['@modeling', '@exposed-entity', '@observed'])
|
|
155
|
+
})
|