@api-client/core 0.20.5 → 0.20.7

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 (55) hide show
  1. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  2. package/build/src/modeling/ApiModel.js +9 -2
  3. package/build/src/modeling/ApiModel.js.map +1 -1
  4. package/build/src/modeling/DataDomain.d.ts +9 -3
  5. package/build/src/modeling/DataDomain.d.ts.map +1 -1
  6. package/build/src/modeling/DataDomain.js +25 -6
  7. package/build/src/modeling/DataDomain.js.map +1 -1
  8. package/build/src/modeling/DependentModel.d.ts +4 -0
  9. package/build/src/modeling/DependentModel.d.ts.map +1 -1
  10. package/build/src/modeling/DependentModel.js.map +1 -1
  11. package/build/src/modeling/DomainEntity.d.ts +20 -0
  12. package/build/src/modeling/DomainEntity.d.ts.map +1 -1
  13. package/build/src/modeling/DomainEntity.js +93 -0
  14. package/build/src/modeling/DomainEntity.js.map +1 -1
  15. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  16. package/build/src/modeling/ExposedEntity.js +55 -4
  17. package/build/src/modeling/ExposedEntity.js.map +1 -1
  18. package/build/src/modeling/RuntimeApiModel.d.ts +22 -0
  19. package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
  20. package/build/src/modeling/RuntimeApiModel.js +108 -3
  21. package/build/src/modeling/RuntimeApiModel.js.map +1 -1
  22. package/build/src/modeling/generators/RuntimeModelGenerator.d.ts +15 -0
  23. package/build/src/modeling/generators/RuntimeModelGenerator.d.ts.map +1 -0
  24. package/build/src/modeling/generators/RuntimeModelGenerator.js +78 -0
  25. package/build/src/modeling/generators/RuntimeModelGenerator.js.map +1 -0
  26. package/build/src/modeling/helpers/endpointHelpers.d.ts +6 -1
  27. package/build/src/modeling/helpers/endpointHelpers.d.ts.map +1 -1
  28. package/build/src/modeling/helpers/endpointHelpers.js +43 -4
  29. package/build/src/modeling/helpers/endpointHelpers.js.map +1 -1
  30. package/build/src/modeling/types.d.ts +15 -0
  31. package/build/src/modeling/types.d.ts.map +1 -1
  32. package/build/src/modeling/types.js.map +1 -1
  33. package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -1
  34. package/build/src/modeling/validation/api_model_rules.js +17 -0
  35. package/build/src/modeling/validation/api_model_rules.js.map +1 -1
  36. package/build/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +3 -3
  38. package/src/modeling/ApiModel.ts +8 -2
  39. package/src/modeling/DataDomain.ts +27 -7
  40. package/src/modeling/DependentModel.ts +4 -0
  41. package/src/modeling/DomainEntity.ts +100 -0
  42. package/src/modeling/ExposedEntity.ts +62 -4
  43. package/src/modeling/RuntimeApiModel.ts +131 -4
  44. package/src/modeling/generators/RuntimeModelGenerator.ts +79 -0
  45. package/src/modeling/helpers/endpointHelpers.ts +51 -4
  46. package/src/modeling/types.ts +16 -0
  47. package/src/modeling/validation/api_model_rules.ts +19 -0
  48. package/tests/unit/modeling/RuntimeApiModel.spec.ts +108 -3
  49. package/tests/unit/modeling/data_domain_entities.spec.ts +56 -0
  50. package/tests/unit/modeling/data_domain_serialization.spec.ts +11 -11
  51. package/tests/unit/modeling/domain_entity_associations.spec.ts +63 -0
  52. package/tests/unit/modeling/exposed_entity.spec.ts +95 -0
  53. package/tests/unit/modeling/generators/RuntimeModelGenerator.spec.ts +192 -0
  54. package/tests/unit/modeling/helpers/endpointHelpers.spec.ts +10 -3
  55. package/tests/unit/modeling/validation/api_model_rules.spec.ts +35 -0
