@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.
Files changed (39) hide show
  1. package/build/src/browser.d.ts +1 -0
  2. package/build/src/browser.d.ts.map +1 -1
  3. package/build/src/browser.js +1 -0
  4. package/build/src/browser.js.map +1 -1
  5. package/build/src/index.d.ts +1 -0
  6. package/build/src/index.d.ts.map +1 -1
  7. package/build/src/index.js +1 -0
  8. package/build/src/index.js.map +1 -1
  9. package/build/src/modeling/ApiModel.d.ts +4 -3
  10. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  11. package/build/src/modeling/ApiModel.js +25 -22
  12. package/build/src/modeling/ApiModel.js.map +1 -1
  13. package/build/src/modeling/ExposedEntity.d.ts +124 -0
  14. package/build/src/modeling/ExposedEntity.d.ts.map +1 -0
  15. package/build/src/modeling/ExposedEntity.js +364 -0
  16. package/build/src/modeling/ExposedEntity.js.map +1 -0
  17. package/build/src/modeling/helpers/endpointHelpers.d.ts +11 -0
  18. package/build/src/modeling/helpers/endpointHelpers.d.ts.map +1 -1
  19. package/build/src/modeling/helpers/endpointHelpers.js +21 -0
  20. package/build/src/modeling/helpers/endpointHelpers.js.map +1 -1
  21. package/build/src/modeling/types.d.ts +12 -15
  22. package/build/src/modeling/types.d.ts.map +1 -1
  23. package/build/src/modeling/types.js.map +1 -1
  24. package/build/src/models/kinds.d.ts +1 -0
  25. package/build/src/models/kinds.d.ts.map +1 -1
  26. package/build/src/models/kinds.js +1 -0
  27. package/build/src/models/kinds.js.map +1 -1
  28. package/build/tsconfig.tsbuildinfo +1 -1
  29. package/data/models/example-generator-api.json +6 -6
  30. package/package.json +1 -1
  31. package/src/modeling/ApiModel.ts +22 -26
  32. package/src/modeling/ExposedEntity.ts +358 -0
  33. package/src/modeling/helpers/endpointHelpers.ts +22 -0
  34. package/src/modeling/types.ts +12 -16
  35. package/src/models/kinds.ts +1 -0
  36. package/tests/unit/modeling/api_model.spec.ts +49 -10
  37. package/tests/unit/modeling/api_model_expose_entity.spec.ts +2 -4
  38. package/tests/unit/modeling/api_model_remove_entity.spec.ts +1 -2
  39. 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 ExposedEntity,
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: [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }],
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.deepEqual(schema.exposes, [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }])
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: [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }],
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.deepEqual(model.exposes, [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }])
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: [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }],
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.deepEqual(json.exposes, [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }])
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: ExposedEntity = { key: entityKey, actions: [], path: '', entity: { key: entityKey } }
218
- model.exposes.push(exposed)
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.strictEqual(retrievedExposedEntity, initialExposedEntity)
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?.relativeCollectionPath, '/entitybs')
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.deepEqual(model.exposes, initialExposes)
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
+ })