@api-client/core 0.18.47 → 0.18.49

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 (33) hide show
  1. package/build/src/mocking/ModelingMock.d.ts +2 -0
  2. package/build/src/mocking/ModelingMock.d.ts.map +1 -1
  3. package/build/src/mocking/ModelingMock.js +2 -0
  4. package/build/src/mocking/ModelingMock.js.map +1 -1
  5. package/build/src/mocking/lib/File.d.ts +1 -0
  6. package/build/src/mocking/lib/File.d.ts.map +1 -1
  7. package/build/src/mocking/lib/File.js +1 -0
  8. package/build/src/mocking/lib/File.js.map +1 -1
  9. package/build/src/mocking/lib/Permission.d.ts +35 -0
  10. package/build/src/mocking/lib/Permission.d.ts.map +1 -0
  11. package/build/src/mocking/lib/Permission.js +89 -0
  12. package/build/src/mocking/lib/Permission.js.map +1 -0
  13. package/build/src/modeling/ApiModel.d.ts +12 -4
  14. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  15. package/build/src/modeling/ApiModel.js +76 -31
  16. package/build/src/modeling/ApiModel.js.map +1 -1
  17. package/build/src/modeling/ExposedEntity.d.ts +9 -0
  18. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  19. package/build/src/modeling/ExposedEntity.js +23 -0
  20. package/build/src/modeling/ExposedEntity.js.map +1 -1
  21. package/build/tsconfig.tsbuildinfo +1 -1
  22. package/data/models/example-generator-api.json +9 -9
  23. package/package.json +3 -3
  24. package/src/mocking/ModelingMock.ts +2 -0
  25. package/src/mocking/lib/File.ts +1 -0
  26. package/src/mocking/lib/Permission.ts +100 -0
  27. package/src/modeling/ApiModel.ts +82 -37
  28. package/src/modeling/ExposedEntity.ts +28 -0
  29. package/tests/unit/mocking/current/Permission.spec.ts +285 -0
  30. package/tests/unit/modeling/api_model.spec.ts +20 -0
  31. package/tests/unit/modeling/api_model_expose_entity.spec.ts +25 -0
  32. package/tests/unit/modeling/api_model_remove_entity.spec.ts +17 -10
  33. package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +107 -0
