@api-client/core 0.18.48 → 0.18.50

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 (61) hide show
  1. package/build/src/mocking/lib/DataCatalog.d.ts +4 -4
  2. package/build/src/mocking/lib/DataCatalog.d.ts.map +1 -1
  3. package/build/src/mocking/lib/DataCatalog.js +8 -8
  4. package/build/src/mocking/lib/DataCatalog.js.map +1 -1
  5. package/build/src/mocking/lib/File.d.ts +2 -2
  6. package/build/src/mocking/lib/File.d.ts.map +1 -1
  7. package/build/src/mocking/lib/File.js +5 -5
  8. package/build/src/mocking/lib/File.js.map +1 -1
  9. package/build/src/mocking/lib/Group.d.ts +1 -1
  10. package/build/src/mocking/lib/Group.d.ts.map +1 -1
  11. package/build/src/mocking/lib/Group.js +2 -2
  12. package/build/src/mocking/lib/Group.js.map +1 -1
  13. package/build/src/mocking/lib/Invitation.d.ts +1 -1
  14. package/build/src/mocking/lib/Invitation.d.ts.map +1 -1
  15. package/build/src/mocking/lib/Invitation.js +2 -2
  16. package/build/src/mocking/lib/Invitation.js.map +1 -1
  17. package/build/src/mocking/lib/Organization.d.ts +1 -1
  18. package/build/src/mocking/lib/Organization.d.ts.map +1 -1
  19. package/build/src/mocking/lib/Organization.js +2 -2
  20. package/build/src/mocking/lib/Organization.js.map +1 -1
  21. package/build/src/mocking/lib/Patch.d.ts +1 -1
  22. package/build/src/mocking/lib/Patch.d.ts.map +1 -1
  23. package/build/src/mocking/lib/Patch.js +2 -2
  24. package/build/src/mocking/lib/Patch.js.map +1 -1
  25. package/build/src/mocking/lib/Trash.d.ts +1 -1
  26. package/build/src/mocking/lib/Trash.d.ts.map +1 -1
  27. package/build/src/mocking/lib/Trash.js +2 -2
  28. package/build/src/mocking/lib/Trash.js.map +1 -1
  29. package/build/src/modeling/ApiModel.d.ts +12 -4
  30. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  31. package/build/src/modeling/ApiModel.js +76 -31
  32. package/build/src/modeling/ApiModel.js.map +1 -1
  33. package/build/src/modeling/ExposedEntity.d.ts +9 -0
  34. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  35. package/build/src/modeling/ExposedEntity.js +23 -0
  36. package/build/src/modeling/ExposedEntity.js.map +1 -1
  37. package/build/tsconfig.tsbuildinfo +1 -1
  38. package/data/models/example-generator-api.json +24 -24
  39. package/package.json +2 -2
  40. package/src/mocking/README.md +40 -0
  41. package/src/mocking/lib/DataCatalog.ts +8 -8
  42. package/src/mocking/lib/File.ts +5 -5
  43. package/src/mocking/lib/Group.ts +2 -2
  44. package/src/mocking/lib/Invitation.ts +2 -2
  45. package/src/mocking/lib/Organization.ts +2 -2
  46. package/src/mocking/lib/Patch.ts +2 -2
  47. package/src/mocking/lib/Trash.ts +2 -2
  48. package/src/modeling/ApiModel.ts +82 -37
  49. package/src/modeling/ExposedEntity.ts +28 -0
  50. package/src/models/README.md +5 -4
  51. package/tests/unit/mocking/current/DataCatalog.spec.ts +28 -0
  52. package/tests/unit/mocking/current/File.spec.ts +79 -0
  53. package/tests/unit/mocking/current/Group.spec.ts +49 -0
  54. package/tests/unit/mocking/current/Invitation.spec.ts +49 -0
  55. package/tests/unit/mocking/current/Organization.spec.ts +50 -0
  56. package/tests/unit/mocking/current/Patch.spec.ts +47 -0
  57. package/tests/unit/mocking/current/Trash.spec.ts +49 -0
  58. package/tests/unit/modeling/api_model.spec.ts +20 -0
  59. package/tests/unit/modeling/api_model_expose_entity.spec.ts +25 -0
  60. package/tests/unit/modeling/api_model_remove_entity.spec.ts +17 -10
  61. package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +107 -0
