@api-client/core 0.19.19 → 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.
- package/build/src/authorization/Utils.js +3 -3
- package/build/src/authorization/Utils.js.map +1 -1
- package/build/src/modeling/ApiModel.d.ts +16 -5
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +17 -2
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/ApiValidation.d.ts.map +1 -1
- package/build/src/modeling/ApiValidation.js +2 -1
- package/build/src/modeling/ApiValidation.js.map +1 -1
- package/build/src/modeling/DomainProperty.d.ts +12 -0
- package/build/src/modeling/DomainProperty.d.ts.map +1 -1
- package/build/src/modeling/DomainProperty.js +23 -28
- package/build/src/modeling/DomainProperty.js.map +1 -1
- package/build/src/modeling/DomainSerialization.js +1 -1
- package/build/src/modeling/DomainSerialization.js.map +1 -1
- package/build/src/modeling/ExposedEntity.d.ts +15 -1
- package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
- package/build/src/modeling/ExposedEntity.js +42 -4
- package/build/src/modeling/ExposedEntity.js.map +1 -1
- package/build/src/modeling/actions/Action.d.ts.map +1 -1
- package/build/src/modeling/actions/Action.js +1 -0
- package/build/src/modeling/actions/Action.js.map +1 -1
- package/build/src/modeling/actions/ListAction.d.ts +3 -17
- package/build/src/modeling/actions/ListAction.d.ts.map +1 -1
- package/build/src/modeling/actions/ListAction.js +18 -38
- package/build/src/modeling/actions/ListAction.js.map +1 -1
- package/build/src/modeling/actions/SearchAction.d.ts +4 -4
- package/build/src/modeling/actions/SearchAction.d.ts.map +1 -1
- package/build/src/modeling/actions/SearchAction.js +16 -13
- package/build/src/modeling/actions/SearchAction.js.map +1 -1
- package/build/src/modeling/generators/oas_312/OasGenerator.d.ts +32 -0
- package/build/src/modeling/generators/oas_312/OasGenerator.d.ts.map +1 -0
- package/build/src/modeling/generators/oas_312/OasGenerator.js +1452 -0
- package/build/src/modeling/generators/oas_312/OasGenerator.js.map +1 -0
- package/build/src/modeling/generators/oas_312/OasSchemaGenerator.d.ts +27 -0
- package/build/src/modeling/generators/oas_312/OasSchemaGenerator.d.ts.map +1 -0
- package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js +295 -0
- package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js.map +1 -0
- package/build/src/modeling/generators/oas_312/types.d.ts +1010 -0
- package/build/src/modeling/generators/oas_312/types.d.ts.map +1 -0
- package/build/src/modeling/generators/oas_312/types.js +2 -0
- package/build/src/modeling/generators/oas_312/types.js.map +1 -0
- package/build/src/modeling/generators/oas_320/OasGenerator.d.ts +16 -0
- package/build/src/modeling/generators/oas_320/OasGenerator.d.ts.map +1 -0
- package/build/src/modeling/generators/oas_320/OasGenerator.js +306 -0
- package/build/src/modeling/generators/oas_320/OasGenerator.js.map +1 -0
- package/build/src/modeling/generators/oas_320/OasSchemaGenerator.d.ts +25 -0
- package/build/src/modeling/generators/oas_320/OasSchemaGenerator.d.ts.map +1 -0
- package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js +237 -0
- package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js.map +1 -0
- package/build/src/modeling/generators/oas_320/types.d.ts +1219 -0
- package/build/src/modeling/generators/oas_320/types.d.ts.map +1 -0
- package/build/src/modeling/generators/oas_320/types.js +2 -0
- package/build/src/modeling/generators/oas_320/types.js.map +1 -0
- package/build/src/modeling/types.d.ts +50 -13
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/src/modeling/validation/api_model_rules.d.ts +1 -0
- package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -1
- package/build/src/modeling/validation/api_model_rules.js +105 -29
- package/build/src/modeling/validation/api_model_rules.js.map +1 -1
- package/build/src/models/ProjectRequest.d.ts.map +1 -1
- package/build/src/models/ProjectRequest.js +0 -4
- package/build/src/models/ProjectRequest.js.map +1 -1
- package/build/src/models/transformers/ArcDexieTransformer.d.ts.map +1 -1
- package/build/src/models/transformers/ArcDexieTransformer.js +0 -4
- package/build/src/models/transformers/ArcDexieTransformer.js.map +1 -1
- package/build/src/models/transformers/ImportUtils.js +1 -1
- package/build/src/models/transformers/ImportUtils.js.map +1 -1
- package/build/src/models/transformers/PostmanBackupTransformer.d.ts.map +1 -1
- package/build/src/models/transformers/PostmanBackupTransformer.js +0 -4
- package/build/src/models/transformers/PostmanBackupTransformer.js.map +1 -1
- package/build/src/runtime/constants.d.ts +7 -0
- package/build/src/runtime/constants.d.ts.map +1 -0
- package/build/src/runtime/constants.js +8 -0
- package/build/src/runtime/constants.js.map +1 -0
- package/build/src/runtime/http-engine/ntlm/Des.d.ts.map +1 -1
- package/build/src/runtime/http-engine/ntlm/Des.js +1 -0
- package/build/src/runtime/http-engine/ntlm/Des.js.map +1 -1
- package/build/src/runtime/variables/EvalFunctions.d.ts.map +1 -1
- package/build/src/runtime/variables/EvalFunctions.js +0 -1
- package/build/src/runtime/variables/EvalFunctions.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/eslint.config.js +6 -0
- package/package.json +3 -1
- package/src/authorization/Utils.ts +3 -3
- package/src/modeling/ApiModel.ts +23 -8
- package/src/modeling/ApiValidation.ts +2 -0
- package/src/modeling/DomainProperty.ts +22 -18
- package/src/modeling/DomainSerialization.ts +1 -1
- package/src/modeling/ExposedEntity.ts +44 -4
- package/src/modeling/actions/Action.ts +1 -0
- package/src/modeling/actions/ListAction.ts +12 -30
- package/src/modeling/actions/SearchAction.ts +11 -8
- package/src/modeling/generators/oas_312/OasGenerator.ts +1685 -0
- package/src/modeling/generators/oas_312/OasSchemaGenerator.ts +322 -0
- package/src/modeling/generators/oas_312/types.ts +1052 -0
- package/src/modeling/generators/oas_320/OasGenerator.ts +359 -0
- package/src/modeling/generators/oas_320/OasSchemaGenerator.ts +255 -0
- package/src/modeling/generators/oas_320/types.ts +1259 -0
- package/src/modeling/types.ts +55 -22
- package/src/modeling/validation/api_model_rules.ts +103 -32
- package/src/models/ProjectRequest.ts +0 -4
- package/src/models/transformers/ArcDexieTransformer.ts +0 -4
- package/src/models/transformers/ImportUtils.ts +1 -1
- package/src/models/transformers/PostmanBackupTransformer.ts +0 -5
- package/src/runtime/constants.ts +9 -0
- package/src/runtime/http-engine/ntlm/Des.ts +1 -0
- package/src/runtime/variables/EvalFunctions.ts +0 -1
- package/tests/test-utils.ts +6 -2
- package/tests/unit/decorators/observed.spec.ts +8 -24
- package/tests/unit/decorators/observed_recursive.spec.ts +0 -1
- package/tests/unit/events/EventsTestHelpers.ts +0 -1
- package/tests/unit/events/events_polyfills.ts +0 -1
- package/tests/unit/legacy-transformers/DataTestHelper.ts +0 -2
- package/tests/unit/legacy-transformers/LegacyExportProcessor.spec.ts +0 -1
- package/tests/unit/modeling/actions/ListAction.spec.ts +9 -69
- package/tests/unit/modeling/actions/SearchAction.spec.ts +9 -35
- package/tests/unit/modeling/api_model.spec.ts +28 -0
- package/tests/unit/modeling/definitions/sku.spec.ts +0 -2
- package/tests/unit/modeling/domain_property.spec.ts +20 -1
- package/tests/unit/modeling/exposed_entity.spec.ts +71 -0
- package/tests/unit/modeling/generators/OasGenerator.spec.ts +302 -0
- 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
|
+
}
|