@@ -0,0 +1,285 @@
1
+ import { test } from '@japa/runner'
2
+ import { Permission } from '../../../../src/mocking/lib/Permission.js'
3
+ import { Kind as PermissionKind } from '../../../../src/models/store/Permission.js'
4
+ import type { IPermission } from '../../../../src/models/store/Permission.js'
5
+
6
+ test.group('permission()', (group) => {
7
+ let permission: Permission
8
+
9
+ group.each.setup(() => {
10
+ permission = new Permission()
11
+ })
12
+
13
+ test('returns an object', ({ assert }) => {
14
+ const result = permission.permission()
15
+ assert.typeOf(result, 'object')
16
+ })
17
+
18
+ test('has the {property} property of a type {type}')
19
+ .with([
20
+ { property: 'kind', type: 'string' },
21
+ { property: 'key', type: 'string' },
22
+ { property: 'type', type: 'string' },
23
+ { property: 'granteeId', type: 'string' },
24
+ { property: 'itemId', type: 'string' },
25
+ { property: 'role', type: 'string' },
26
+ { property: 'addingUser', type: 'string' },
27
+ { property: 'depth', type: 'number' },
28
+ { property: 'sourceRule', type: 'string' },
29
+ ])
30
+ .run(({ assert }, { property, type }) => {
31
+ const result = permission.permission()
32
+ assert.typeOf(result[property as keyof IPermission], type)
33
+ })
34
+
35
+ test('has the correct kind', ({ assert }) => {
36
+ const result = permission.permission()
37
+ assert.equal(result.kind, PermissionKind)
38
+ })
39
+
40
+ test('type is one of the valid values', ({ assert }) => {
41
+ const result = permission.permission()
42
+ assert.include(['user', 'group', 'organization'], result.type)
43
+ })
44
+
45
+ test('role is one of the valid values', ({ assert }) => {
46
+ const result = permission.permission()
47
+ assert.include(['reader', 'commenter', 'writer', 'owner'], result.role)
48
+ })
49
+
50
+ test('sourceRule is one of the valid values', ({ assert }) => {
51
+ const result = permission.permission()
52
+ assert.include(['direct_user_grant', 'creator_default_owner', 'parent_owner_editor_rule'], result.sourceRule)
53
+ })
54
+
55
+ test('uses passed key', ({ assert }) => {
56
+ const result = permission.permission({ key: 'custom-key' })
57
+ assert.equal(result.key, 'custom-key')
58
+ })
59
+
60
+ test('uses passed type', ({ assert }) => {
61
+ const result = permission.permission({ type: 'group' })
62
+ assert.equal(result.type, 'group')
63
+ })
64
+
65
+ test('uses passed granteeId', ({ assert }) => {
66
+ const result = permission.permission({ granteeId: 'grantee-123' })
67
+ assert.equal(result.granteeId, 'grantee-123')
68
+ })
69
+
70
+ test('uses passed itemId', ({ assert }) => {
71
+ const result = permission.permission({ itemId: 'item-456' })
72
+ assert.equal(result.itemId, 'item-456')
73
+ })
74
+
75
+ test('uses passed role', ({ assert }) => {
76
+ const result = permission.permission({ role: 'writer' })
77
+ assert.equal(result.role, 'writer')
78
+ })
79
+
80
+ test('uses passed addingUser', ({ assert }) => {
81
+ const result = permission.permission({ addingUser: 'user-789' })
82
+ assert.equal(result.addingUser, 'user-789')
83
+ })
84
+
85
+ test('uses passed depth', ({ assert }) => {
86
+ const result = permission.permission({ depth: 2 })
87
+ assert.equal(result.depth, 2)
88
+ })
89
+
90
+ test('uses passed sourceRule', ({ assert }) => {
91
+ const result = permission.permission({ sourceRule: 'creator_default_owner' })
92
+ assert.equal(result.sourceRule, 'creator_default_owner')
93
+ })
94
+
95
+ test('generates displayName by default', ({ assert }) => {
96
+ const result = permission.permission()
97
+ assert.typeOf(result.displayName, 'string')
98
+ })
99
+
100
+ test('uses passed displayName', ({ assert }) => {
101
+ const result = permission.permission({ displayName: 'John Doe' })
102
+ assert.equal(result.displayName, 'John Doe')
103
+ })
104
+
105
+ test('can explicitly set displayName to undefined', ({ assert }) => {
106
+ const result = permission.permission({ displayName: undefined })
107
+ assert.isUndefined(result.displayName)
108
+ })
109
+
110
+ test('generates expirationTime for user type by default', ({ assert }) => {
111
+ const result = permission.permission({ type: 'user' })
112
+ assert.typeOf(result.expirationTime, 'number')
113
+ })
114
+
115
+ test('generates expirationTime for group type by default', ({ assert }) => {
116
+ const result = permission.permission({ type: 'group' })
117
+ assert.typeOf(result.expirationTime, 'number')
118
+ })
119
+
120
+ test('does not generate expirationTime for organization type', ({ assert }) => {
121
+ const result = permission.permission({ type: 'organization' })
122
+ assert.isUndefined(result.expirationTime)
123
+ })
124
+
125
+ test('uses passed expirationTime', ({ assert }) => {
126
+ const time = Date.now() + 86400000
127
+ const result = permission.permission({ expirationTime: time })
128
+ assert.equal(result.expirationTime, time)
129
+ })
130
+
131
+ test('can explicitly set expirationTime to undefined', ({ assert }) => {
132
+ const result = permission.permission({ type: 'user', expirationTime: undefined })
133
+ assert.isUndefined(result.expirationTime)
134
+ })
135
+ })
136
+
137
+ test.group('userPermission()', (group) => {
138
+ let permission: Permission
139
+
140
+ group.each.setup(() => {
141
+ permission = new Permission()
142
+ })
143
+
144
+ test('returns an object', ({ assert }) => {
145
+ const result = permission.userPermission()
146
+ assert.typeOf(result, 'object')
147
+ })
148
+
149
+ test('has type set to user', ({ assert }) => {
150
+ const result = permission.userPermission()
151
+ assert.equal(result.type, 'user')
152
+ })
153
+
154
+ test('has displayName', ({ assert }) => {
155
+ const result = permission.userPermission()
156
+ assert.typeOf(result.displayName, 'string')
157
+ })
158
+
159
+ test('has expirationTime', ({ assert }) => {
160
+ const result = permission.userPermission()
161
+ assert.typeOf(result.expirationTime, 'number')
162
+ })
163
+
164
+ test('uses passed init values', ({ assert }) => {
165
+ const result = permission.userPermission({ role: 'owner', itemId: 'item-123' })
166
+ assert.equal(result.type, 'user')
167
+ assert.equal(result.role, 'owner')
168
+ assert.equal(result.itemId, 'item-123')
169
+ })
170
+ })
171
+
172
+ test.group('groupPermission()', (group) => {
173
+ let permission: Permission
174
+
175
+ group.each.setup(() => {
176
+ permission = new Permission()
177
+ })
178
+
179
+ test('returns an object', ({ assert }) => {
180
+ const result = permission.groupPermission()
181
+ assert.typeOf(result, 'object')
182
+ })
183
+
184
+ test('has type set to group', ({ assert }) => {
185
+ const result = permission.groupPermission()
186
+ assert.equal(result.type, 'group')
187
+ })
188
+
189
+ test('has displayName', ({ assert }) => {
190
+ const result = permission.groupPermission()
191
+ assert.typeOf(result.displayName, 'string')
192
+ })
193
+
194
+ test('has expirationTime', ({ assert }) => {
195
+ const result = permission.groupPermission()
196
+ assert.typeOf(result.expirationTime, 'number')
197
+ })
198
+
199
+ test('uses passed init values', ({ assert }) => {
200
+ const result = permission.groupPermission({ role: 'commenter', granteeId: 'group-456' })
201
+ assert.equal(result.type, 'group')
202
+ assert.equal(result.role, 'commenter')
203
+ assert.equal(result.granteeId, 'group-456')
204
+ })
205
+ })
206
+
207
+ test.group('organizationPermission()', (group) => {
208
+ let permission: Permission
209
+
210
+ group.each.setup(() => {
211
+ permission = new Permission()
212
+ })
213
+
214
+ test('returns an object', ({ assert }) => {
215
+ const result = permission.organizationPermission()
216
+ assert.typeOf(result, 'object')
217
+ })
218
+
219
+ test('has type set to organization', ({ assert }) => {
220
+ const result = permission.organizationPermission()
221
+ assert.equal(result.type, 'organization')
222
+ })
223
+
224
+ test('has displayName', ({ assert }) => {
225
+ const result = permission.organizationPermission()
226
+ assert.typeOf(result.displayName, 'string')
227
+ })
228
+
229
+ test('does not have expirationTime', ({ assert }) => {
230
+ const result = permission.organizationPermission()
231
+ assert.isUndefined(result.expirationTime)
232
+ })
233
+
234
+ test('uses passed init values', ({ assert }) => {
235
+ const result = permission.organizationPermission({ role: 'reader', addingUser: 'admin-789' })
236
+ assert.equal(result.type, 'organization')
237
+ assert.equal(result.role, 'reader')
238
+ assert.equal(result.addingUser, 'admin-789')
239
+ })
240
+ })
241
+
242
+ test.group('permissions()', (group) => {
243
+ let permission: Permission
244
+
245
+ group.each.setup(() => {
246
+ permission = new Permission()
247
+ })
248
+
249
+ test('returns an array', ({ assert }) => {
250
+ const result = permission.permissions()
251
+ assert.isArray(result)
252
+ })
253
+
254
+ test('returns 25 items by default', ({ assert }) => {
255
+ const result = permission.permissions()
256
+ assert.lengthOf(result, 25)
257
+ })
258
+
259
+ test('returns specified number of items', ({ assert }) => {
260
+ const result = permission.permissions(10)
261
+ assert.lengthOf(result, 10)
262
+ })
263
+
264
+ test('all items have the correct kind', ({ assert }) => {
265
+ const result = permission.permissions(5)
266
+ result.forEach((item) => {
267
+ assert.equal(item.kind, PermissionKind)
268
+ })
269
+ })
270
+
271
+ test('uses passed init values for all items', ({ assert }) => {
272
+ const result = permission.permissions(5, { type: 'user', role: 'owner' })
273
+ result.forEach((item) => {
274
+ assert.equal(item.type, 'user')
275
+ assert.equal(item.role, 'owner')
276
+ })
277
+ })
278
+
279
+ test('generates unique keys', ({ assert }) => {
280
+ const result = permission.permissions(10)
281
+ const keys = result.map((item) => item.key)
282
+ const uniqueKeys = new Set(keys)
283
+ assert.equal(keys.length, uniqueKeys.size)
284
+ })
285
+ })
@@ -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
+ })