@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
@@ -0,0 +1,359 @@
1
+ import type {
2
+ InfoObject,
3
+ MediaTypeObject,
4
+ OpenApi320,
5
+ ParameterObject,
6
+ PathItemObject,
7
+ PathsObject,
8
+ ReferenceObject,
9
+ RequestBodyObject,
10
+ SecurityRequirementObject,
11
+ SecuritySchemeObject,
12
+ OperationObject,
13
+ } from './types.js'
14
+ import type { ApiModel } from '../../ApiModel.js'
15
+ import type { Action } from '../../actions/Action.js'
16
+ import { OasSchemaGenerator } from './OasSchemaGenerator.js'
17
+ import type { MatchUserRoleAccessRule } from '../../rules/MatchUserRole.js'
18
+
19
+ export class OasGenerator {
20
+ /**
21
+ * @param model The API model to generate OAS for.
22
+ */
23
+ constructor(private model: ApiModel) {}
24
+
25
+ generate(): OpenApi320 {
26
+ const { domain } = this.model
27
+ if (!domain) {
28
+ throw new Error('Data domain is required for OAS generation')
29
+ }
30
+ const schemaGen = new OasSchemaGenerator(domain)
31
+ const paths = this.generatePaths(schemaGen)
32
+
33
+ const result: OpenApi320 = {
34
+ openapi: '3.2.0',
35
+ info: this.generateInfo(),
36
+ paths: paths,
37
+ components: {
38
+ schemas: schemaGen.getSchemas(),
39
+ securitySchemes: this.generateSecuritySchemes(),
40
+ },
41
+ }
42
+
43
+ if (this.model.accessRule && this.model.accessRule.length > 0) {
44
+ // Security requirements are determined by how the session is transported.
45
+ // OAS allows logical OR by adding separate objects in the global security array.
46
+ const securityRequirements: SecurityRequirementObject[] = []
47
+
48
+ if (this.model.session?.jwt?.enabled) {
49
+ securityRequirements.push({ BearerAuth: [] })
50
+ }
51
+ if (this.model.session?.cookie?.enabled) {
52
+ securityRequirements.push({ CookieAuth: [] })
53
+ }
54
+
55
+ if (securityRequirements.length > 0) {
56
+ result.security = securityRequirements
57
+ }
58
+ }
59
+
60
+ return result
61
+ }
62
+
63
+ private generateInfo(): InfoObject {
64
+ const info: InfoObject = {
65
+ title: this.model.info.displayName || this.model.info.name || 'API',
66
+ version: this.model.info.version || '1.0.0',
67
+ }
68
+
69
+ if (this.model.info.description) {
70
+ info.description = this.model.info.description
71
+ }
72
+
73
+ if (this.model.termsOfService) {
74
+ info.termsOfService = this.model.termsOfService
75
+ }
76
+
77
+ if (this.model.contact) {
78
+ info.contact = { ...this.model.contact }
79
+ }
80
+
81
+ if (this.model.license) {
82
+ info.license = { ...this.model.license }
83
+ }
84
+
85
+ return info
86
+ }
87
+
88
+ private generateSecuritySchemes(): Record<string, ReferenceObject | SecuritySchemeObject> {
89
+ const schemes: Record<string, ReferenceObject | SecuritySchemeObject> = {}
90
+
91
+ // OpenAPI doesn't have a native "Username/Password" strategy (except via OAuth2 flows).
92
+ // The actual security required to make calls to the API is defined by the Session transport.
93
+ if (this.model.session) {
94
+ if (this.model.session.jwt?.enabled) {
95
+ schemes['BearerAuth'] = {
96
+ type: 'http',
97
+ scheme: 'bearer',
98
+ bearerFormat: 'JWT',
99
+ description: 'JWT authorization obtained after trading Username/Password credentials',
100
+ }
101
+ }
102
+ if (this.model.session.cookie?.enabled) {
103
+ schemes['CookieAuth'] = {
104
+ type: 'apiKey',
105
+ in: 'cookie',
106
+ name: this.model.session.cookie.name || 'as',
107
+ description: 'Session cookie obtained after authenticating',
108
+ }
109
+ }
110
+ }
111
+
112
+ return schemes
113
+ }
114
+
115
+ private generatePaths(schemaGen: OasSchemaGenerator): PathsObject {
116
+ const paths: PathsObject = {}
117
+
118
+ // Depending on the configured session transports, the API will expose different login endpoints.
119
+ if (this.model.authentication?.strategy === 'UsernamePassword') {
120
+ if (this.model.session?.jwt?.enabled) {
121
+ paths['/auth/token'] = {
122
+ post: {
123
+ operationId: 'session_token_exchange',
124
+ summary: 'Exchange credentials for a JWT token',
125
+ requestBody: {
126
+ required: true,
127
+ content: {
128
+ 'application/json': {
129
+ schema: {
130
+ type: 'object',
131
+ properties: {
132
+ username: { type: 'string' },
133
+ password: { type: 'string', format: 'password' },
134
+ },
135
+ required: ['username', 'password'],
136
+ },
137
+ },
138
+ },
139
+ },
140
+ responses: {
141
+ '200': {
142
+ description: 'Returns the JSON Web Token',
143
+ content: {
144
+ 'application/json': {
145
+ schema: {
146
+ type: 'object',
147
+ properties: {
148
+ token: { type: 'string' },
149
+ },
150
+ required: ['token'],
151
+ },
152
+ },
153
+ },
154
+ },
155
+ '401': { description: 'Invalid credentials' },
156
+ },
157
+ security: [],
158
+ },
159
+ }
160
+
161
+ paths['/auth/token/{token}'] = {
162
+ delete: {
163
+ operationId: 'session_token_revoke',
164
+ summary: 'Revoke a specific JWT token',
165
+ parameters: [
166
+ {
167
+ name: 'token',
168
+ in: 'path',
169
+ required: true,
170
+ schema: { type: 'string' },
171
+ description: 'The session token to revoke',
172
+ },
173
+ ],
174
+ responses: {
175
+ '204': { description: 'Token revoked successfully' },
176
+ '401': { description: 'Unauthenticated' },
177
+ },
178
+ security: [],
179
+ },
180
+ }
181
+ }
182
+
183
+ if (this.model.session?.cookie?.enabled) {
184
+ paths['/auth/cookie'] = {
185
+ post: {
186
+ operationId: 'session_cookie_exchange',
187
+ summary: 'Obtain a session cookie and redirect to a web app',
188
+ requestBody: {
189
+ required: true,
190
+ content: {
191
+ 'application/json': {
192
+ schema: {
193
+ type: 'object',
194
+ properties: {
195
+ username: { type: 'string' },
196
+ password: { type: 'string', format: 'password' },
197
+ redirectUri: { type: 'string', format: 'uri' },
198
+ },
199
+ required: ['username', 'password', 'redirectUri'],
200
+ },
201
+ },
202
+ },
203
+ },
204
+ responses: {
205
+ '302': { description: 'Redirects to the provided URI with the session cookie set' },
206
+ '401': { description: 'Invalid credentials' },
207
+ },
208
+ security: [],
209
+ },
210
+ }
211
+ }
212
+ }
213
+
214
+ for (const expose of this.model.exposes.values()) {
215
+ const colPath = expose.getAbsoluteCollectionPath()
216
+ const resPath = expose.getAbsoluteResourcePath()
217
+
218
+ const domainEntity = this.model.domain?.findEntity(expose.entity.key)
219
+ let schemaRef: ReferenceObject | undefined = undefined
220
+ if (domainEntity) {
221
+ schemaRef = schemaGen.generateRootRef(domainEntity)
222
+ }
223
+
224
+ const responseContent = schemaRef ? { content: { 'application/json': { schema: schemaRef } } } : {}
225
+ const responseListContent = schemaRef
226
+ ? { content: { 'application/json': { schema: { type: 'array' as const, items: schemaRef } } } }
227
+ : {}
228
+ const requestContent: RequestBodyObject | undefined = schemaRef
229
+ ? {
230
+ required: true,
231
+ content: { 'application/json': { schema: schemaRef } as MediaTypeObject },
232
+ }
233
+ : undefined
234
+
235
+ if (expose.hasCollection && colPath) {
236
+ const pathItem: PathItemObject = paths[colPath] || {}
237
+ paths[colPath] = pathItem
238
+
239
+ const listAction = expose.actions.find((a) => a.kind === 'list')
240
+ const createAction = expose.actions.find((a) => a.kind === 'create')
241
+
242
+ if (listAction) {
243
+ const op: OperationObject = {
244
+ operationId: `list_${expose.key}`,
245
+ responses: { '200': { description: 'Success', ...responseListContent } },
246
+ }
247
+ this.applyActionSecurity(listAction, op)
248
+ pathItem.get = op
249
+ }
250
+
251
+ if (createAction) {
252
+ const op: OperationObject = {
253
+ operationId: `create_${expose.key}`,
254
+ requestBody: requestContent,
255
+ responses: { '201': { description: 'Created', ...responseContent } },
256
+ }
257
+ this.applyActionSecurity(createAction, op)
258
+ pathItem.post = op
259
+ }
260
+ }
261
+
262
+ if (resPath) {
263
+ const pathItem: PathItemObject = paths[resPath] || {}
264
+ paths[resPath] = pathItem
265
+
266
+ const readAction = expose.actions.find((a) => a.kind === 'read')
267
+ const updateAction = expose.actions.find((a) => a.kind === 'update')
268
+ const deleteAction = expose.actions.find((a) => a.kind === 'delete')
269
+
270
+ const parameters = this.extractPathParameters(resPath)
271
+
272
+ if (readAction) {
273
+ const op: OperationObject = {
274
+ operationId: `read_${expose.key}`,
275
+ parameters,
276
+ responses: { '200': { description: 'Success', ...responseContent } },
277
+ }
278
+ this.applyActionSecurity(readAction, op)
279
+ pathItem.get = op
280
+ }
281
+
282
+ if (updateAction) {
283
+ const op: OperationObject = {
284
+ operationId: `update_${expose.key}`,
285
+ parameters,
286
+ requestBody: requestContent,
287
+ responses: { '200': { description: 'Updated', ...responseContent } },
288
+ }
289
+ this.applyActionSecurity(updateAction, op)
290
+ pathItem.put = op
291
+ }
292
+
293
+ if (deleteAction) {
294
+ const op: OperationObject = {
295
+ operationId: `delete_${expose.key}`,
296
+ parameters,
297
+ responses: { '204': { description: 'Deleted' } },
298
+ }
299
+ this.applyActionSecurity(deleteAction, op)
300
+ pathItem.delete = op
301
+ }
302
+ }
303
+ }
304
+
305
+ return paths
306
+ }
307
+
308
+ private extractPathParameters(path: string): (ReferenceObject | ParameterObject)[] {
309
+ const params: (ReferenceObject | ParameterObject)[] = []
310
+ const regex = /\{([^}]+)\}/g
311
+ let match: RegExpExecArray | null
312
+ while ((match = regex.exec(path)) !== null) {
313
+ params.push({
314
+ name: match[1],
315
+ in: 'path',
316
+ required: true,
317
+ schema: { type: 'string' },
318
+ })
319
+ }
320
+ return params
321
+ }
322
+
323
+ private applyActionSecurity(action: Action, operation: OperationObject): void {
324
+ const rules = action.accessRule
325
+
326
+ // Check if anonymous access is explicitly allowed via an AllowPublic rule
327
+ const isPublic = rules.some((r) => r.type === 'allowPublic')
328
+ if (isPublic) {
329
+ operation.security = []
330
+ return
331
+ }
332
+
333
+ if (rules.length > 0) {
334
+ // It's restricted. Define operational security requirements.
335
+ const items: SecurityRequirementObject[] = []
336
+ if (this.model.session?.jwt?.enabled) items.push({ BearerAuth: [] })
337
+ if (this.model.session?.cookie?.enabled) items.push({ CookieAuth: [] })
338
+
339
+ if (items.length > 0) {
340
+ operation.security = items
341
+ }
342
+ }
343
+
344
+ // Extract any matching roles specific to the RBAC capabilities
345
+ const roleRules = rules.filter((r) => r.type === 'matchUserRole') as MatchUserRoleAccessRule[]
346
+ const roles = roleRules.flatMap((r) => r.role || [])
347
+
348
+ if (roles.length > 0) {
349
+ const uniqueRoles = Array.from(new Set(roles))
350
+
351
+ // Inject natively as vendor extension for processing tooling
352
+ // ;(operation as any)['x-roles'] = uniqueRoles
353
+
354
+ // Append strictly to the markdown description so developers always see the RBAC bounds
355
+ const rolesDesc = `**Required Roles:** ${uniqueRoles.join(', ')}`
356
+ operation.description = operation.description ? `${operation.description}\n\n${rolesDesc}` : rolesDesc
357
+ }
358
+ }
359
+ }
@@ -0,0 +1,255 @@
1
+ import type { ReferenceObject, SchemaObject } from './types.js'
2
+ import { AssociationWebBindings, PropertyWebBindings } from '../../Bindings.js'
3
+ import type { DataDomain } from '../../DataDomain.js'
4
+ import type { DomainEntity } from '../../DomainEntity.js'
5
+ import type { DomainProperty } from '../../DomainProperty.js'
6
+ import { SemanticType } from '../../Semantics.js'
7
+
8
+ export class OasSchemaGenerator {
9
+ private memorySchemas: Record<string, SchemaObject> = {}
10
+ private memoryNames: Record<string, string> = {}
11
+
12
+ constructor(private domain: DataDomain) {}
13
+
14
+ /**
15
+ * Retrieves the dictionary of all processed OAS JSON schemas.
16
+ */
17
+ public getSchemas(): Record<string, SchemaObject> {
18
+ const result: Record<string, SchemaObject> = {}
19
+ for (const [key, value] of Object.entries(this.memorySchemas)) {
20
+ result[this.memoryNames[key]] = value
21
+ }
22
+ return result
23
+ }
24
+
25
+ /**
26
+ * Generates schemas recursively for an entity and returns the $ref pointing to the root representation.
27
+ * @param entity
28
+ */
29
+ public generateRootRef(entity: DomainEntity): ReferenceObject {
30
+ this.generateEntity(entity)
31
+ return { $ref: `#/components/schemas/${this.generateSchemaKey(entity)}` }
32
+ }
33
+
34
+ /**
35
+ * Generates a schema key for an entity.
36
+ */
37
+ public generateSchemaKey(entity: DomainEntity): string {
38
+ return entity.info.name || 'entity ' + entity.key
39
+ }
40
+
41
+ private generateEntity(entity: DomainEntity): void {
42
+ if (this.memorySchemas[entity.key]) {
43
+ return // Already processed or processing
44
+ }
45
+
46
+ // Preemptively assign to avoid infinite recursion over bidirectional associations
47
+ const schema: SchemaObject = {
48
+ type: 'object',
49
+ properties: {},
50
+ required: [],
51
+ }
52
+ this.memorySchemas[entity.key] = schema
53
+ const schemaKey = this.generateSchemaKey(entity)
54
+ this.memoryNames[entity.key] = schemaKey
55
+
56
+ const required: string[] = []
57
+
58
+ for (const prop of entity.properties) {
59
+ if (prop.required && !prop.readOnly) {
60
+ required.push(prop.info.name || prop.key)
61
+ }
62
+ if (!schema.properties) {
63
+ schema.properties = {}
64
+ }
65
+ const webBindings = prop.readBinding('web') as PropertyWebBindings | undefined
66
+ if (webBindings?.hidden) {
67
+ continue
68
+ }
69
+ schema.properties[prop.info.name || prop.key] = this.mapPropertyBase(prop, webBindings)
70
+ }
71
+
72
+ for (const assoc of entity.associations) {
73
+ const bindings = assoc.readBinding('web') as AssociationWebBindings | undefined
74
+ if (bindings?.hidden) {
75
+ continue
76
+ }
77
+
78
+ const targets = Array.from(assoc.listTargets())
79
+ if (targets.length === 0) continue
80
+
81
+ const linked = assoc.schema?.linked === true
82
+ const targetRefs: (SchemaObject | ReferenceObject)[] = []
83
+
84
+ for (const targetEntity of targets) {
85
+ if (linked) {
86
+ // When an entity is linked then we treat it as a regular property, which value is a key to
87
+ // the target entity. The consuming application has to construct the endpoint manually.
88
+ const targetName = targetEntity.info.displayName || targetEntity.info.name || targetEntity.key
89
+ targetRefs.push({
90
+ type: 'string',
91
+ description: `The ID of the linked ${targetName}.`,
92
+ })
93
+ } else {
94
+ // If not linked, embed the resource as a reference to the schema.
95
+ if (targetEntity !== entity) {
96
+ this.generateEntity(targetEntity)
97
+ }
98
+ targetRefs.push({ $ref: `#/components/schemas/${this.generateSchemaKey(targetEntity)}` })
99
+ }
100
+ }
101
+
102
+ let assocSchema: SchemaObject | ReferenceObject
103
+ if (targetRefs.length > 1) {
104
+ const unionType = assoc.schema?.unionType || 'anyOf'
105
+ if (unionType === 'oneOf') {
106
+ assocSchema = { oneOf: targetRefs }
107
+ } else if (unionType === 'allOf') {
108
+ assocSchema = { allOf: targetRefs }
109
+ } else {
110
+ assocSchema = { anyOf: targetRefs }
111
+ }
112
+ } else {
113
+ assocSchema = targetRefs[0]
114
+ }
115
+
116
+ const propName = assoc.info.name || assoc.key
117
+ if (!schema.properties) {
118
+ schema.properties = {}
119
+ }
120
+
121
+ if (assoc.multiple) {
122
+ schema.properties[propName] = {
123
+ type: 'array',
124
+ items: assocSchema,
125
+ }
126
+ } else {
127
+ schema.properties[propName] = assocSchema
128
+ }
129
+ }
130
+
131
+ if (required.length > 0) {
132
+ schema.required = required
133
+ } else {
134
+ delete schema.required
135
+ }
136
+ }
137
+
138
+ private mapPropertyBase(prop: DomainProperty, webBindings?: PropertyWebBindings): SchemaObject | ReferenceObject {
139
+ const base: SchemaObject = {}
140
+
141
+ if (prop.readOnly) {
142
+ base.readOnly = true
143
+ }
144
+
145
+ if (prop.writeOnly) {
146
+ base.writeOnly = true
147
+ }
148
+
149
+ if (prop.deprecated) {
150
+ base.deprecated = true
151
+ }
152
+
153
+ if (prop.schema) {
154
+ if (prop.schema.minimum !== undefined) {
155
+ if (prop.schema.exclusiveMinimum) {
156
+ base.exclusiveMinimum = prop.schema.minimum
157
+ } else {
158
+ base.minimum = prop.schema.minimum
159
+ }
160
+ }
161
+ if (prop.schema.maximum !== undefined) {
162
+ if (prop.schema.exclusiveMaximum) {
163
+ base.exclusiveMaximum = prop.schema.maximum
164
+ } else {
165
+ base.maximum = prop.schema.maximum
166
+ }
167
+ }
168
+ if (prop.schema.pattern) {
169
+ base.pattern = prop.schema.pattern
170
+ }
171
+ if (prop.schema.enum) {
172
+ base.enum = [...prop.schema.enum]
173
+ }
174
+ if (prop.schema.defaultValue?.value !== undefined) {
175
+ base.default = prop.schema.defaultValue.value
176
+ }
177
+ if (prop.schema.examples) {
178
+ base.examples = [...prop.schema.examples]
179
+ }
180
+ }
181
+
182
+ if (prop.info && prop.info.description) {
183
+ base.description = prop.info.description
184
+ }
185
+
186
+ if (prop.hasSemantic(SemanticType.Password)) {
187
+ base.format = 'password'
188
+ }
189
+
190
+ if (webBindings?.xml) {
191
+ base.xml = { ...webBindings.xml }
192
+ }
193
+
194
+ switch (prop.type) {
195
+ case 'string':
196
+ base.type = 'string'
197
+ break
198
+ case 'number':
199
+ if (webBindings?.format === 'int32' || webBindings?.format === 'int64') {
200
+ base.type = 'integer'
201
+ } else {
202
+ base.type = 'number'
203
+ }
204
+ if (prop.schema?.multipleOf !== undefined) {
205
+ base.multipleOf = prop.schema.multipleOf
206
+ }
207
+ break
208
+ case 'boolean':
209
+ base.type = 'boolean'
210
+ break
211
+ case 'date':
212
+ base.type = 'string'
213
+ base.format = 'date'
214
+ break
215
+ case 'datetime':
216
+ base.type = 'string'
217
+ base.format = 'date-time'
218
+ break
219
+ case 'time':
220
+ base.type = 'string'
221
+ base.format = 'time'
222
+ break
223
+ case 'binary':
224
+ base.type = 'string'
225
+ if (webBindings?.format) {
226
+ base.format = webBindings.format
227
+ } else {
228
+ base.format = 'binary'
229
+ }
230
+ if (webBindings?.fileTypes && webBindings.fileTypes.length > 0) {
231
+ const typesStr = webBindings.fileTypes.join(', ')
232
+ // Standard JSON Schema keyword for media wrapping (single value string usually,
233
+ // but some tools handle comma separated)
234
+ base.contentMediaType = webBindings.fileTypes.length === 1 ? webBindings.fileTypes[0] : typesStr
235
+ // Vendor extension for tools supporting multiple explicit arrays
236
+ // ;(base as any)['x-file-types'] = webBindings.fileTypes
237
+ // Append to description so viewers can always see the requirements natively
238
+ const typesDesc = `**Allowed file types:** ${typesStr}`
239
+ base.description = base.description ? `${base.description}\n\n${typesDesc}` : typesDesc
240
+ }
241
+ break
242
+ default:
243
+ base.type = 'string' // fallback
244
+ }
245
+
246
+ if (prop.multiple) {
247
+ return {
248
+ type: 'array',
249
+ items: base,
250
+ }
251
+ }
252
+
253
+ return base
254
+ }
255
+ }