@@ -0,0 +1,192 @@
1
+ import { test } from '@japa/runner'
2
+ import { ApiModel } from '../../../../src/modeling/ApiModel.js'
3
+ import { ExposedEntityKind } from '../../../../src/models/index.js'
4
+ import { RuntimeModelGenerator } from '../../../../src/modeling/generators/RuntimeModelGenerator.js'
5
+ import { ApiModelKind } from '../../../../src/models/index.js'
6
+ import type { UpdateActionSchema } from '../../../../src/modeling/actions/index.js'
7
+
8
+ test.group('RuntimeModelGenerator', () => {
9
+ 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
+ })
32
+
33
+ const generator = new RuntimeModelGenerator(apiModel)
34
+ const result = await generator.generate()
35
+
36
+ assert.equal(result.key, 'test-api')
37
+ assert.deepEqual(result.routingMap['GET']![0], {
38
+ path: '/users',
39
+ lookup: { exposedEntityKey: 'users', actionKind: 'list' },
40
+ })
41
+
42
+ assert.deepEqual(result.routingMap['GET']![1], {
43
+ path: '/users/{id}',
44
+ lookup: { exposedEntityKey: 'users', actionKind: 'read' },
45
+ })
46
+
47
+ assert.deepEqual(result.routingMap['POST']![0], {
48
+ path: '/users',
49
+ lookup: { exposedEntityKey: 'users', actionKind: 'create' },
50
+ })
51
+
52
+ assert.deepEqual(result.routingMap['POST']![1], {
53
+ path: '/users/search',
54
+ lookup: { exposedEntityKey: 'users', actionKind: 'search' },
55
+ })
56
+
57
+ assert.deepEqual(result.routingMap['PUT']![0], {
58
+ path: '/users/{id}',
59
+ lookup: { exposedEntityKey: 'users', actionKind: 'update' },
60
+ })
61
+
62
+ assert.deepEqual(result.routingMap['PATCH']![0], {
63
+ path: '/users/{id}',
64
+ lookup: { exposedEntityKey: 'users', actionKind: 'update' },
65
+ })
66
+
67
+ assert.deepEqual(result.routingMap['DELETE']![0], {
68
+ path: '/users/{id}',
69
+ lookup: { exposedEntityKey: 'users', actionKind: 'delete' },
70
+ })
71
+ }).tags(['@modeling', '@generator'])
72
+
73
+ test('generates routes from an ApiModelSchema object', async ({ assert }) => {
74
+ const schema = {
75
+ kind: ApiModelKind,
76
+ key: 'test-api-schema',
77
+ exposes: [
78
+ {
79
+ key: 'posts',
80
+ kind: ExposedEntityKind,
81
+ entity: { key: 'post' },
82
+ resourcePath: '/posts/{id}',
83
+ collectionPath: '/posts',
84
+ hasCollection: true,
85
+ isRoot: true,
86
+ actions: [{ kind: 'list' }],
87
+ },
88
+ ],
89
+ }
90
+
91
+ const generator = new RuntimeModelGenerator(schema as any)
92
+ const result = await generator.generate()
93
+
94
+ assert.equal(result.key, 'test-api-schema')
95
+ assert.lengthOf(result.routingMap['GET'] || [], 1)
96
+ assert.deepEqual(result.routingMap['GET']![0], {
97
+ path: '/posts',
98
+ lookup: { exposedEntityKey: 'posts', actionKind: 'list' },
99
+ })
100
+ }).tags(['@modeling', '@generator'])
101
+
102
+ 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
+ })
117
+
118
+ const generator = new RuntimeModelGenerator(apiModel)
119
+ const result = await generator.generate()
120
+
121
+ // Without a collection path, list, create, search should be ignored.
122
+ assert.deepEqual(result.routingMap, {})
123
+ }).tags(['@modeling', '@generator'])
124
+
125
+ 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
+ })
158
+
159
+ const generator = new RuntimeModelGenerator(apiModel)
160
+ const result = await generator.generate()
161
+
162
+ assert.deepInclude(result.routingMap['GET']!, {
163
+ path: '/users/{userId}',
164
+ lookup: { exposedEntityKey: 'users', actionKind: 'read' },
165
+ })
166
+
167
+ assert.deepInclude(result.routingMap['GET']!, {
168
+ path: '/users/{userId}/posts',
169
+ lookup: { exposedEntityKey: 'user-posts', actionKind: 'list' },
170
+ })
171
+
172
+ assert.deepInclude(result.routingMap['GET']!, {
173
+ path: '/users/{userId}/posts/{postId}',
174
+ lookup: { exposedEntityKey: 'user-posts', actionKind: 'read' },
175
+ })
176
+
177
+ assert.deepInclude(result.routingMap['POST']!, {
178
+ path: '/users/{userId}/posts',
179
+ lookup: { exposedEntityKey: 'user-posts', actionKind: 'create' },
180
+ })
181
+
182
+ assert.deepInclude(result.routingMap['PATCH']!, {
183
+ path: '/users/{userId}/posts/{postId}',
184
+ lookup: { exposedEntityKey: 'user-posts', actionKind: 'update' },
185
+ })
186
+
187
+ assert.deepInclude(result.routingMap['DELETE']!, {
188
+ path: '/users/{userId}/posts/{postId}',
189
+ lookup: { exposedEntityKey: 'user-posts', actionKind: 'delete' },
190
+ })
191
+ }).tags(['@modeling', '@generator'])
192
+ })
@@ -2,9 +2,16 @@ import { test } from '@japa/runner'
2
2
  import * as helpers from '../../../../src/modeling/helpers/endpointHelpers.js'
