@api-client/core 0.20.8 → 0.20.9

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.
@@ -1,84 +1,152 @@
1
1
  import { test } from '@japa/runner'
2
- import { ApiModel } from '../../../../src/modeling/ApiModel.js'
2
+ import { ApiModel, ApiModelSchema } from '../../../../src/modeling/ApiModel.js'
3
3
  import { ExposedEntityKind } from '../../../../src/models/index.js'
4
4
  import { RuntimeModelGenerator } from '../../../../src/modeling/generators/RuntimeModelGenerator.js'
5
- import { ApiModelKind } from '../../../../src/models/index.js'
6
5
  import type { UpdateActionSchema } from '../../../../src/modeling/actions/index.js'
6
+ import { DataDomain } from '../../../../src/modeling/DataDomain.js'
7
+ import { userOnlyDomain, blogDomain, singletonDomain } from '../../../fixtures/modeling/runtime/domains.js'
7
8
 
8
9
  test.group('RuntimeModelGenerator', () => {
9
10
  test('generates routes from an ApiModel instance', async ({ assert }) => {
10
- const apiModel = new ApiModel({
11
- key: 'test-api',
12
- exposes: [
13
- {
14
- key: 'users',
15
- kind: ExposedEntityKind,
16
- entity: { key: 'user' },
17
- resourcePath: '/users/{id}',
18
- collectionPath: '/users',
19
- hasCollection: true,
20
- isRoot: true,
21
- actions: [
22
- { kind: 'list' },
23
- { kind: 'create' },
24
- { kind: 'search' },
25
- { kind: 'read' },
26
- { kind: 'update', allowedMethods: ['PUT', 'PATCH'] } as UpdateActionSchema,
27
- { kind: 'delete' },
28
- ],
29
- },
30
- ],
31
- })
11
+ const { domain, user } = userOnlyDomain()
12
+ const apiModel = new ApiModel(
13
+ {
14
+ key: 'test-api',
15
+ exposes: [
16
+ {
17
+ key: 'users',
18
+ kind: ExposedEntityKind,
19
+ entity: { key: user.key },
20
+ resourcePath: '/users/{id}',
21
+ collectionPath: '/users',
22
+ hasCollection: true,
23
+ isRoot: true,
24
+ actions: [
25
+ { kind: 'list' },
26
+ { kind: 'create' },
27
+ { kind: 'search' },
28
+ { kind: 'read' },
29
+ { kind: 'update', allowedMethods: ['PUT', 'PATCH'] } as UpdateActionSchema,
30
+ { kind: 'delete' },
31
+ ],
32
+ },
33
+ ],
34
+ },
35
+ domain.toJSON()
36
+ )
32
37
 
33
38
  const generator = new RuntimeModelGenerator(apiModel)
34
39
  const result = await generator.generate()
35
40
 
36
41
  assert.equal(result.key, 'test-api')
37
- assert.deepEqual(result.routingMap['GET']![0], {
42
+ assert.deepEqual(result.routingMap['GET'][0], {
38
43
  path: '/users',
39
- lookup: { exposedEntityKey: 'users', actionKind: 'list' },
44
+ lookup: {
45
+ exposedEntityKey: 'users',
46
+ exposedEntityName: 'user',
47
+ actionKind: 'list',
48
+ params: [],
49
+ },
40
50
  })
41
51
 
42
- assert.deepEqual(result.routingMap['GET']![1], {
52
+ assert.deepEqual(result.routingMap['GET'][1], {
43
53
  path: '/users/{id}',
44
- lookup: { exposedEntityKey: 'users', actionKind: 'read' },
54
+ lookup: {
55
+ exposedEntityKey: 'users',
56
+ exposedEntityName: 'user',
57
+ actionKind: 'read',
58
+ params: [
59
+ {
60
+ paramName: 'id',
61
+ exposedEntityKey: 'users',
62
+ exposedEntityName: 'user',
63
+ propertyName: 'id',
64
+ },
65
+ ],
66
+ },
45
67
  })
46
68
 
47
- assert.deepEqual(result.routingMap['POST']![0], {
69
+ assert.deepEqual(result.routingMap['POST'][0], {
48
70
  path: '/users',
49
- lookup: { exposedEntityKey: 'users', actionKind: 'create' },
71
+ lookup: {
72
+ exposedEntityKey: 'users',
73
+ exposedEntityName: 'user',
74
+ actionKind: 'create',
75
+ params: [],
76
+ },
50
77
  })
51
78
 
52
- assert.deepEqual(result.routingMap['POST']![1], {
79
+ assert.deepEqual(result.routingMap['POST'][1], {
53
80
  path: '/users/search',
54
- lookup: { exposedEntityKey: 'users', actionKind: 'search' },
81
+ lookup: {
82
+ exposedEntityKey: 'users',
83
+ exposedEntityName: 'user',
84
+ actionKind: 'search',
85
+ params: [],
86
+ },
55
87
  })
56
88
 
57
- assert.deepEqual(result.routingMap['PUT']![0], {
89
+ assert.deepEqual(result.routingMap['PUT'][0], {
58
90
  path: '/users/{id}',
59
- lookup: { exposedEntityKey: 'users', actionKind: 'update' },
91
+ lookup: {
92
+ exposedEntityKey: 'users',
93
+ exposedEntityName: 'user',
94
+ actionKind: 'update',
95
+ params: [
96
+ {
97
+ paramName: 'id',
98
+ exposedEntityKey: 'users',
99
+ exposedEntityName: 'user',
100
+ propertyName: 'id',
101
+ },
102
+ ],
103
+ },
60
104
  })
61
105
 
62
- assert.deepEqual(result.routingMap['PATCH']![0], {
106
+ assert.deepEqual(result.routingMap['PATCH'][0], {
63
107
  path: '/users/{id}',
64
- lookup: { exposedEntityKey: 'users', actionKind: 'update' },
108
+ lookup: {
109
+ exposedEntityKey: 'users',
110
+ exposedEntityName: 'user',
111
+ actionKind: 'update',
112
+ params: [
113
+ {
114
+ paramName: 'id',
115
+ exposedEntityKey: 'users',
116
+ exposedEntityName: 'user',
117
+ propertyName: 'id',
118
+ },
119
+ ],
120
+ },
65
121
  })
66
122
 
67
- assert.deepEqual(result.routingMap['DELETE']![0], {
123
+ assert.deepEqual(result.routingMap['DELETE'][0], {
68
124
  path: '/users/{id}',
69
- lookup: { exposedEntityKey: 'users', actionKind: 'delete' },
125
+ lookup: {
126
+ exposedEntityKey: 'users',
127
+ exposedEntityName: 'user',
128
+ actionKind: 'delete',
129
+ params: [
130
+ {
131
+ paramName: 'id',
132
+ exposedEntityKey: 'users',
133
+ exposedEntityName: 'user',
134
+ propertyName: 'id',
135
+ },
136
+ ],
137
+ },
70
138
  })
71
139
  }).tags(['@modeling', '@generator'])
72
140
 
73
141
  test('generates routes from an ApiModelSchema object', async ({ assert }) => {
74
- const schema = {
75
- kind: ApiModelKind,
142
+ const { domain, post } = blogDomain()
143
+ const schema: Partial<ApiModelSchema> = {
76
144
  key: 'test-api-schema',
77
145
  exposes: [
78
146
  {
79
- key: 'posts',
80
147
  kind: ExposedEntityKind,
81
- entity: { key: 'post' },
148
+ key: 'posts',
149
+ entity: { key: post.key },
82
150
  resourcePath: '/posts/{id}',
83
151
  collectionPath: '/posts',
84
152
  hasCollection: true,
@@ -88,32 +156,43 @@ test.group('RuntimeModelGenerator', () => {
88
156
  ],
89
157
  }
90
158
 
91
- const generator = new RuntimeModelGenerator(schema as any)
159
+ const am = new ApiModel(schema, domain.toJSON())
160
+ const generator = new RuntimeModelGenerator(am)
92
161
  const result = await generator.generate()
93
162
 
94
163
  assert.equal(result.key, 'test-api-schema')
95
164
  assert.lengthOf(result.routingMap['GET'] || [], 1)
96
165
  assert.deepEqual(result.routingMap['GET']![0], {
97
166
  path: '/posts',
98
- lookup: { exposedEntityKey: 'posts', actionKind: 'list' },
167
+ lookup: {
168
+ exposedEntityKey: 'posts',
169
+ exposedEntityName: 'posts',
170
+ actionKind: 'list',
171
+ params: [],
172
+ },
99
173
  })
100
174
  }).tags(['@modeling', '@generator'])
101
175
 
102
176
  test('handles entities without collection paths safely', async ({ assert }) => {
103
- const apiModel = new ApiModel({
104
- key: 'test-api',
105
- exposes: [
106
- {
107
- key: 'singleton',
108
- kind: ExposedEntityKind,
109
- entity: { key: 'config' },
110
- resourcePath: '/singleton',
111
- hasCollection: false,
112
- isRoot: true,
113
- actions: [{ kind: 'list' }, { kind: 'create' }, { kind: 'search' }],
114
- },
115
- ],
116
- })
177
+ const { domain, config } = singletonDomain()
178
+
179
+ const apiModel = new ApiModel(
180
+ {
181
+ key: 'test-api',
182
+ exposes: [
183
+ {
184
+ key: 'singleton',
185
+ kind: ExposedEntityKind,
186
+ entity: { key: config.key },
187
+ resourcePath: '/singleton',
188
+ hasCollection: false,
189
+ isRoot: true,
190
+ actions: [{ kind: 'list' }, { kind: 'create' }, { kind: 'search' }],
191
+ },
192
+ ],
193
+ },
194
+ domain.toJSON()
195
+ )
117
196
 
118
197
  const generator = new RuntimeModelGenerator(apiModel)
119
198
  const result = await generator.generate()
@@ -123,70 +202,229 @@ test.group('RuntimeModelGenerator', () => {
123
202
  }).tags(['@modeling', '@generator'])
124
203
 
125
204
  test('generates routes for nested entities (sub-endpoints)', async ({ assert }) => {
126
- const apiModel = new ApiModel({
127
- key: 'test-api',
128
- exposes: [
129
- {
130
- key: 'users',
131
- kind: ExposedEntityKind,
132
- entity: { key: 'user' },
133
- resourcePath: '/users/{userId}',
134
- collectionPath: '/users',
135
- hasCollection: true,
136
- isRoot: true,
137
- actions: [{ kind: 'read' }],
138
- },
139
- {
140
- key: 'user-posts',
141
- kind: ExposedEntityKind,
142
- entity: { key: 'post' },
143
- resourcePath: '/posts/{postId}',
144
- collectionPath: '/posts',
145
- hasCollection: true,
146
- isRoot: false,
147
- parent: { key: 'users', association: { key: 'user-posts' } },
148
- actions: [
149
- { kind: 'list' },
150
- { kind: 'read' },
151
- { kind: 'create' },
152
- { kind: 'update', allowedMethods: ['PATCH'] } as UpdateActionSchema,
153
- { kind: 'delete' },
154
- ],
155
- },
156
- ],
157
- })
205
+ const domain = new DataDomain({ info: { name: 'Testing Domain', version: '1.0.0' } })
206
+ const model = domain.addModel({ info: { name: 'Container model' } })
207
+ const user = model.addEntity({ info: { name: 'user' } })
208
+ user.addProperty({ info: { name: 'id' }, primary: true })
209
+ const post = model.addEntity({ info: { name: 'post' } })
210
+ post.addProperty({ info: { name: 'id' }, primary: true })
211
+ const userPostAssoc = user.addAssociation({ info: { name: 'user-post' }, targets: [{ key: post.key }] })
212
+ const apiModel = new ApiModel(
213
+ {
214
+ key: 'test-api',
215
+ exposes: [
216
+ {
217
+ key: 'users',
218
+ kind: ExposedEntityKind,
219
+ entity: { key: user.key },
220
+ resourcePath: '/users/{userId}',
221
+ collectionPath: '/users',
222
+ hasCollection: true,
223
+ isRoot: true,
224
+ actions: [{ kind: 'read' }],
225
+ },
226
+ {
227
+ key: 'user-posts',
228
+ kind: ExposedEntityKind,
229
+ entity: { key: post.key },
230
+ resourcePath: '/posts/{postId}',
231
+ collectionPath: '/posts',
232
+ hasCollection: true,
233
+ isRoot: false,
234
+ parent: { key: 'users', association: { key: userPostAssoc.key } },
235
+ actions: [
236
+ { kind: 'list' },
237
+ { kind: 'read' },
238
+ { kind: 'create' },
239
+ { kind: 'update', allowedMethods: ['PATCH'] } as UpdateActionSchema,
240
+ { kind: 'delete' },
241
+ ],
242
+ },
243
+ ],
244
+ },
245
+ domain.toJSON()
246
+ )
158
247
 
159
248
  const generator = new RuntimeModelGenerator(apiModel)
160
249
  const result = await generator.generate()
161
250
 
162
- assert.deepInclude(result.routingMap['GET']!, {
163
- path: '/users/{userId}',
164
- lookup: { exposedEntityKey: 'users', actionKind: 'read' },
165
- })
251
+ const GET = result.routingMap['GET']
166
252
 
167
- assert.deepInclude(result.routingMap['GET']!, {
168
- path: '/users/{userId}/posts',
169
- lookup: { exposedEntityKey: 'user-posts', actionKind: 'list' },
170
- })
253
+ const getUser = GET.find((route) => route.path === '/users/{userId}')
254
+ assert.ok(getUser, 'User route should exist')
255
+ assert.deepInclude(
256
+ getUser,
257
+ {
258
+ path: '/users/{userId}',
259
+ lookup: {
260
+ exposedEntityKey: 'users',
261
+ exposedEntityName: 'user',
262
+ actionKind: 'read',
263
+ params: [
264
+ {
265
+ paramName: 'userId',
266
+ exposedEntityKey: 'users',
267
+ exposedEntityName: 'user',
268
+ propertyName: 'id',
269
+ },
270
+ ],
271
+ },
272
+ },
273
+ 'User route should have correct lookup'
274
+ )
171
275
 
172
- assert.deepInclude(result.routingMap['GET']!, {
173
- path: '/users/{userId}/posts/{postId}',
174
- lookup: { exposedEntityKey: 'user-posts', actionKind: 'read' },
175
- })
276
+ const getUserPosts = GET.find((route) => route.path === '/users/{userId}/posts')
277
+ assert.ok(getUserPosts, 'User posts route should exist')
278
+ assert.deepInclude(
279
+ getUserPosts,
280
+ {
281
+ path: '/users/{userId}/posts',
282
+ lookup: {
283
+ exposedEntityKey: 'user-posts',
284
+ exposedEntityName: 'post',
285
+ actionKind: 'list',
286
+ params: [
287
+ // parent only
288
+ {
289
+ paramName: 'userId',
290
+ exposedEntityKey: 'users',
291
+ exposedEntityName: 'user',
292
+ propertyName: 'id',
293
+ associationName: 'user-post',
294
+ },
295
+ ],
296
+ },
297
+ },
298
+ 'User posts route should have correct lookup'
299
+ )
176
300
 
177
- assert.deepInclude(result.routingMap['POST']!, {
178
- path: '/users/{userId}/posts',
179
- lookup: { exposedEntityKey: 'user-posts', actionKind: 'create' },
180
- })
301
+ const getUserPost = GET.find((route) => route.path === '/users/{userId}/posts/{postId}')
302
+ assert.ok(getUserPost, 'User post route should exist')
303
+ assert.deepInclude(
304
+ getUserPost,
305
+ {
306
+ path: '/users/{userId}/posts/{postId}',
307
+ lookup: {
308
+ exposedEntityKey: 'user-posts',
309
+ exposedEntityName: 'post',
310
+ actionKind: 'read',
311
+ params: [
312
+ {
313
+ // own, this is correct, because the {postId} is in the path
314
+ paramName: 'postId',
315
+ exposedEntityKey: 'user-posts',
316
+ exposedEntityName: 'post',
317
+ propertyName: 'id',
318
+ },
319
+ // parent
320
+ {
321
+ paramName: 'userId',
322
+ exposedEntityKey: 'users',
323
+ exposedEntityName: 'user',
324
+ propertyName: 'id',
325
+ associationName: 'user-post',
326
+ },
327
+ ],
328
+ },
329
+ },
330
+ 'User post route should have correct lookup'
331
+ )
181
332
 
182
- assert.deepInclude(result.routingMap['PATCH']!, {
183
- path: '/users/{userId}/posts/{postId}',
184
- lookup: { exposedEntityKey: 'user-posts', actionKind: 'update' },
185
- })
333
+ const POST = result.routingMap['POST']
186
334
 
187
- assert.deepInclude(result.routingMap['DELETE']!, {
188
- path: '/users/{userId}/posts/{postId}',
189
- lookup: { exposedEntityKey: 'user-posts', actionKind: 'delete' },
190
- })
335
+ // there's only one POST in this API.
336
+ const postPost = POST.find((route) => route.path === '/users/{userId}/posts')
337
+ assert.ok(postPost, 'POST post route should exist')
338
+ assert.deepInclude(
339
+ postPost,
340
+ {
341
+ path: '/users/{userId}/posts',
342
+ lookup: {
343
+ exposedEntityKey: 'user-posts',
344
+ exposedEntityName: 'post',
345
+ actionKind: 'create',
346
+ params: [
347
+ // parent
348
+ {
349
+ paramName: 'userId',
350
+ exposedEntityKey: 'users',
351
+ exposedEntityName: 'user',
352
+ propertyName: 'id',
353
+ associationName: 'user-post',
354
+ },
355
+ ],
356
+ },
357
+ },
358
+ 'POST post route should have correct lookup'
359
+ )
360
+
361
+ const PATCH = result.routingMap['PATCH']
362
+
363
+ // there's only one PATCH in this API.
364
+ const patchPost = PATCH.find((route) => route.path === '/users/{userId}/posts/{postId}')
365
+ assert.ok(patchPost, 'PATCH post route should exist')
366
+ assert.deepInclude(
367
+ patchPost,
368
+ {
369
+ path: '/users/{userId}/posts/{postId}',
370
+ lookup: {
371
+ exposedEntityKey: 'user-posts',
372
+ exposedEntityName: 'post',
373
+ actionKind: 'update',
374
+ params: [
375
+ {
376
+ // own, this is correct, because the {postId} is in the path
377
+ paramName: 'postId',
378
+ exposedEntityKey: 'user-posts',
379
+ exposedEntityName: 'post',
380
+ propertyName: 'id',
381
+ },
382
+ // parent
383
+ {
384
+ paramName: 'userId',
385
+ exposedEntityKey: 'users',
386
+ exposedEntityName: 'user',
387
+ propertyName: 'id',
388
+ associationName: 'user-post',
389
+ },
390
+ ],
391
+ },
392
+ },
393
+ 'PATCH post route should have correct lookup'
394
+ )
395
+
396
+ // there's only one DELETE in this API.
397
+ const DELETE = result.routingMap['DELETE']
398
+ const deletePost = DELETE.find((route) => route.path === '/users/{userId}/posts/{postId}')
399
+ assert.ok(deletePost, 'DELETE post route should exist')
400
+ assert.deepInclude(
401
+ deletePost,
402
+ {
403
+ path: '/users/{userId}/posts/{postId}',
404
+ lookup: {
405
+ exposedEntityKey: 'user-posts',
406
+ exposedEntityName: 'post',
407
+ actionKind: 'delete',
408
+ params: [
409
+ {
410
+ // own, this is correct, because the {postId} is in the path
411
+ paramName: 'postId',
412
+ exposedEntityKey: 'user-posts',
413
+ exposedEntityName: 'post',
414
+ propertyName: 'id',
415
+ },
416
+ // parent
417
+ {
418
+ paramName: 'userId',
419
+ exposedEntityKey: 'users',
420
+ exposedEntityName: 'user',
421
+ propertyName: 'id',
422
+ associationName: 'user-post',
423
+ },
424
+ ],
425
+ },
426
+ },
427
+ 'DELETE post route should have correct lookup'
428
+ )
191
429
  }).tags(['@modeling', '@generator'])
192
430
  })