@@ -138,6 +138,13 @@ test.group('dataCatalogs()', (group) => {
138
138
  catalog.dataCatalogs(5)
139
139
  assert.equal(spy.callCount, 5)
140
140
  })
141
+
142
+ test('uses passed initialization values', ({ assert }) => {
143
+ const result = catalog.dataCatalogs(3, { name: 'Batch Catalog' })
144
+ result.forEach((item) => {
145
+ assert.equal(item.name, 'Batch Catalog')
146
+ })
147
+ })
141
148
  })
142
149
 
143
150
  test.group('dataCatalogVersion()', (group) => {
@@ -265,6 +272,13 @@ test.group('dataCatalogVersions()', (group) => {
265
272
  catalog.dataCatalogVersions(3)
266
273
  assert.equal(spy.callCount, 3)
267
274
  })
275
+
276
+ test('uses passed initialization values', ({ assert }) => {
277
+ const result = catalog.dataCatalogVersions(3, { version: 'v1.2.3' })
278
+ result.forEach((item) => {
279
+ assert.equal(item.version, 'v1.2.3')
280
+ })
281
+ })
268
282
  })
269
283
 
270
284
  test.group('dataCatalogWithVersion()', (group) => {
@@ -372,6 +386,13 @@ test.group('dataCatalogsWithVersion()', (group) => {
372
386
  assert.isAtLeast(item.versions.length, 1)
373
387
  })
374
388
  })
389
+
390
+ test('uses passed initialization values', ({ assert }) => {
391
+ const result = catalog.dataCatalogsWithVersion(3, { name: 'Versioned Batch' })
392
+ result.forEach((item) => {
393
+ assert.equal(item.name, 'Versioned Batch')
394
+ })
395
+ })
375
396
  })
376
397
 
377
398
  test.group('versionInfo()', (group) => {
@@ -446,4 +467,11 @@ test.group('versionInfos()', (group) => {
446
467
  catalog.versionInfos(6)
447
468
  assert.equal(spy.callCount, 6)
448
469
  })
470
+
471
+ test('uses passed initialization values', ({ assert }) => {
472
+ const result = catalog.versionInfos(3, { version: 'v9.9.9' })
473
+ result.forEach((item) => {
474
+ assert.equal(item.version, 'v9.9.9')
475
+ })
476
+ })
449
477
  })
