@api-client/core 0.19.18 → 0.19.20

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 (154) hide show
  1. package/build/src/authorization/Utils.js +3 -3
  2. package/build/src/authorization/Utils.js.map +1 -1
  3. package/build/src/browser.d.ts +1 -1
  4. package/build/src/browser.d.ts.map +1 -1
  5. package/build/src/browser.js.map +1 -1
  6. package/build/src/index.d.ts +1 -1
  7. package/build/src/index.d.ts.map +1 -1
  8. package/build/src/index.js.map +1 -1
  9. package/build/src/mocking/lib/Organization.d.ts +5 -1
  10. package/build/src/mocking/lib/Organization.d.ts.map +1 -1
  11. package/build/src/mocking/lib/Organization.js +17 -0
  12. package/build/src/mocking/lib/Organization.js.map +1 -1
  13. package/build/src/modeling/ApiModel.d.ts +16 -5
  14. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  15. package/build/src/modeling/ApiModel.js +17 -2
  16. package/build/src/modeling/ApiModel.js.map +1 -1
  17. package/build/src/modeling/ApiValidation.d.ts.map +1 -1
  18. package/build/src/modeling/ApiValidation.js +2 -1
  19. package/build/src/modeling/ApiValidation.js.map +1 -1
  20. package/build/src/modeling/DomainProperty.d.ts +12 -0
  21. package/build/src/modeling/DomainProperty.d.ts.map +1 -1
  22. package/build/src/modeling/DomainProperty.js +23 -28
  23. package/build/src/modeling/DomainProperty.js.map +1 -1
  24. package/build/src/modeling/DomainSerialization.js +1 -1
  25. package/build/src/modeling/DomainSerialization.js.map +1 -1
  26. package/build/src/modeling/ExposedEntity.d.ts +15 -1
  27. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  28. package/build/src/modeling/ExposedEntity.js +42 -4
  29. package/build/src/modeling/ExposedEntity.js.map +1 -1
  30. package/build/src/modeling/actions/Action.d.ts.map +1 -1
  31. package/build/src/modeling/actions/Action.js +1 -0
  32. package/build/src/modeling/actions/Action.js.map +1 -1
  33. package/build/src/modeling/actions/ListAction.d.ts +3 -17
  34. package/build/src/modeling/actions/ListAction.d.ts.map +1 -1
  35. package/build/src/modeling/actions/ListAction.js +18 -38
  36. package/build/src/modeling/actions/ListAction.js.map +1 -1
  37. package/build/src/modeling/actions/SearchAction.d.ts +4 -4
  38. package/build/src/modeling/actions/SearchAction.d.ts.map +1 -1
  39. package/build/src/modeling/actions/SearchAction.js +16 -13
  40. package/build/src/modeling/actions/SearchAction.js.map +1 -1
  41. package/build/src/modeling/generators/oas_312/OasGenerator.d.ts +32 -0
  42. package/build/src/modeling/generators/oas_312/OasGenerator.d.ts.map +1 -0
  43. package/build/src/modeling/generators/oas_312/OasGenerator.js +1452 -0
  44. package/build/src/modeling/generators/oas_312/OasGenerator.js.map +1 -0
  45. package/build/src/modeling/generators/oas_312/OasSchemaGenerator.d.ts +27 -0
  46. package/build/src/modeling/generators/oas_312/OasSchemaGenerator.d.ts.map +1 -0
  47. package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js +295 -0
  48. package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js.map +1 -0
  49. package/build/src/modeling/generators/oas_312/types.d.ts +1010 -0
  50. package/build/src/modeling/generators/oas_312/types.d.ts.map +1 -0
  51. package/build/src/modeling/generators/oas_312/types.js +2 -0
  52. package/build/src/modeling/generators/oas_312/types.js.map +1 -0
  53. package/build/src/modeling/generators/oas_320/OasGenerator.d.ts +16 -0
  54. package/build/src/modeling/generators/oas_320/OasGenerator.d.ts.map +1 -0
  55. package/build/src/modeling/generators/oas_320/OasGenerator.js +306 -0
  56. package/build/src/modeling/generators/oas_320/OasGenerator.js.map +1 -0
  57. package/build/src/modeling/generators/oas_320/OasSchemaGenerator.d.ts +25 -0
  58. package/build/src/modeling/generators/oas_320/OasSchemaGenerator.d.ts.map +1 -0
  59. package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js +237 -0
  60. package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js.map +1 -0
  61. package/build/src/modeling/generators/oas_320/types.d.ts +1219 -0
  62. package/build/src/modeling/generators/oas_320/types.d.ts.map +1 -0
  63. package/build/src/modeling/generators/oas_320/types.js +2 -0
  64. package/build/src/modeling/generators/oas_320/types.js.map +1 -0
  65. package/build/src/modeling/types.d.ts +50 -13
  66. package/build/src/modeling/types.d.ts.map +1 -1
  67. package/build/src/modeling/types.js.map +1 -1
  68. package/build/src/modeling/validation/api_model_rules.d.ts +1 -0
  69. package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -1
  70. package/build/src/modeling/validation/api_model_rules.js +105 -29
  71. package/build/src/modeling/validation/api_model_rules.js.map +1 -1
  72. package/build/src/models/ProjectRequest.d.ts.map +1 -1
  73. package/build/src/models/ProjectRequest.js +0 -4
  74. package/build/src/models/ProjectRequest.js.map +1 -1
  75. package/build/src/models/store/Organization.d.ts +13 -0
  76. package/build/src/models/store/Organization.d.ts.map +1 -1
  77. package/build/src/models/store/Organization.js.map +1 -1
  78. package/build/src/models/transformers/ArcDexieTransformer.d.ts.map +1 -1
  79. package/build/src/models/transformers/ArcDexieTransformer.js +0 -4
  80. package/build/src/models/transformers/ArcDexieTransformer.js.map +1 -1
  81. package/build/src/models/transformers/ImportUtils.js +1 -1
  82. package/build/src/models/transformers/ImportUtils.js.map +1 -1
  83. package/build/src/models/transformers/PostmanBackupTransformer.d.ts.map +1 -1
  84. package/build/src/models/transformers/PostmanBackupTransformer.js +0 -4
  85. package/build/src/models/transformers/PostmanBackupTransformer.js.map +1 -1
  86. package/build/src/runtime/constants.d.ts +7 -0
  87. package/build/src/runtime/constants.d.ts.map +1 -0
  88. package/build/src/runtime/constants.js +8 -0
  89. package/build/src/runtime/constants.js.map +1 -0
  90. package/build/src/runtime/http-engine/ntlm/Des.d.ts.map +1 -1
  91. package/build/src/runtime/http-engine/ntlm/Des.js +1 -0
  92. package/build/src/runtime/http-engine/ntlm/Des.js.map +1 -1
  93. package/build/src/runtime/variables/EvalFunctions.d.ts.map +1 -1
  94. package/build/src/runtime/variables/EvalFunctions.js +0 -1
  95. package/build/src/runtime/variables/EvalFunctions.js.map +1 -1
  96. package/build/src/sdk/OrganizationsSdk.d.ts +17 -1
  97. package/build/src/sdk/OrganizationsSdk.d.ts.map +1 -1
  98. package/build/src/sdk/OrganizationsSdk.js +76 -0
  99. package/build/src/sdk/OrganizationsSdk.js.map +1 -1
  100. package/build/src/sdk/RouteBuilder.d.ts +2 -0
  101. package/build/src/sdk/RouteBuilder.d.ts.map +1 -1
  102. package/build/src/sdk/RouteBuilder.js +6 -0
  103. package/build/src/sdk/RouteBuilder.js.map +1 -1
  104. package/build/src/sdk/SdkMock.d.ts +12 -0
  105. package/build/src/sdk/SdkMock.d.ts.map +1 -1
  106. package/build/src/sdk/SdkMock.js +32 -0
  107. package/build/src/sdk/SdkMock.js.map +1 -1
  108. package/build/tsconfig.tsbuildinfo +1 -1
  109. package/eslint.config.js +6 -0
  110. package/package.json +3 -1
  111. package/src/authorization/Utils.ts +3 -3
  112. package/src/mocking/lib/Organization.ts +22 -1
  113. package/src/modeling/ApiModel.ts +23 -8
  114. package/src/modeling/ApiValidation.ts +2 -0
  115. package/src/modeling/DomainProperty.ts +22 -18
  116. package/src/modeling/DomainSerialization.ts +1 -1
  117. package/src/modeling/ExposedEntity.ts +44 -4
  118. package/src/modeling/actions/Action.ts +1 -0
  119. package/src/modeling/actions/ListAction.ts +12 -30
  120. package/src/modeling/actions/SearchAction.ts +11 -8
  121. package/src/modeling/generators/oas_312/OasGenerator.ts +1685 -0
  122. package/src/modeling/generators/oas_312/OasSchemaGenerator.ts +322 -0
  123. package/src/modeling/generators/oas_312/types.ts +1052 -0
  124. package/src/modeling/generators/oas_320/OasGenerator.ts +359 -0
  125. package/src/modeling/generators/oas_320/OasSchemaGenerator.ts +255 -0
  126. package/src/modeling/generators/oas_320/types.ts +1259 -0
  127. package/src/modeling/types.ts +55 -22
  128. package/src/modeling/validation/api_model_rules.ts +103 -32
  129. package/src/models/ProjectRequest.ts +0 -4
  130. package/src/models/store/Organization.ts +14 -0
  131. package/src/models/transformers/ArcDexieTransformer.ts +0 -4
  132. package/src/models/transformers/ImportUtils.ts +1 -1
  133. package/src/models/transformers/PostmanBackupTransformer.ts +0 -5
  134. package/src/runtime/constants.ts +9 -0
  135. package/src/runtime/http-engine/ntlm/Des.ts +1 -0
  136. package/src/runtime/variables/EvalFunctions.ts +0 -1
  137. package/src/sdk/OrganizationsSdk.ts +81 -1
  138. package/src/sdk/RouteBuilder.ts +8 -0
  139. package/src/sdk/SdkMock.ts +50 -0
  140. package/tests/test-utils.ts +6 -2
  141. package/tests/unit/decorators/observed.spec.ts +8 -24
  142. package/tests/unit/decorators/observed_recursive.spec.ts +0 -1
  143. package/tests/unit/events/EventsTestHelpers.ts +0 -1
  144. package/tests/unit/events/events_polyfills.ts +0 -1
  145. package/tests/unit/legacy-transformers/DataTestHelper.ts +0 -2
  146. package/tests/unit/legacy-transformers/LegacyExportProcessor.spec.ts +0 -1
  147. package/tests/unit/modeling/actions/ListAction.spec.ts +9 -69
  148. package/tests/unit/modeling/actions/SearchAction.spec.ts +9 -35
  149. package/tests/unit/modeling/api_model.spec.ts +28 -0
  150. package/tests/unit/modeling/definitions/sku.spec.ts +0 -2
  151. package/tests/unit/modeling/domain_property.spec.ts +20 -1
  152. package/tests/unit/modeling/exposed_entity.spec.ts +71 -0
  153. package/tests/unit/modeling/generators/OasGenerator.spec.ts +302 -0
  154. package/tests/unit/modeling/validation/api_model_rules.spec.ts +113 -15
