@api-client/core 0.18.27 → 0.18.28

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.
@@ -10,10 +10,7 @@ import {
10
10
  type ApiLicense,
11
11
  } from '../../../src/index.js'
12
12
 
13
- test.group('ApiModel.createSchema()', (g) => {
14
- g.tests.forEach((test) => {
15
- test.tags(['@modeling', '@api', '@schema'])
16
- })
13
+ test.group('ApiModel.createSchema()', () => {
17
14
  test('creates a schema with default values', ({ assert }) => {
18
15
  const schema = ApiModel.createSchema()
19
16
  assert.equal(schema.kind, ApiModelKind)
@@ -31,13 +28,13 @@ test.group('ApiModel.createSchema()', (g) => {
31
28
  assert.isUndefined(schema.termsOfService)
32
29
  assert.isUndefined(schema.contact)
33
30
  assert.isUndefined(schema.license)
34
- })
31
+ }).tags(['@modeling', '@api', '@schema'])
35
32
 
36
33
  test('creates a schema with provided values', ({ assert }) => {
37
34
  const input: Partial<ApiModelSchema> = {
38
35
  key: 'test-api',
39
36
  info: { name: 'Test API', description: 'A test API' },
40
- exposes: [{ key: 'entity1', actions: [] }],
37
+ exposes: [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }],
41
38
  user: { key: 'user-entity' },
42
39
  dependencyList: [{ key: 'domain1', version: '1.0.0' }],
43
40
  authentication: { strategy: 'UsernamePassword' },
@@ -54,7 +51,7 @@ test.group('ApiModel.createSchema()', (g) => {
54
51
  assert.equal(schema.kind, ApiModelKind)
55
52
  assert.equal(schema.key, 'test-api')
56
53
  assert.deepInclude(schema.info, { name: 'Test API', description: 'A test API' })
57
- assert.deepEqual(schema.exposes, [{ key: 'entity1', actions: [] }])
54
+ assert.deepEqual(schema.exposes, [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }])
58
55
  assert.deepEqual(schema.user, { key: 'user-entity' })
59
56
  assert.deepEqual(schema.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
60
57
  assert.deepEqual(schema.authentication, { strategy: 'UsernamePassword' })
@@ -65,24 +62,20 @@ test.group('ApiModel.createSchema()', (g) => {
65
62
  assert.equal(schema.termsOfService, 'https://example.com/terms')
66
63
  assert.deepEqual(schema.contact, { name: 'John Doe', email: 'john.doe@example.com' })
67
64
  assert.deepEqual(schema.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
68
- })
65
+ }).tags(['@modeling', '@api', '@schema'])
69
66
 
70
67
  test('creates a schema with partial info', ({ assert }) => {
71
68
  const schema = ApiModel.createSchema({ info: { name: 'Partial API' } })
72
69
  assert.deepInclude(schema.info, { name: 'Partial API' })
73
- })
70
+ }).tags(['@modeling', '@api', '@schema'])
74
71
 
75
72
  test('creates a schema with empty info', ({ assert }) => {
76
73
  const schema = ApiModel.createSchema({ info: {} })
77
74
  assert.deepInclude(schema.info, { name: 'Unnamed API' })
78
- })
75
+ }).tags(['@modeling', '@api', '@schema'])
79
76
  })
80
77
 
81
- test.group('ApiModel.constructor()', (g) => {
82
- g.tests.forEach((test) => {
83
- test.tags(['@modeling', '@api', '@creation'])
84
- })
85
-
78
+ test.group('ApiModel.constructor()', () => {
86
79
  test('creates an instance with default values', ({ assert }) => {
87
80
  const model = new ApiModel()
88
81
  assert.equal(model.kind, ApiModelKind)
@@ -100,14 +93,14 @@ test.group('ApiModel.constructor()', (g) => {
100
93
  assert.isUndefined(model.contact)
101
94
  assert.isUndefined(model.license)
102
95
  assert.deepEqual(model.dependencyList, [])
103
- })
96
+ }).tags(['@modeling', '@api', '@creation'])
104
97
 
105
98
  test('creates an instance with provided schema values', ({ assert }) => {
106
99
  const schema: ApiModelSchema = {
107
100
  kind: ApiModelKind,
108
101
  key: 'test-api',
109
102
  info: { name: 'Test API', description: 'A test API' },
110
- exposes: [{ key: 'entity1', actions: [] }],
103
+ exposes: [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }],
111
104
  user: { key: 'user-entity' },
112
105
  dependencyList: [{ key: 'domain1', version: '1.0.0' }],
113
106
  authentication: { strategy: 'UsernamePassword' },
@@ -123,7 +116,7 @@ test.group('ApiModel.constructor()', (g) => {
123
116
 
124
117
  assert.equal(model.key, 'test-api')
125
118
  assert.equal(model.info.name, 'Test API')
126
- assert.deepEqual(model.exposes, [{ key: 'entity1', actions: [] }])
119
+ assert.deepEqual(model.exposes, [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }])
127
120
  assert.deepEqual(model.user, { key: 'user-entity' })
128
121
  assert.deepEqual(model.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
129
122
  assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
@@ -134,7 +127,7 @@ test.group('ApiModel.constructor()', (g) => {
134
127
  assert.equal(model.termsOfService, 'https://example.com/terms')
135
128
  assert.deepEqual(model.contact, { name: 'John Doe', email: 'john.doe@example.com' })
136
129
  assert.deepEqual(model.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
137
- })
130
+ }).tags(['@modeling', '@api', '@creation'])
138
131
 
139
132
  test('creates an instance with a DataDomain', ({ assert }) => {
140
133
  const domainSchema = DataDomain.createSchema({ key: 'my-domain' })
@@ -146,7 +139,7 @@ test.group('ApiModel.constructor()', (g) => {
146
139
  assert.isDefined(model.domain)
147
140
  assert.instanceOf(model.domain, DataDomain)
148
141
  assert.equal(model.domain!.key, 'my-domain')
149
- })
142
+ }).tags(['@modeling', '@api', '@creation'])
150
143
 
151
144
  test('notifies change when info is modified', async ({ assert }) => {
152
145
  const model = new ApiModel()
@@ -157,13 +150,10 @@ test.group('ApiModel.constructor()', (g) => {
157
150
  model.info.name = 'New Name'
158
151
  await Promise.resolve() // Allow microtask to run
159
152
  assert.isTrue(notified)
160
- })
153
+ }).tags(['@modeling', '@api', '@creation'])
161
154
  })
162
155
 
163
- test.group('ApiModel.toJSON()', (g) => {
164
- g.tests.forEach((test) => {
165
- test.tags(['@modeling', '@api', '@serialization'])
166
- })
156
+ test.group('ApiModel.toJSON()', () => {
167
157
  test('serializes default values', ({ assert }) => {
168
158
  const model = new ApiModel()
169
159
  const json = model.toJSON()
@@ -182,14 +172,14 @@ test.group('ApiModel.toJSON()', (g) => {
182
172
  assert.isUndefined(json.termsOfService)
183
173
  assert.isUndefined(json.contact)
184
174
  assert.isUndefined(json.license)
185
- })
175
+ }).tags(['@modeling', '@api', '@serialization'])
186
176
 
187
177
  test('serializes all provided values', ({ assert }) => {
188
178
  const schema: ApiModelSchema = {
189
179
  kind: ApiModelKind,
190
180
  key: 'test-api',
191
181
  info: { name: 'Test API', description: 'A test API' },
192
- exposes: [{ key: 'entity1', actions: [] }],
182
+ exposes: [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }],
193
183
  user: { key: 'user-entity' },
194
184
  dependencyList: [{ key: 'domain1', version: '1.0.0' }],
195
185
  authentication: { strategy: 'UsernamePassword' },
@@ -206,7 +196,7 @@ test.group('ApiModel.toJSON()', (g) => {
206
196
 
207
197
  assert.equal(json.key, 'test-api')
208
198
  assert.deepInclude(json.info, { name: 'Test API', description: 'A test API' })
209
- assert.deepEqual(json.exposes, [{ key: 'entity1', actions: [] }])
199
+ assert.deepEqual(json.exposes, [{ key: 'entity1', actions: [], path: '', entity: { key: 'entity1' } }])
210
200
  assert.deepEqual(json.user, { key: 'user-entity' })
211
201
  assert.deepEqual(json.dependencyList, [{ key: 'domain1', version: '1.0.0' }])
212
202
  assert.deepEqual(json.authentication, { strategy: 'UsernamePassword' })
@@ -217,125 +207,23 @@ test.group('ApiModel.toJSON()', (g) => {
217
207
  assert.equal(json.termsOfService, 'https://example.com/terms')
218
208
  assert.deepEqual(json.contact, { name: 'John Doe', email: 'john.doe@example.com' })
219
209
  assert.deepEqual(json.license, { name: 'MIT', url: 'https://opensource.org/licenses/MIT' })
220
- })
221
- })
222
-
223
- test.group('ApiModel.exposeEntity()', (g) => {
224
- g.tests.forEach((test) => {
225
- test.tags(['@modeling', '@api'])
226
- })
227
- test('exposes a new entity', ({ assert }) => {
228
- const model = new ApiModel()
229
- const entityKey = 'new-entity'
230
- const exposedEntity = model.exposeEntity(entityKey)
231
-
232
- assert.isDefined(exposedEntity)
233
- assert.equal(exposedEntity.key, entityKey)
234
- assert.deepEqual(exposedEntity.actions, [])
235
- assert.includeDeepMembers(model.exposes, [exposedEntity])
236
- })
237
-
238
- test('returns an existing entity if already exposed', ({ assert }) => {
239
- const model = new ApiModel()
240
- const entityKey = 'existing-entity'
241
- const initialExposedEntity = model.exposeEntity(entityKey)
242
- const retrievedExposedEntity = model.exposeEntity(entityKey)
243
-
244
- assert.strictEqual(retrievedExposedEntity, initialExposedEntity)
245
- assert.lengthOf(model.exposes, 1)
246
- })
247
-
248
- test('notifies change when a new entity is exposed', async ({ assert }) => {
249
- const model = new ApiModel()
250
- let notified = false
251
- model.addEventListener('change', () => {
252
- notified = true
253
- })
254
- model.exposeEntity('notify-entity')
255
- await Promise.resolve() // Allow microtask to run
256
- assert.isTrue(notified)
257
- })
258
-
259
- test('does not notify change if entity already exposed', async ({ assert }) => {
260
- const model = new ApiModel()
261
- model.exposeEntity('no-notify-entity') // First exposure
262
- await Promise.resolve() // Allow microtask to run
263
- let notified = false
264
- model.addEventListener('change', () => {
265
- notified = true
266
- })
267
- model.exposeEntity('no-notify-entity') // Second exposure
268
- await Promise.resolve() // Allow microtask to run
269
- assert.isFalse(notified)
270
- })
271
- })
272
-
273
- test.group('ApiModel.removeEntity()', (g) => {
274
- g.tests.forEach((test) => {
275
- test.tags(['@modeling', '@api'])
276
- })
277
- test('removes an existing entity', ({ assert }) => {
278
- const model = new ApiModel()
279
- const entityKey = 'entity-to-remove'
280
- model.exposeEntity(entityKey)
281
- assert.lengthOf(model.exposes, 1)
282
-
283
- model.removeEntity(entityKey)
284
- assert.lengthOf(model.exposes, 0)
285
- })
286
-
287
- test('does nothing if entity does not exist', ({ assert }) => {
288
- const model = new ApiModel()
289
- model.exposeEntity('existing-entity')
290
- const initialExposes = [...model.exposes]
291
-
292
- model.removeEntity('non-existing-entity')
293
- assert.deepEqual(model.exposes, initialExposes)
294
- })
295
-
296
- test('notifies change when an entity is removed', async ({ assert }) => {
297
- const model = new ApiModel()
298
- const entityKey = 'notify-remove-entity'
299
- model.exposeEntity(entityKey)
300
-
301
- let notified = false
302
- model.addEventListener('change', () => {
303
- notified = true
304
- })
305
- model.removeEntity(entityKey)
306
- await Promise.resolve() // Allow microtask to run
307
- assert.isTrue(notified)
308
- })
309
-
310
- test('does not notify change if entity to remove does not exist', async ({ assert }) => {
311
- const model = new ApiModel()
312
- let notified = false
313
- model.addEventListener('change', () => {
314
- notified = true
315
- })
316
- model.removeEntity('no-notify-remove-entity')
317
- await Promise.resolve() // Allow microtask to run
318
- assert.isFalse(notified)
319
- })
210
+ }).tags(['@modeling', '@api', '@serialization'])
320
211
  })
321
212
 
322
- test.group('ApiModel.getExposedEntity()', (g) => {
323
- g.tests.forEach((test) => {
324
- test.tags(['@modeling', '@api'])
325
- })
213
+ test.group('ApiModel.getExposedEntity()', () => {
326
214
  test('returns an existing exposed entity', ({ assert }) => {
327
215
  const model = new ApiModel()
328
216
  const entityKey = 'get-entity'
329
- const exposed: ExposedEntity = { key: entityKey, actions: [] }
217
+ const exposed: ExposedEntity = { key: entityKey, actions: [], path: '', entity: { key: entityKey } }
330
218
  model.exposes.push(exposed)
331
219
 
332
- const retrievedEntity = model.getExposedEntity(entityKey)
220
+ const retrievedEntity = model.getExposedEntity({ key: entityKey })
333
221
  assert.deepEqual(retrievedEntity, exposed)
334
- })
222
+ }).tags(['@modeling', '@api'])
335
223
 
336
224
  test('returns undefined if entity is not exposed', ({ assert }) => {
337
225
  const model = new ApiModel()
338
- const retrievedEntity = model.getExposedEntity('non-exposed-entity')
226
+ const retrievedEntity = model.getExposedEntity({ key: 'non-exposed-entity' })
339
227
  assert.isUndefined(retrievedEntity)
340
- })
228
+ }).tags(['@modeling', '@api'])
341
229
  })
@@ -0,0 +1,190 @@
1
+ import { test } from '@japa/runner'
2
+ import { ApiModel, DataDomain } from '../../../src/index.js'
3
+
4
+ test.group('ApiModel.exposeEntity()', () => {
5
+ test('exposes a new entity', ({ assert }) => {
6
+ const domain = new DataDomain()
7
+ domain.info.version = '1.0.0'
8
+ const dm = domain.addModel()
9
+ const e1 = dm.addEntity()
10
+ const model = new ApiModel()
11
+ model.attachDataDomain(domain)
12
+ const exposedEntity = model.exposeEntity({ key: e1.key })
13
+
14
+ assert.isDefined(exposedEntity)
15
+ assert.typeOf(exposedEntity.key, 'string')
16
+ assert.deepEqual(exposedEntity.entity, { key: e1.key })
17
+ assert.deepEqual(exposedEntity.actions, [])
18
+ assert.includeDeepMembers(model.exposes, [exposedEntity])
19
+ }).tags(['@modeling', '@api'])
20
+
21
+ test('returns an existing entity if already exposed', ({ assert }) => {
22
+ const domain = new DataDomain()
23
+ domain.info.version = '1.0.0'
24
+ const dm = domain.addModel()
25
+ const e1 = dm.addEntity()
26
+ const model = new ApiModel()
27
+ model.attachDataDomain(domain)
28
+ const initialExposedEntity = model.exposeEntity({ key: e1.key })
29
+ const retrievedExposedEntity = model.exposeEntity({ key: e1.key })
30
+
31
+ assert.strictEqual(retrievedExposedEntity, initialExposedEntity)
32
+ assert.lengthOf(model.exposes, 1)
33
+ }).tags(['@modeling', '@api'])
34
+
35
+ test('notifies change when a new entity is exposed', async ({ assert }) => {
36
+ const domain = new DataDomain()
37
+ domain.info.version = '1.0.0'
38
+ const dm = domain.addModel()
39
+ const e1 = dm.addEntity()
40
+ const model = new ApiModel()
41
+ model.attachDataDomain(domain)
42
+ let notified = false
43
+ model.addEventListener('change', () => {
44
+ notified = true
45
+ })
46
+ model.exposeEntity({ key: e1.key })
47
+ await Promise.resolve() // Allow microtask to run
48
+ assert.isTrue(notified)
49
+ }).tags(['@modeling', '@api'])
50
+
51
+ test('does not notify change if entity already exposed', async ({ assert }) => {
52
+ const domain = new DataDomain()
53
+ domain.info.version = '1.0.0'
54
+ const dm = domain.addModel()
55
+ const e1 = dm.addEntity()
56
+ const model = new ApiModel()
57
+ model.attachDataDomain(domain)
58
+ model.exposeEntity({ key: e1.key }) // First exposure
59
+ await Promise.resolve() // Allow microtask to run
60
+ let notified = false
61
+ model.addEventListener('change', () => {
62
+ notified = true
63
+ })
64
+ model.exposeEntity({ key: e1.key }) // Second exposure
65
+ await Promise.resolve() // Allow microtask to run
66
+ assert.isFalse(notified)
67
+ }).tags(['@modeling', '@api'])
68
+
69
+ test('exposes nested entities through associations', ({ assert }) => {
70
+ const domain = new DataDomain()
71
+ domain.info.version = '1.0.0'
72
+ const dm = domain.addModel()
73
+ const eA = dm.addEntity({ info: { name: 'A' } })
74
+ const eB = dm.addEntity({ info: { name: 'B' } })
75
+ // Add association from A to B
76
+ eA.addAssociation({ key: eB.key }, { info: { name: 'entityB' } })
77
+ const model = new ApiModel()
78
+ model.attachDataDomain(domain)
79
+ const exposedA = model.exposeEntity({ key: eA.key }, { followAssociations: true })
80
+ // Find nested exposure for B
81
+ const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
82
+ assert.isDefined(nestedB)
83
+ assert.deepEqual(nestedB?.parent?.key, exposedA.key)
84
+ assert.strictEqual(nestedB?.path, 'entitybs')
85
+ })
86
+
87
+ test('does not infinitely expose circular associations', ({ assert }) => {
88
+ const domain = new DataDomain()
89
+ domain.info.version = '1.0.0'
90
+ const dm = domain.addModel()
91
+ const eA = dm.addEntity({ info: { name: 'A' } })
92
+ const eB = dm.addEntity({ info: { name: 'B' } })
93
+ // A -> B, B -> A
94
+ eA.addAssociation({ key: eB.key }, { info: { name: 'assocAB' } })
95
+ eB.addAssociation({ key: eA.key }, { info: { name: 'assocBA' } })
96
+ const model = new ApiModel()
97
+ model.attachDataDomain(domain)
98
+ model.exposeEntity({ key: eA.key }, { followAssociations: true, maxDepth: 4 })
99
+
100
+ assert.lengthOf(model.exposes, 2, 'has only 2 exposures')
101
+ // Should expose A (root), B (nested under A), but not infinitely nest
102
+ const exposedA = model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)
103
+ const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
104
+ // There should be only one nested exposure for B under A
105
+ assert.isDefined(exposedA)
106
+ assert.isDefined(nestedB)
107
+ // There should NOT be a nested exposure for A under B
108
+ const circularA = model.exposes.find((e) => !e.isRoot && e.entity.key === eA.key && e.parent?.key === nestedB?.key)
109
+ assert.isUndefined(circularA)
110
+ })
111
+
112
+ test('does not expose self-association as nested entity', ({ assert }) => {
113
+ const domain = new DataDomain()
114
+ domain.info.version = '1.0.0'
115
+ const dm = domain.addModel()
116
+ const eA = dm.addEntity({ info: { name: 'A' } })
117
+ // A -> A (self-association)
118
+ eA.addAssociation({ key: eA.key }, { info: { name: 'assocAA' } })
119
+ const model = new ApiModel()
120
+ model.attachDataDomain(domain)
121
+ model.exposeEntity({ key: eA.key }, { followAssociations: true })
122
+ // Should only expose A as root, not as nested
123
+ const exposedA = model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)
124
+ const nestedA = model.exposes.find((e) => !e.isRoot && e.entity.key === eA.key)
125
+ assert.isDefined(exposedA)
126
+ assert.isUndefined(nestedA)
127
+ })
128
+
129
+ test('exposes multi-level associations (A -> B -> C)', ({ assert }) => {
130
+ const domain = new DataDomain()
131
+ domain.info.version = '1.0.0'
132
+ const dm = domain.addModel()
133
+ const eA = dm.addEntity({ info: { name: 'A' } })
134
+ const eB = dm.addEntity({ info: { name: 'B' } })
135
+ const eC = dm.addEntity({ info: { name: 'C' } })
136
+ eA.addAssociation({ key: eB.key }, { info: { name: 'toB' } })
137
+ eB.addAssociation({ key: eC.key }, { info: { name: 'toC' } })
138
+ const model = new ApiModel()
139
+ model.attachDataDomain(domain)
140
+ model.exposeEntity({ key: eA.key }, { followAssociations: true })
141
+ const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
142
+ const nestedC = model.exposes.find((e) => !e.isRoot && e.entity.key === eC.key)
143
+ assert.isDefined(nestedB)
144
+ assert.isDefined(nestedC)
145
+ assert.deepEqual(nestedB?.parent?.key, model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)?.key)
146
+ assert.deepEqual(nestedC?.parent?.key, nestedB?.key)
147
+ })
148
+
149
+ test('exposes multiple associations from one entity (A -> B, A -> C)', ({ assert }) => {
150
+ const domain = new DataDomain()
151
+ domain.info.version = '1.0.0'
152
+ const dm = domain.addModel()
153
+ const eA = dm.addEntity({ info: { name: 'A' } })
154
+ const eB = dm.addEntity({ info: { name: 'B' } })
155
+ const eC = dm.addEntity({ info: { name: 'C' } })
156
+ eA.addAssociation({ key: eB.key }, { info: { name: 'toB' } })
157
+ eA.addAssociation({ key: eC.key }, { info: { name: 'toC' } })
158
+ const model = new ApiModel()
159
+ model.attachDataDomain(domain)
160
+ model.exposeEntity({ key: eA.key }, { followAssociations: true })
161
+ const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
162
+ const nestedC = model.exposes.find((e) => !e.isRoot && e.entity.key === eC.key)
163
+ assert.isDefined(nestedB)
164
+ assert.isDefined(nestedC)
165
+ assert.deepEqual(nestedB?.parent?.key, model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)?.key)
166
+ assert.deepEqual(nestedC?.parent?.key, model.exposes.find((e) => e.isRoot && e.entity.key === eA.key)?.key)
167
+ })
168
+
169
+ test('respects maxDepth option (A -> B -> C -> D, maxDepth=2)', ({ assert }) => {
170
+ const domain = new DataDomain()
171
+ domain.info.version = '1.0.0'
172
+ const dm = domain.addModel()
173
+ const eA = dm.addEntity({ info: { name: 'A' } })
174
+ const eB = dm.addEntity({ info: { name: 'B' } })
175
+ const eC = dm.addEntity({ info: { name: 'C' } })
176
+ const eD = dm.addEntity({ info: { name: 'D' } })
177
+ eA.addAssociation({ key: eB.key }, { info: { name: 'toB' } })
178
+ eB.addAssociation({ key: eC.key }, { info: { name: 'toC' } })
179
+ eC.addAssociation({ key: eD.key }, { info: { name: 'toD' } })
180
+ const model = new ApiModel()
181
+ model.attachDataDomain(domain)
182
+ model.exposeEntity({ key: eA.key }, { followAssociations: true, maxDepth: 2 })
183
+ const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
184
+ const nestedC = model.exposes.find((e) => !e.isRoot && e.entity.key === eC.key)
185
+ const nestedD = model.exposes.find((e) => !e.isRoot && e.entity.key === eD.key)
186
+ assert.isDefined(nestedB)
187
+ assert.isDefined(nestedC)
188
+ assert.isUndefined(nestedD)
189
+ })
190
+ })
@@ -0,0 +1,82 @@
1
+ import { test } from '@japa/runner'
2
+ import { ApiModel, DataDomain } from '../../../src/index.js'
3
+
4
+ test.group('ApiModel.removeEntity()', () => {
5
+ test('removes an existing entity', ({ assert }) => {
6
+ const domain = new DataDomain()
7
+ domain.info.version = '1.0.0'
8
+ const dm = domain.addModel()
9
+ const e1 = dm.addEntity()
10
+ const model = new ApiModel()
11
+ model.attachDataDomain(domain)
12
+ model.exposeEntity({ key: e1.key })
13
+ assert.lengthOf(model.exposes, 1)
14
+
15
+ model.removeEntity({ key: e1.key })
16
+ assert.lengthOf(model.exposes, 0)
17
+ }).tags(['@modeling', '@api'])
18
+
19
+ test('removes an entity and its nested children', ({ assert }) => {
20
+ const domain = new DataDomain()
21
+ domain.info.version = '1.0.0'
22
+ const dm = domain.addModel()
23
+ const eA = dm.addEntity({ info: { name: 'A' } })
24
+ const eB = dm.addEntity({ info: { name: 'B' } })
25
+ // A -> B
26
+ eA.addAssociation({ key: eB.key }, { info: { name: 'toB' } })
27
+ const model = new ApiModel()
28
+ model.attachDataDomain(domain)
29
+ model.exposeEntity({ key: eA.key }, { followAssociations: true })
30
+ // Ensure nested exposure for B was created
31
+ const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
32
+ assert.isDefined(nestedB)
33
+ assert.isAbove(model.exposes.length, 1)
34
+
35
+ // Remove root entity A and expect children to be removed as well
36
+ model.removeEntity({ key: eA.key })
37
+ assert.lengthOf(model.exposes, 0)
38
+ }).tags(['@modeling', '@api'])
39
+
40
+ test('does nothing if entity does not exist', ({ assert }) => {
41
+ const domain = new DataDomain()
42
+ domain.info.version = '1.0.0'
43
+ const dm = domain.addModel()
44
+ const e1 = dm.addEntity()
45
+ const model = new ApiModel()
46
+ model.attachDataDomain(domain)
47
+ model.exposeEntity({ key: e1.key })
48
+ const initialExposes = [...model.exposes]
49
+
50
+ model.removeEntity({ key: 'non-existing-entity' })
51
+ assert.deepEqual(model.exposes, initialExposes)
52
+ }).tags(['@modeling', '@api'])
53
+
54
+ test('notifies change when an entity is removed', async ({ assert }) => {
55
+ const domain = new DataDomain()
56
+ domain.info.version = '1.0.0'
57
+ const dm = domain.addModel()
58
+ const e1 = dm.addEntity()
59
+ const model = new ApiModel()
60
+ model.attachDataDomain(domain)
61
+ model.exposeEntity({ key: e1.key })
62
+
63
+ let notified = false
64
+ model.addEventListener('change', () => {
65
+ notified = true
66
+ })
67
+ model.removeEntity({ key: e1.key })
68
+ await Promise.resolve() // Allow microtask to run
69
+ assert.isTrue(notified)
70
+ }).tags(['@modeling', '@api'])
71
+
72
+ test('does not notify change if entity to remove does not exist', async ({ assert }) => {
73
+ const model = new ApiModel()
74
+ let notified = false
75
+ model.addEventListener('change', () => {
76
+ notified = true
77
+ })
78
+ model.removeEntity({ key: 'no-notify-remove-entity' })
79
+ await Promise.resolve() // Allow microtask to run
80
+ assert.isFalse(notified)
81
+ }).tags(['@modeling', '@api'])
82
+ })
@@ -0,0 +1,10 @@
1
+ import { test } from '@japa/runner'
2
+ import * as helpers from '../../../../src/modeling/helpers/endpointHelpers.js'
3
+
4
+ test.group('endpointHelpers', () => {
5
+ test('paramNameFor extracts last key', ({ assert }) => {
6
+ assert.equal(helpers.paramNameFor('product'), 'productId')
7
+ assert.equal(helpers.paramNameFor('domain:product'), 'productId')
8
+ assert.equal(helpers.paramNameFor('a:b:c'), 'cId')
9
+ })
10
+ })