@@ -0,0 +1,79 @@
1
+ import { test } from '@japa/runner'
2
+ import { File } from '../../../../src/mocking/lib/File.js'
3
+ import { FolderKind, ProjectKind } from '../../../../src/models/kinds.js'
4
+ import type { IFile } from '../../../../src/models/store/File.js'
5
+
6
+ test.group('File', (group) => {
7
+ let fileMock: File
8
+
9
+ group.each.setup(() => {
10
+ fileMock = new File()
11
+ })
12
+
13
+ test('file() returns a valid object', ({ assert }) => {
14
+ const result = fileMock.file()
15
+ assert.typeOf(result, 'object')
16
+ assert.property(result, 'kind')
17
+ assert.property(result, 'info')
18
+ })
19
+
20
+ test('file(init) allows overriding properties', ({ assert }) => {
21
+ const init: Partial<IFile> = {
22
+ kind: ProjectKind,
23
+ info: { name: 'My Project' },
24
+ }
25
+ const result = fileMock.file(init)
26
+ assert.equal(result.kind, ProjectKind)
27
+ assert.equal(result.info.name, 'My Project')
28
+ })
29
+
30
+ test('files() returns a list', ({ assert }) => {
31
+ const result = fileMock.files(5)
32
+ assert.isArray(result)
33
+ assert.lengthOf(result, 5)
34
+ })
35
+
36
+ test('files(size, init) allows overriding properties', ({ assert }) => {
37
+ const init: Partial<IFile> = {
38
+ kind: ProjectKind,
39
+ }
40
+ const result = fileMock.files(3, init)
41
+ assert.lengthOf(result, 3)
42
+ result.forEach((item) => {
43
+ assert.equal(item.kind, ProjectKind)
44
+ })
45
+ })
46
+
47
+ test('folder() returns a valid object', ({ assert }) => {
48
+ const result = fileMock.folder()
49
+ assert.typeOf(result, 'object')
50
+ assert.equal(result.kind, FolderKind)
51
+ })
52
+
53
+ test('folder(init) allows overriding properties', ({ assert }) => {
54
+ const init: Partial<IFile> = {
55
+ key: 'custom-folder-key',
56
+ }
57
+ const result = fileMock.folder(init)
58
+ assert.equal(result.kind, FolderKind)
59
+ assert.equal(result.key, 'custom-folder-key')
60
+ })
61
+
62
+ test('folders() returns a list', ({ assert }) => {
63
+ const result = fileMock.folders(5)
64
+ assert.isArray(result)
65
+ assert.lengthOf(result, 5)
66
+ })
67
+
68
+ test('folders(size, init) allows overriding properties', ({ assert }) => {
69
+ const init: Partial<IFile> = {
70
+ key: 'batch-key',
71
+ }
72
+ const result = fileMock.folders(3, init)
73
+ assert.lengthOf(result, 3)
74
+ result.forEach((item) => {
75
+ assert.equal(item.kind, FolderKind)
76
+ assert.equal(item.key, 'batch-key')
77
+ })
78
+ })
79
+ })
@@ -0,0 +1,49 @@
1
+ import { test } from '@japa/runner'
2
+ import { Group } from '../../../../src/mocking/lib/Group.js'
3
+ import { GroupKind } from '../../../../src/models/kinds.js'
4
+ import type { GroupSchema } from '../../../../src/models/store/Group.js'
5
+
6
+ test.group('Group', (group) => {
7
+ let groupMock: Group
8
+
9
+ group.each.setup(() => {
10
+ groupMock = new Group()
11
+ })
12
+
13
+ test('group() returns a valid object', ({ assert }) => {
14
+ const result = groupMock.group()
15
+ assert.typeOf(result, 'object')
16
+ assert.equal(result.kind, GroupKind)
17
+ assert.property(result, 'key')
18
+ assert.property(result, 'name')
19
+ assert.property(result, 'color')
20
+ assert.property(result, 'users')
21
+ })
22
+
23
+ test('group(init) allows overriding properties', ({ assert }) => {
24
+ const init: Partial<GroupSchema> = {
25
+ name: 'Custom Group',
26
+ color: '#ff0000',
27
+ }
28
+ const result = groupMock.group(init)
29
+ assert.equal(result.name, 'Custom Group')
30
+ assert.equal(result.color, '#ff0000')
31
+ })
32
+
33
+ test('groups() returns a list', ({ assert }) => {
34
+ const result = groupMock.groups(5)
35
+ assert.isArray(result)
36
+ assert.lengthOf(result, 5)
37
+ })
38
+
39
+ test('groups(size, init) allows overriding properties', ({ assert }) => {
40
+ const init: Partial<GroupSchema> = {
41
+ description: 'Batch Description',
42
+ }
43
+ const result = groupMock.groups(3, init)
44
+ assert.lengthOf(result, 3)
45
+ result.forEach((item) => {
46
+ assert.equal(item.description, 'Batch Description')
47
+ })
48
+ })
49
+ })
@@ -0,0 +1,49 @@
1
+ import { test } from '@japa/runner'
2
+ import { Invitation } from '../../../../src/mocking/lib/Invitation.js'
3
+ import { InvitationKind } from '../../../../src/models/kinds.js'
4
+ import type { InvitationSchema } from '../../../../src/models/store/Invitation.js'
5
+
6
+ test.group('Invitation', (group) => {
7
+ let invitation: Invitation
8
+
9
+ group.each.setup(() => {
10
+ invitation = new Invitation()
11
+ })
12
+
13
+ test('invitation() returns a valid object', ({ assert }) => {
14
+ const result = invitation.invitation()
15
+ assert.typeOf(result, 'object')
16
+ assert.equal(result.kind, InvitationKind)
17
+ assert.property(result, 'key')
18
+ assert.property(result, 'email')
19
+ assert.property(result, 'token')
20
+ assert.property(result, 'status')
21
+ })
22
+
23
+ test('invitation(init) allows overriding properties', ({ assert }) => {
24
+ const init: Partial<InvitationSchema> = {
25
+ email: 'test@example.com',
26
+ status: 'accepted',
27
+ }
28
+ const result = invitation.invitation(init)
29
+ assert.equal(result.email, 'test@example.com')
30
+ assert.equal(result.status, 'accepted')
31
+ })
32
+
33
+ test('invitations() returns a list', ({ assert }) => {
34
+ const result = invitation.invitations(5)
35
+ assert.isArray(result)
36
+ assert.lengthOf(result, 5)
37
+ })
38
+
39
+ test('invitations(size, init) allows overriding properties', ({ assert }) => {
40
+ const init: Partial<InvitationSchema> = {
41
+ status: 'declined',
42
+ }
43
+ const result = invitation.invitations(3, init)
44
+ assert.lengthOf(result, 3)
45
+ result.forEach((item) => {
46
+ assert.equal(item.status, 'declined')
47
+ })
48
+ })
49
+ })
@@ -0,0 +1,50 @@
1
+ import { test } from '@japa/runner'
2
+ import { Organization } from '../../../../src/mocking/lib/Organization.js'
3
+ import { OrganizationKind } from '../../../../src/models/kinds.js'
4
+ import type { IOrganization } from '../../../../src/models/store/Organization.js'
5
+
6
+ test.group('Organization', (group) => {
7
+ let organization: Organization
8
+
9
+ group.each.setup(() => {
10
+ organization = new Organization()
11
+ })
12
+
13
+ test('organization() returns a valid object', ({ assert }) => {
14
+ const result = organization.organization()
15
+ assert.typeOf(result, 'object')
16
+ assert.equal(result.kind, OrganizationKind)
17
+ assert.property(result, 'key')
18
+ assert.property(result, 'name')
19
+ assert.property(result, 'createdBy')
20
+ assert.property(result, 'createdDate')
21
+ assert.property(result, 'grantType')
22
+ })
23
+
24
+ test('organization(init) allows overriding properties', ({ assert }) => {
25
+ const init: Partial<IOrganization> = {
26
+ key: 'custom-org-key',
27
+ name: 'Custom Org',
28
+ }
29
+ const result = organization.organization(init)
30
+ assert.equal(result.key, 'custom-org-key')
31
+ assert.equal(result.name, 'Custom Org')
32
+ })
33
+
34
+ test('organizations() returns a list', ({ assert }) => {
35
+ const result = organization.organizations(5)
36
+ assert.isArray(result)
37
+ assert.lengthOf(result, 5)
38
+ })
39
+
40
+ test('organizations(size, init) allows overriding properties', ({ assert }) => {
41
+ const init: Partial<IOrganization> = {
42
+ name: 'Batch Org',
43
+ }
44
+ const result = organization.organizations(3, init)
45
+ assert.lengthOf(result, 3)
46
+ result.forEach((item) => {
47
+ assert.equal(item.name, 'Batch Org')
48
+ })
49
+ })
50
+ })
@@ -0,0 +1,47 @@
1
+ import { test } from '@japa/runner'
2
+ import { Patch } from '../../../../src/mocking/lib/Patch.js'
3
+ import type { MediaPatchRevision } from '../../../../src/patch/types.js'
4
+
5
+ test.group('Patch', (group) => {
6
+ let patch: Patch
7
+
8
+ group.each.setup(() => {
9
+ patch = new Patch()
10
+ })
11
+
12
+ test('mediaPatchRevision() returns a valid object', ({ assert }) => {
13
+ const result = patch.mediaPatchRevision()
14
+ assert.typeOf(result, 'object')
15
+ assert.property(result, 'id')
16
+ assert.property(result, 'timestamp')
17
+ assert.property(result, 'patch')
18
+ assert.property(result, 'version')
19
+ })
20
+
21
+ test('mediaPatchRevision(init) allows overriding properties', ({ assert }) => {
22
+ const init: Partial<MediaPatchRevision> = {
23
+ id: 'custom-id',
24
+ version: 99,
25
+ }
26
+ const result = patch.mediaPatchRevision(init)
27
+ assert.equal(result.id, 'custom-id')
28
+ assert.equal(result.version, 99)
29
+ })
30
+
31
+ test('mediaPatchRevisions() returns a list', ({ assert }) => {
32
+ const result = patch.mediaPatchRevisions(5)
33
+ assert.isArray(result)
34
+ assert.lengthOf(result, 5)
35
+ })
36
+
37
+ test('mediaPatchRevisions(size, init) allows overriding properties', ({ assert }) => {
38
+ const init: Partial<MediaPatchRevision> = {
39
+ version: 123,
40
+ }
41
+ const result = patch.mediaPatchRevisions(3, init)
42
+ assert.lengthOf(result, 3)
43
+ result.forEach((item) => {
44
+ assert.equal(item.version, 123)
45
+ })
46
+ })
47
+ })
@@ -0,0 +1,49 @@
1
+ import { test } from '@japa/runner'
2
+ import { Trash } from '../../../../src/mocking/lib/Trash.js'
3
+ import { ProjectKind } from '../../../../src/models/kinds.js'
4
+ import type { TrashEntry } from '../../../../src/models/TrashEntry.js'
5
+
6
+ test.group('Trash', (group) => {
7
+ let trash: Trash
8
+
9
+ group.each.setup(() => {
10
+ trash = new Trash()
11
+ })
12
+
13
+ test('trashEntry() returns a valid object', ({ assert }) => {
14
+ const result = trash.trashEntry()
15
+ assert.typeOf(result, 'object')
16
+ assert.property(result, 'key')
17
+ assert.property(result, 'refKey')
18
+ assert.property(result, 'kind')
19
+ assert.property(result, 'info')
20
+ assert.property(result, 'capabilities')
21
+ })
22
+
23
+ test('trashEntry(init) allows overriding properties', ({ assert }) => {
24
+ const init: Partial<TrashEntry> = {
25
+ key: 'custom-key',
26
+ kind: ProjectKind,
27
+ }
28
+ const result = trash.trashEntry(init)
29
+ assert.equal(result.key, 'custom-key')
30
+ assert.equal(result.kind, ProjectKind)
31
+ })
32
+
33
+ test('trashEntries() returns a list', ({ assert }) => {
34
+ const result = trash.trashEntries(5)
35
+ assert.isArray(result)
36
+ assert.lengthOf(result, 5)
37
+ })
38
+
39
+ test('trashEntries(size, init) allows overriding properties', ({ assert }) => {
40
+ const init: Partial<TrashEntry> = {
41
+ kind: ProjectKind,
42
+ }
43
+ const result = trash.trashEntries(3, init)
44
+ assert.lengthOf(result, 3)
45
+ result.forEach((item) => {
46
+ assert.equal(item.kind, ProjectKind)
47
+ })
48
+ })
49
+ })
@@ -163,6 +163,26 @@ test.group('ApiModel.constructor()', () => {
163
163
  assert.equal(model.domain!.key, 'my-domain')
164
164
  }).tags(['@modeling', '@api', '@creation'])