@@ -231,6 +231,77 @@ test.group('ExposedEntity', () => {
231
231
  assert.equal(ex.accessRule![0].type, 'allowPublic')
232
232
  }).tags(['@modeling', '@exposed-entity', '@immutability'])
233
233
 
234
+ test('initializes with paginationContract', ({ assert }) => {
235
+ const model = new ApiModel()
236
+ const contract = {
237
+ searchableFields: ['name'],
238
+ filterableFields: ['status'],
239
+ sortableFields: ['createdAt'],
240
+ }
241
+ const ex = new ExposedEntity(model, {
242
+ paginationContract: contract,
243
+ })
244
+
245
+ assert.deepEqual(ex.paginationContract, contract)
246
+ }).tags(['@modeling', '@exposed-entity'])
247
+
248
+ test('toJSON serializes paginationContract safely', ({ assert }) => {
249
+ const model = new ApiModel()
250
+ const ex = new ExposedEntity(model, {
251
+ paginationContract: { sortableFields: ['name'], filterableFields: [], searchableFields: [] },
252
+ })
253
+
254
+ const json = ex.toJSON()
255
+ assert.deepEqual(json.paginationContract, { sortableFields: ['name'], filterableFields: [], searchableFields: [] })
256
+
257
+ // Modify json to ensure immutability
258
+ json.paginationContract!.sortableFields.push('modified')
259
+ assert.lengthOf(ex.paginationContract!.sortableFields, 1)
260
+ }).tags(['@modeling', '@exposed-entity', '@serialization', '@immutability'])
261
+
262
+ test('constructor deep copies paginationContract (immutability)', ({ assert }) => {
263
+ const model = new ApiModel()
264
+ const contract = {
265
+ searchableFields: ['name'],
266
+ filterableFields: [],
267
+ sortableFields: [],
268
+ }
269
+ const ex = new ExposedEntity(model, {
270
+ paginationContract: contract,
271
+ })
272
+
273
+ contract.searchableFields.push('age')
274
+ assert.lengthOf(ex.paginationContract!.searchableFields, 1)
275
+ }).tags(['@modeling', '@exposed-entity', '@immutability'])
276
+
277
+ test('notifies change when paginationContract changes', async ({ assert }) => {
278
+ const model = new ApiModel()
279
+ const ex = new ExposedEntity(model, {})
280
+ let notified = false
281
+ ex.addEventListener('change', () => {
282
+ notified = true
283
+ })
284
+
285
+ ex.paginationContract = { sortableFields: [], filterableFields: [], searchableFields: [] }
286
+ await Promise.resolve()
287
+ assert.isTrue(notified)
288
+ }).tags(['@modeling', '@exposed-entity', '@observed'])
289
+
290
+ test('notifies change when paginationContract property mutates', async ({ assert }) => {
291
+ const model = new ApiModel()
292
+ const ex = new ExposedEntity(model, {
293
+ paginationContract: { sortableFields: [], filterableFields: [], searchableFields: [] },
294
+ })
295
+ let notified = false
296
+ ex.addEventListener('change', () => {
297
+ notified = true
298
+ })
299
+
300
+ ex.paginationContract!.sortableFields.push('name')
301
+ await Promise.resolve()
302
+ assert.isTrue(notified)
303
+ }).tags(['@modeling', '@exposed-entity', '@observed'])
304
+
234
305
  test('getAllRules() aggregates rules from entity, parent, and API', ({ assert }) => {
235
306
  const model = new ApiModel()
236
307
  model.accessRule = [new AccessRule({ type: 'allowPublic' })]
@@ -0,0 +1,302 @@
1
+ import { test } from '@japa/runner'
2
+ import { OasGenerator } from '../../../../src/modeling/generators/oas_312/OasGenerator.js'
3
+ import { ApiModel } from '../../../../src/modeling/ApiModel.js'
4
+ import { DataDomain } from '../../../../src/modeling/DataDomain.js'
5
+ import { SemanticType } from '../../../../src/modeling/Semantics.js'
6
+ import { AllowAuthenticatedAccessRule } from '../../../../src/modeling/rules/AllowAuthenticated.js'
7
+
8
+ test.group('OasGenerator', (group) => {
9
+ let domain: DataDomain
10
+ let api: ApiModel
11
+
12
+ group.each.setup(() => {
13
+ domain = new DataDomain({ info: { name: 'Test Domain', version: '1.0.0' } })
14
+ const model = domain.addModel({ info: { name: 'model1' } })
15
+ const user = model.addEntity({ info: { name: 'user' } })
16
+ user.addSemantic({ id: SemanticType.User })
17
+ user.addProperty({
18
+ type: 'string',
19
+ required: true,
20
+ info: { name: 'id' },
21
+ index: true,
22
+ primary: true,
23
+ readOnly: true,
24
+ })
25
+ const name = user.addProperty({ type: 'string', required: true, info: { name: 'name' }, search: true })
26
+ const email = user.addProperty({
27
+ type: 'string',
28
+ required: true,
29
+ index: true,
30
+ unique: true,
31
+ search: true,
32
+ info: { name: 'email' },
33
+ semantics: [{ id: SemanticType.Email }, { id: SemanticType.Username }],
34
+ })
35
+ const passwd = user.addProperty({
36
+ type: 'string',
37
+ required: true,
38
+ info: { name: 'password' },
39
+ writeOnly: true,
40
+ semantics: [{ id: SemanticType.Password }],
41
+ })
42
+ const role = user.addProperty({
43
+ type: 'string',
44
+ required: true,
45
+ info: { name: 'role' },
46
+ schema: { enum: ['admin', 'user'] },
47
+ index: true,
48
+ })
49
+ role.addSemantic({ id: SemanticType.UserRole })
50
+ user.addProperty({
51
+ type: 'string',
52
+ required: true,
53
+ info: { name: 'avatar' },
54
+ semantics: [{ id: SemanticType.ImageURL }],
55
+ })
56
+ user.addProperty({
57
+ type: 'string',
58
+ required: false,
59
+ info: { name: 'cv' },
60
+ semantics: [{ id: SemanticType.FileURL }],
61
+ })
62
+
63
+ const post = model.addEntity({
64
+ info: { name: 'post', description: 'Represents a post published by a user', displayName: 'Post' },
65
+ })
66
+ post.addProperty({
67
+ type: 'string',
68
+ required: true,
69
+ info: { name: 'id', description: 'Post ID', displayName: 'ID' },
70
+ primary: true,
71
+ readOnly: true,
72
+ })
73
+ const postTitle = post.addProperty({
74
+ type: 'string',
75
+ required: true,
76
+ info: { name: 'title', description: 'Post title', displayName: 'Title' },
77
+ semantics: [{ id: SemanticType.Title }],
78
+ schema: {
79
+ examples: ['My blog title', 'Another title'],
80
+ minimum: 5,
81
+ maximum: 255,
82
+ },
83
+ search: true,
84
+ })
85
+ const postSlug = post.addProperty({
86
+ type: 'string',
87
+ required: true,
88
+ info: {
89
+ name: 'slug',
90
+ description: 'Post slug. It is auto generated when a post is created and cannot be changed.',
91
+ displayName: 'Slug',
92
+ },
93
+ readOnly: true,
94
+ index: true,
95
+ semantics: [{ id: SemanticType.PublicUniqueName, config: {} }],
96
+ })
97
+ post.addProperty({ type: 'string', required: true, info: { name: 'content' }, search: true })
98
+ const postCreatedAt = post.addProperty({
99
+ type: 'string',
100
+ required: true,
101
+ info: { name: 'created_at' },
102
+ index: true,
103
+ semantics: [{ id: SemanticType.CreatedTimestamp }],
104
+ })
105
+ post.addProperty({
106
+ type: 'string',
107
+ required: true,
108
+ info: { name: 'updated_at' },
109
+ index: true,
110
+ semantics: [{ id: SemanticType.UpdatedTimestamp }],
111
+ })
112
+ post.addAssociation({
113
+ info: { name: 'author' },
114
+ targets: [{ key: user.key }],
115
+ multiple: false,
116
+ schema: { linked: true },
117
+ semantics: [{ id: SemanticType.ResourceOwnerIdentifier }],
118
+ })
119
+
120
+ api = new ApiModel({ key: 'api1', info: { name: 'Test API', version: '1.0.0' } }, domain)
121
+ api.license = {
122
+ name: 'BSD-3-Clause-No-Nuclear-License-2014',
123
+ url: 'https://opensource.org/licenses/BSD-3-Clause-No-Nuclear-License-2014',
124
+ }
125
+ api.contact = {
126
+ email: 'info@api.com',
127
+ name: 'API Admin',
128
+ url: 'https://api.com',
129
+ }
130
+ api.accessRule = [new AllowAuthenticatedAccessRule()]
131
+ api.session = {
132
+ properties: [email.key],
133
+ secret: 'test-secret',
134
+ cookie: {
135
+ enabled: true,
136
+ httpOnly: true,
137
+ secure: true,
138
+ kind: 'cookie',
139
+ lifetime: '30d',
140
+ name: 'sis',
141
+ sameSite: 'lax',
142
+ },
143
+ jwt: {
144
+ enabled: true,
145
+ kind: 'jwt',
146
+ lifetime: '30d',
147
+ },
148
+ }
149
+ api.authentication = {
150
+ strategy: 'UsernamePassword',
151
+ passwordKey: passwd.key,
152
+ }
153
+ api.authorization = {
154
+ strategy: 'RBAC',
155
+ roleKey: role.key,
156
+ }
157
+ api.pagination = { kind: 'cursor', defaultLimit: 50, maxLimit: 70 }
158
+ const userExposure = api.exposeEntity({ key: user.key })
159
+ userExposure.paginationContract = {
160
+ filterableFields: [role.key],
161
+ sortableFields: [name.key, email.key, role.key],
162
+ searchableFields: [name.key, email.key],
163
+ }
164
+ const postExposure = api.exposeEntity({ key: post.key })
165
+ postExposure.paginationContract = {
166
+ filterableFields: [postSlug.key, postCreatedAt.key],
167
+ sortableFields: [postSlug.key, postCreatedAt.key],
168
+ searchableFields: [postTitle.key],
169
+ }
170
+ userExposure.addAction({ kind: 'list', cacheTtl: 1800 })
171
+ userExposure.addAction({ kind: 'create', accessRule: [{ type: 'allowPublic' }] })
172
+ userExposure.addAction({ kind: 'read' })
173
+ userExposure.addAction({ kind: 'update', allowedMethods: ['PUT'] })
174
+ userExposure.addAction({ kind: 'delete', strategy: 'soft', retentionPeriod: 30 })
175
+ userExposure.addAction({ kind: 'search', maxAstDepth: 1 })
176
+ postExposure.addAction({ kind: 'list', cacheTtl: 3600 })
177
+ postExposure.addAction({ kind: 'update', allowedMethods: ['PUT', 'PATCH'] })
178
+ postExposure.addAction({ kind: 'delete', strategy: 'hard' })
179
+ postExposure.addAction({ kind: 'search', maxAstDepth: 2 })
180
+ })
181
+
182
+ test('generates basic OAS structure', async ({ assert }) => {
183
+ const generator = new OasGenerator(api)
184
+ const oas = generator.generate()
185
+
186
+ assert.equal(oas.openapi, '3.1.0')
187
+ assert.typeOf(oas.info, 'object')
188
+ assert.isObject(oas.paths)
189
+ assert.isObject(oas.components)
190
+ })
191
+
192
+ test('generates cookie auth endpoints', ({ assert }) => {
193
+ const generator = new OasGenerator(api)
194
+ const oas = generator.generate()
195
+
196
+ const components = oas.components as any
197
+ assert.deepEqual(components.securitySchemes.CookieAuth, {
198
+ type: 'apiKey',
199
+ in: 'cookie',
200
+ name: 'sis',
201
+ description: 'Session cookie obtained after authenticating',
202
+ })
203
+
204
+ const paths = oas.paths as any
205
+ assert.isObject(paths['/auth/cookie'].post)
206
+ assert.isObject(paths['/auth/cookie/logout'].post)
207
+ assert.include(paths['/auth/cookie'].post.tags, 'Authentication')
208
+ })
209
+
210
+ test('generates jwt auth endpoints', ({ assert }) => {
211
+ const generator = new OasGenerator(api)
212
+ const oas = generator.generate()
213
+
214
+ const components = oas.components as any
215
+ assert.deepEqual(components.securitySchemes.BearerAuth, {
216
+ type: 'http',
217
+ scheme: 'bearer',
218
+ bearerFormat: 'JWT',
219
+ description: 'JWT authorization obtained after trading Username/Password credentials',
220
+ })
221
+
222
+ const paths = oas.paths as any
223
+ assert.isObject(paths['/auth/token'].post)
224
+ assert.isObject(paths['/auth/token/{token}'].delete)
225
+ assert.include(paths['/auth/token'].post.tags, 'Authentication')
226
+ })
227
+
228
+ test('generates paths for exposed entity actions', ({ assert }) => {
229
+ const generator = new OasGenerator(api)
230
+ const oas = generator.generate()
231
+
232
+ const paths = oas.paths as any
233
+
234
+ // User endpoints (CRUD)
235
+ assert.isObject(paths['/users'].get, 'User list')
236
+ assert.isObject(paths['/users'].post, 'User create')
237
+ assert.isObject(paths['/users/{id}'].get, 'User read')
238
+ assert.isObject(paths['/users/{id}'].put, 'User put')
239
+ assert.isObject(paths['/users/{id}'].delete, 'User delete')
240
+
241
+ // Post endpoints (list, update, delete)
242
+ assert.isObject(paths['/posts'].get)
243
+ assert.isUndefined(paths['/posts'].post) // no create Action
244
+ assert.isUndefined(paths['/posts/{id}'].get) // no read action
245
+ assert.isObject(paths['/posts/{id}'].patch)
246
+ assert.isObject(paths['/posts/{id}'].put)
247
+ assert.isObject(paths['/posts/{id}'].delete)
248
+ })
249
+
250
+ test('generates standard error responses', ({ assert }) => {
251
+ const generator = new OasGenerator(api)
252
+ const oas = generator.generate()
253
+
254
+ const paths = oas.paths as any
255
+ const postUser = paths['/users'].post
256
+ const getUser = paths['/users'].get
257
+
258
+ assert.isObject(postUser.responses['400']) // Validation Error
259
+ assert.equal(
260
+ postUser.responses['400'].content['application/json'].schema.$ref,
261
+ '#/components/schemas/Error400BadRequestInvalidFormat'
262
+ )
263
+
264
+ // Should have 401 when using auth
265
+ assert.isObject(getUser.responses['401'])
266
+ assert.equal(
267
+ getUser.responses['401'].content['application/json'].schema.$ref,
268
+ '#/components/schemas/Error401Unauthorized'
269
+ )
270
+ })
271
+
272
+ test('generates search endpoints and AST queries', ({ assert }) => {
273
+ const generator = new OasGenerator(api)
274
+ const oas = generator.generate()
275
+
276
+ const paths = oas.paths as any
277
+ assert.isObject(paths['/users/search'].post)
278
+ assert.isObject(paths['/posts/search'].post)
279
+
280
+ assert.include(paths['/users/search'].post.tags, 'User')
281
+ assert.include(paths['/posts/search'].post.tags, 'Post')
282
+
283
+ const components = oas.components as any
284
+ assert.isObject(components.schemas.SearchWhereAST)
285
+ assert.isObject(components.schemas.SearchWhereNode)
286
+ assert.isObject(components.schemas.SearchWhereOperatorMap)
287
+ })
288
+
289
+ test('generates file upload endpoint', ({ assert }) => {
290
+ const generator = new OasGenerator(api)
291
+ const oas = generator.generate()
292
+
293
+ const paths = oas.paths as any
294
+ assert.isObject(paths['/upload'].post)
295
+ assert.include(paths['/upload'].post.tags, 'File Upload')
296
+
297
+ const requestBody = paths['/upload'].post.requestBody
298
+ assert.isObject(requestBody.content['*/*'])
299
+ assert.equal(requestBody.content['*/*'].schema.type, 'string')
300
+ assert.equal(requestBody.content['*/*'].schema.format, 'binary')
301
+ })
302
+ })
@@ -9,6 +9,7 @@ import {
9
9
  validateApiModelMetadata,
10
10
  validateExposedEntity,
11
11
  validateAction,
12
+ validateApiPagination,
12
13
  } from '../../../../src/modeling/validation/api_model_rules.js'
13
14
  import { ApiValidation } from '../../../../src/modeling/ApiValidation.js'
14
15
  import { ExposedEntity } from '../../../../src/modeling/ExposedEntity.js'
@@ -248,25 +249,60 @@ test.group('ApiModel Validation', () => {
248
249
  assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_INVALID_RESOURCE_PATH_FORMAT'))
249
250
  })
250
251
 
251
- test('validateAction - List needs pagination', ({ assert }) => {
252
- const model = new ApiModel()
253
- const exposure = new ExposedEntity(model, { entity: { key: '1' } })
254
- const action = new ListAction(exposure)
255
- // @ts-expect-error testing undefined pagination
256
- action.pagination = undefined
252
+ test('validateExposedEntity - List needs pagination contract', ({ assert }) => {
253
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
254
+ const model = new ApiModel({}, domain)
255
+ const exposure = new ExposedEntity(model, {
256
+ entity: { key: 'fake' },
257
+ hasCollection: true,
258
+ collectionPath: '/items',
259
+ resourcePath: '/items/{id}',
260
+ })
261
+ exposure.actions = [new ListAction(exposure)]
257
262
 
258
- const issues = validateAction(action, exposure, model.key)
259
- assert.isTrue(issues.some((i) => i.code === 'ACTION_LIST_MISSING_PAGINATION'))
263
+ const issues = validateExposedEntity(exposure, model)
264
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_MISSING_PAGINATION_CONTRACT'))
260
265
  })
261
266
 
262
- test('validateAction - Search needs fields', ({ assert }) => {
263
- const model = new ApiModel()
264
- const exposure = new ExposedEntity(model, { entity: { key: '1' } })
265
- const action = new SearchAction(exposure)
266
- action.fields = []
267
+ test('validateExposedEntity - Search needs fields', ({ assert }) => {
268
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
269
+ const model = new ApiModel({}, domain)
270
+ const exposure = new ExposedEntity(model, {
271
+ entity: { key: 'fake' },
272
+ hasCollection: true,
273
+ collectionPath: '/items',
274
+ resourcePath: '/items/{id}',
275
+ })
276
+ exposure.actions = [new SearchAction(exposure)]
277
+ exposure.paginationContract = {
278
+ searchableFields: [],
279
+ filterableFields: [],
280
+ sortableFields: [],
281
+ }
267
282
 
268
- const issues = validateAction(action, exposure, model.key)
269
- assert.isTrue(issues.some((i) => i.code === 'ACTION_SEARCH_MISSING_FIELDS'))
283
+ const issues = validateExposedEntity(exposure, model)
284
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_SEARCH_MISSING_FIELDS'))
285
+ })
286
+
287
+ test('validateExposedEntity - List needs filters and sorting', ({ assert }) => {
288
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
289
+ const model = new ApiModel({}, domain)
290
+ const exposure = new ExposedEntity(model, {
291
+ entity: { key: 'fake' },
292
+ hasCollection: true,
293
+ collectionPath: '/items',
294
+ resourcePath: '/items/{id}',
295
+ })
296
+ exposure.actions = [new ListAction(exposure)]
297
+ exposure.paginationContract = {
298
+ searchableFields: [],
299
+ filterableFields: [],
300
+ sortableFields: [],
301
+ }
302
+
303
+ const issues = validateExposedEntity(exposure, model)
304
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_LIST_MISSING_FILTERS'))
305
+ assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_LIST_MISSING_SORTING'))
270
306
  })
271
307
 
272
308
  test('validateAction - Delete needs strategy', ({ assert }) => {
@@ -289,6 +325,68 @@ test.group('ApiModel Validation', () => {
289
325
  const issues = validateAction(action, exposure, model.key)
290
326
  assert.isTrue(issues.some((i) => i.code === 'ACTION_UPDATE_MISSING_METHODS'))
291
327
  })
328
+
329
+ test('validateApiPagination - missing pagination', ({ assert }) => {
330
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
331
+ const model = new ApiModel({}, domain)
332
+ // @ts-expect-error testing missing pagination property
333
+ model.pagination = undefined
334
+
335
+ const exposure = new ExposedEntity(model, {
336
+ entity: { key: 'fake' },
337
+ hasCollection: true,
338
+ collectionPath: '/items',
339
+ resourcePath: '/items/{id}',
340
+ })
341
+ exposure.actions = [new ListAction(exposure)]
342
+ model.exposes.set(exposure.key, exposure)
343
+
344
+ const issues = validateApiPagination(model)
345
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_PAGINATION'))
346
+ })
347
+
348
+ test('validateApiPagination - missing default limit', ({ assert }) => {
349
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
350
+ const model = new ApiModel(
351
+ {
352
+ pagination: { kind: 'offset', maxLimit: 100 },
353
+ },
354
+ domain
355
+ )
356
+ const exposure = new ExposedEntity(model, {
357
+ entity: { key: 'fake' },
358
+ hasCollection: true,
359
+ collectionPath: '/items',
360
+ resourcePath: '/items/{id}',
361
+ })
362
+ exposure.actions = [new ListAction(exposure)]
363
+ model.exposes.set(exposure.key, exposure)
364
+
365
+ const issues = validateApiPagination(model)
366
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_PAGINATION_DEFAULT_LIMIT'))
367
+ })
368
+
369
+ test('validateApiPagination - missing max limit', ({ assert }) => {
370
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
371
+ const model = new ApiModel(
372
+ {
373
+ pagination: { kind: 'offset', defaultLimit: 20 },
374
+ },
375
+ domain
376
+ )
377
+ const exposure = new ExposedEntity(model, {
378
+ entity: { key: 'fake' },
379
+ hasCollection: true,
380
+ collectionPath: '/items',
381
+ resourcePath: '/items/{id}',
382
+ })
383
+ exposure.actions = [new ListAction(exposure)]
384
+ model.exposes.set(exposure.key, exposure)
385
+
386
+ const issues = validateApiPagination(model)
387
+ assert.isTrue(issues.some((i) => i.code === 'API_MISSING_PAGINATION_MAX_LIMIT'))
388
+ })
389
+
292
390
  test('validateApiModel - success aggregates without issues', ({ assert }) => {
293
391
  const domain = new DataDomain({ info: { version: '1.0.0' } })
294
392
  const modelNode = domain.addModel({ key: 'users' })