3
3
 
4
4
  test.group('endpointHelpers', () => {
5
- test('paramNameFor extracts last key', ({ assert }) => {
5
+ test('paramNameFor handles various arbitrary strings', ({ assert }) => {
6
6
  assert.equal(helpers.paramNameFor('product'), 'productId')
7
- assert.equal(helpers.paramNameFor('domain:product'), 'productId')
8
- assert.equal(helpers.paramNameFor('a:b:c'), 'cId')
7
+ assert.equal(helpers.paramNameFor('user_post'), 'userPostId')
8
+ assert.equal(helpers.paramNameFor('UserPost'), 'userPostId')
9
+ assert.equal(helpers.paramNameFor('USER_POST'), 'userPostId')
10
+ assert.equal(helpers.paramNameFor('user-post'), 'userPostId')
11
+ assert.equal(helpers.paramNameFor('User Post! '), 'userPostId')
12
+ assert.equal(helpers.paramNameFor('123item'), '_123itemId')
13
+ assert.equal(helpers.paramNameFor('XMLHttp'), 'xmlHttpId')
14
+ assert.throws(() => helpers.paramNameFor('!@#$'), 'Cannot generate a valid parameter name from "!@#$".')
15
+ assert.throws(() => helpers.paramNameFor(''), 'Cannot generate parameter name from an empty string.')
9
16
  })
10
17
  })
@@ -249,6 +249,41 @@ test.group('ApiModel Validation', () => {
249
249
  assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_INVALID_RESOURCE_PATH_FORMAT'))
250
250
  })
251
251
 
252
+ test('validateExposedEntity - catches duplicate path parameters in branch', ({ assert }) => {
253
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
254
+ const model = new ApiModel({}, domain)
255
+
256
+ // Parent exposure with parameter {id}
257
+ const parentExposure = new ExposedEntity(model, {
258
+ key: 'parent',
259
+ entity: { key: 'fakeParent' },
260
+ hasCollection: true,
261
+ collectionPath: '/parents',
262
+ resourcePath: '/parents/{id}',
263
+ isRoot: true,
264
+ actions: [],
265
+ })
266
+
267
+ // Child exposure with the same parameter {id}
268
+ const childExposure = new ExposedEntity(model, {
269
+ key: 'child',
270
+ entity: { key: 'fakeChild' },
271
+ hasCollection: true,
272
+ collectionPath: '/children',
273
+ resourcePath: '/children/{id}',
274
+ parent: { key: 'parent', association: { key: 'toChildren' } },
275
+ actions: [],
276
+ })
277
+
278
+ model.exposes = new Map([
279
+ [parentExposure.key, parentExposure],
280
+ [childExposure.key, childExposure],
281
+ ])
282
+
283
+ const issues = validateExposedEntity(childExposure, model)
284
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_DUPLICATE_PATH_PARAMETER'))
285
+ })
286
+
252
287
  test('validateExposedEntity - List needs pagination contract', ({ assert }) => {
253
288
  const domain = new DataDomain({ info: { version: '1.0.0' } })
254
289
  const model = new ApiModel({}, domain)