165
165
 
166
+ test('initializes domain dependency correctly when passed to constructor', ({ assert }) => {
167
+ const domain = new DataDomain()
168
+ domain.info.version = '1.0.0'
169
+
170
+ const model = new ApiModel({}, domain)
171
+
172
+ assert.isDefined(model.domain)
173
+ assert.equal(model.domain?.key, domain.key)
174
+ assert.lengthOf(model.dependencyList, 1)
175
+ assert.equal(model.dependencyList[0].key, domain.key)
176
+ assert.equal(model.dependencyList[0].version, '1.0.0')
177
+ }).tags(['@modeling', '@api', '@creation'])
178
+
179
+ test('throws if passed domain has no version', ({ assert }) => {
180
+ const domain = new DataDomain()
181
+ // No version set
182
+
183
+ assert.throws(() => new ApiModel({}, domain), /must have a version/)
184
+ }).tags(['@modeling', '@api', '@creation'])
185
+
166
186
  test('notifies change when info is modified', async ({ assert }) => {
167
187
  const model = new ApiModel()
168
188
  let notified = false
@@ -186,4 +186,29 @@ test.group('ApiModel.exposeEntity()', () => {
186
186
  assert.isDefined(nestedC)
187
187
  assert.isUndefined(nestedD)
188
188
  })
189
+ test('resolves root path collision by appending a number', ({ assert }) => {
190
+ const domain = new DataDomain()
191
+ domain.info.version = '1.0.0'
192
+ const dm = domain.addModel()
193
+ // Two entities that will generate the same plural path "/items"
194
+ const e1 = dm.addEntity({ info: { name: 'Item' } })
195
+ const e2 = dm.addEntity({ info: { name: 'Item' } }) // Same name
196
+
197
+ const model = new ApiModel()
198
+ model.attachDataDomain(domain)
199
+
200
+ // Expose first entity -> /items
201
+ const exp1 = model.exposeEntity({ key: e1.key })
202
+ assert.equal(exp1.collectionPath, '/items')
203
+
204
+ // Expose second entity -> should resolve collision to /items-1
205
+ const exp2 = model.exposeEntity({ key: e2.key })
206
+ assert.equal(exp2.collectionPath, '/items-1')
207
+ assert.equal(exp2.resourcePath, '/items-1/{id}')
208
+
209
+ // Expose third entity -> /items-2
210
+ const e3 = dm.addEntity({ info: { name: 'Item' } })
211
+ const exp3 = model.exposeEntity({ key: e3.key })
212
+ assert.equal(exp3.collectionPath, '/items-2')
213
+ }).tags(['@modeling', '@api'])
189
214
  })
@@ -9,10 +9,10 @@ test.group('ApiModel.removeEntity()', () => {
9
9
  const e1 = dm.addEntity()
10
10
  const model = new ApiModel()
11
11
  model.attachDataDomain(domain)
12
- model.exposeEntity({ key: e1.key })
12
+ const exposure = model.exposeEntity({ key: e1.key })
13
13
  assert.lengthOf(model.exposes, 1)
14
14
 
15
- model.removeEntity({ key: e1.key })
15
+ model.removeExposedEntity(exposure.key)
16
16
  assert.lengthOf(model.exposes, 0)
17
17
  }).tags(['@modeling', '@api'])
18
18
 
@@ -26,18 +26,18 @@ test.group('ApiModel.removeEntity()', () => {
26
26
  eA.addAssociation({ key: eB.key }, { info: { name: 'toB' } })
27
27
  const model = new ApiModel()
28
28
  model.attachDataDomain(domain)
29
- model.exposeEntity({ key: eA.key }, { followAssociations: true })
29
+ const rootExposure = model.exposeEntity({ key: eA.key }, { followAssociations: true })
30
30
  // Ensure nested exposure for B was created
31
31
  const nestedB = model.exposes.find((e) => !e.isRoot && e.entity.key === eB.key)
32
32
  assert.isDefined(nestedB)
33
33
  assert.isAbove(model.exposes.length, 1)
34
34
 
35
- // Remove root entity A and expect children to be removed as well
36
- model.removeEntity({ key: eA.key })
35
+ // Remove root exposure for A and expect children to be removed as well
36
+ model.removeExposedEntity(rootExposure.key)
37
37
  assert.lengthOf(model.exposes, 0)
38
38
  }).tags(['@modeling', '@api'])
39
39
 
40
- test('does nothing if entity does not exist', ({ assert }) => {
40
+ test('throws error if entity does not exist', ({ assert }) => {
41
41
  const domain = new DataDomain()
42
42
  domain.info.version = '1.0.0'
43
43
  const dm = domain.addModel()
@@ -46,7 +46,10 @@ test.group('ApiModel.removeEntity()', () => {
46
46
  model.attachDataDomain(domain)
47
47
  model.exposeEntity({ key: e1.key })
48
48
 
49
- model.removeEntity({ key: 'non-existing-entity' })
49
+ assert.throws(
50
+ () => model.removeExposedEntity('non-existing-key'),
51
+ 'Exposed entity with key "non-existing-key" not found.'
52
+ )
50
53
  assert.lengthOf(model.exposes, 1, 'exposes count should remain unchanged')
51
54
  }).tags(['@modeling', '@api'])
52
55
 
@@ -57,13 +60,13 @@ test.group('ApiModel.removeEntity()', () => {
57
60
  const e1 = dm.addEntity()
58
61
  const model = new ApiModel()
59
62
  model.attachDataDomain(domain)
60
- model.exposeEntity({ key: e1.key })
63
+ const exposure = model.exposeEntity({ key: e1.key })
61
64
 
62
65
  let notified = false
63
66
  model.addEventListener('change', () => {
64
67
  notified = true
65
68
  })
66
- model.removeEntity({ key: e1.key })
69
+ model.removeExposedEntity(exposure.key)
67
70
  await Promise.resolve() // Allow microtask to run
68
71
  assert.isTrue(notified)
69
72
  }).tags(['@modeling', '@api'])
@@ -74,7 +77,11 @@ test.group('ApiModel.removeEntity()', () => {
74
77
  model.addEventListener('change', () => {
75
78
  notified = true
76
79
  })
77
- model.removeEntity({ key: 'no-notify-remove-entity' })
80
+ try {
81
+ model.removeExposedEntity('no-notify-remove-entity')
82
+ } catch {
83
+ // ignore error
84
+ }
78
85
  await Promise.resolve() // Allow microtask to run
79
86
  assert.isFalse(notified)
80
87
  }).tags(['@modeling', '@api'])
@@ -0,0 +1,107 @@
1
+ import { test } from '@japa/runner'
2
+ import { ApiModel, DataDomain } from '../../../src/index.js'
3
+
4
+ test.group('ExposedEntity Path Setter Validation', () => {
5
+ test('throws when setting collection path that collides with another root entity', ({ assert }) => {
6
+ const domain = new DataDomain()
7
+ domain.info.version = '1.0.0'
8
+ const dm = domain.addModel()
9
+ const e1 = dm.addEntity({ info: { name: 'A' } })
10
+ const e2 = dm.addEntity({ info: { name: 'B' } })
11
+
12
+ const model = new ApiModel()
13
+ model.attachDataDomain(domain)
14
+
15
+ model.exposeEntity({ key: e1.key }) // /as
16
+ const exp2 = model.exposeEntity({ key: e2.key }) // /bs
17
+
18
+ // Try to rename exp2's collection path to /as (collision)
19
+ assert.throws(
20
+ () => exp2.setCollectionPath('/as'),
21
+ 'Collection path "/as" is already in use by another root entity.'
22
+ )
23
+ })
24
+
25
+ test('throws when setting resource path that collides with another root entity (singleton)', ({ assert }) => {
26
+ const domain = new DataDomain()
27
+ domain.info.version = '1.0.0'
28
+ const dm = domain.addModel()
29
+ const e1 = dm.addEntity({ info: { name: 'A' } })
30
+ const e2 = dm.addEntity({ info: { name: 'B' } }) // Collection-less
31
+
32
+ const model = new ApiModel()
33
+ model.attachDataDomain(domain)
34
+
35
+ const exp1 = model.exposeEntity({ key: e1.key }) // /as, /as/{id}
36
+ // Manually force a collision for testing resource path (singleton vs singleton or singleton vs resource)
37
+ // Let's make exp2 a singleton
38
+ const exp2 = model.exposeEntity({ key: e2.key })
39
+ // Remove collection from exp2 so we can set arbitrary resource path
40
+ // Note: implementation of setResourcePath for collection-less allows any 2 segments
41
+ // We need to simulate the state where hasCollection is false
42
+ exp2.hasCollection = false
43
+
44
+ // Set exp1 resource path to something specific
45
+ exp1.hasCollection = false
46
+ exp1.setResourcePath('/shared/path')
47
+
48
+ // Try to set exp2 resource path to same
49
+ assert.throws(
50
+ () => exp2.setResourcePath('/shared/path'),
51
+ 'Resource path "/shared/path" is already in use by another root entity.'
52
+ )
53
+ })
54
+
55
+ test('allows setting non-colliding paths for root entity', ({ assert }) => {
56
+ const domain = new DataDomain()
57
+ domain.info.version = '1.0.0'
58
+ const dm = domain.addModel()
59
+ const e1 = dm.addEntity({ info: { name: 'A' } })
60
+
61
+ const model = new ApiModel()
62
+ model.attachDataDomain(domain)
63
+
64
+ const exp1 = model.exposeEntity({ key: e1.key }) // /as
65
+
66
+ exp1.setCollectionPath('/new-path')
67
+ assert.equal(exp1.collectionPath, '/new-path')
68
+
69
+ exp1.hasCollection = false // allow arbitrary resource path
70
+ exp1.setResourcePath('/custom/resource')
71
+ assert.equal(exp1.resourcePath, '/custom/resource')
72
+ })
73
+
74
+ test('does not validate collision for non-root entities', ({ assert }) => {
75
+ const domain = new DataDomain()
76
+ domain.info.version = '1.0.0'
77
+ const dm = domain.addModel()
78
+ const eA = dm.addEntity({ info: { name: 'A' } })
79
+ const eB = dm.addEntity({ info: { name: 'B' } })
80
+ eA.addAssociation({ key: eB.key }, { info: { name: 'toB' } })
81
+
82
+ const model = new ApiModel()
83
+ model.attachDataDomain(domain)
84
+
85
+ // expose A -> B
86
+ const rootExp = model.exposeEntity({ key: eA.key }, { followAssociations: true })
87
+ const nestedExp = model.exposes.find((e) => !e.isRoot)
88
+
89
+ assert.isDefined(nestedExp)
90
+ // Assuming root entity has collection path /as
91
+ assert.equal(rootExp.collectionPath, '/as')
92
+
93
+ // Try to set nested entity's collection path to /as
94
+ // Since it's not root, it should NOT check for collision with rootExp
95
+ // (In reality this path is technically valid relative to parent, but here we just check that
96
+ // it doesn't throw the specific root collision error)
97
+
98
+ // setCollectionPath logic for non-root:
99
+ // It doesn't check checks.
100
+
101
+ try {
102
+ nestedExp?.setCollectionPath('/as')
103
+ } catch (e) {
104
+ assert.notInclude((e as Error).message, 'already in use by another root entity')
105
+ }
106
+ })
107
+ })