@sapporta/rest-open-api 3.52.1 → 3.52.2
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/CHANGELOG.md +6 -0
- package/README.md +76 -9
- package/index.cjs.d.ts +1 -0
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +673 -0
- package/index.cjs.mjs +2 -0
- package/index.esm.js +668 -0
- package/package.json +15 -5
- package/src/index.d.ts +2 -0
- package/src/lib/contract-traversal.d.ts +14 -0
- package/src/lib/parsers/body.d.ts +14 -0
- package/src/lib/parsers/headers.d.ts +14 -0
- package/src/lib/parsers/path-params.d.ts +20 -0
- package/src/lib/parsers/query-params.d.ts +15 -0
- package/src/lib/parsers/test-helpers.d.ts +4 -0
- package/src/lib/parsers/utils.d.ts +11 -0
- package/src/lib/ts-rest-open-api.d.ts +46 -0
- package/src/lib/types.d.ts +83 -0
- package/src/lib/utils.d.ts +5 -0
- package/.babelrc +0 -10
- package/.eslintrc.json +0 -21
- package/jest.config.ts +0 -16
- package/project.json +0 -51
- package/src/index.ts +0 -6
- package/src/lib/contract-traversal.ts +0 -217
- package/src/lib/parsers/body.ts +0 -71
- package/src/lib/parsers/headers.ts +0 -163
- package/src/lib/parsers/path-params.spec.ts +0 -235
- package/src/lib/parsers/path-params.ts +0 -140
- package/src/lib/parsers/query-params.spec.ts +0 -129
- package/src/lib/parsers/query-params.ts +0 -89
- package/src/lib/parsers/test-helpers.ts +0 -26
- package/src/lib/parsers/utils.spec.ts +0 -117
- package/src/lib/parsers/utils.ts +0 -82
- package/src/lib/ts-rest-open-api.ts +0 -245
- package/src/lib/types.ts +0 -109
- package/src/lib/utils.ts +0 -92
- package/tsconfig.json +0 -22
- package/tsconfig.lib.json +0 -10
- package/tsconfig.spec.json +0 -9
- package/typedoc.json +0 -5
- /package/src/lib/parsers/{index.ts → index.d.ts} +0 -0
package/index.cjs.js
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var restCore = require('@sapporta/rest-core');
|
|
6
|
+
|
|
7
|
+
const schemaToParameter = (schema, where, required, key, jsonQuery) => {
|
|
8
|
+
let description = undefined;
|
|
9
|
+
if ('description' in schema) {
|
|
10
|
+
description = schema['description'];
|
|
11
|
+
delete schema['description'];
|
|
12
|
+
}
|
|
13
|
+
let examples = undefined;
|
|
14
|
+
if ('mediaExamples' in schema) {
|
|
15
|
+
examples = schema['mediaExamples'];
|
|
16
|
+
delete schema['mediaExamples'];
|
|
17
|
+
}
|
|
18
|
+
const isDeepObject = 'properties' in schema;
|
|
19
|
+
if (jsonQuery) {
|
|
20
|
+
return {
|
|
21
|
+
name: key,
|
|
22
|
+
in: where,
|
|
23
|
+
...(description && { description }),
|
|
24
|
+
...(required && { required }),
|
|
25
|
+
content: {
|
|
26
|
+
'application/json': {
|
|
27
|
+
schema,
|
|
28
|
+
...(examples && { examples }),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return {
|
|
35
|
+
name: key,
|
|
36
|
+
in: where,
|
|
37
|
+
...(examples && { examples }),
|
|
38
|
+
...(description && { description }),
|
|
39
|
+
...(required && { required }),
|
|
40
|
+
...(isDeepObject && !jsonQuery && { style: 'deepObject' }),
|
|
41
|
+
schema,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Convert a @type {SchemaObject} to an array of @type {ParameterObject}
|
|
47
|
+
*
|
|
48
|
+
* @param schema - Zod 4 JSON schema output
|
|
49
|
+
* @param where - The location of the parameters
|
|
50
|
+
* @param jsonQuery - Whether the schema is a JSON query
|
|
51
|
+
* @returns The parameters for the schema
|
|
52
|
+
*/
|
|
53
|
+
const schemaObjectToParameters = (schema, where, jsonQuery = false) => {
|
|
54
|
+
var _a;
|
|
55
|
+
const parameters = [];
|
|
56
|
+
if (schema.type === 'object') {
|
|
57
|
+
const requiredSet = new Set((_a = schema.required) !== null && _a !== void 0 ? _a : []);
|
|
58
|
+
const properties = schema.properties;
|
|
59
|
+
if (!properties) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
63
|
+
parameters.push(schemaToParameter(value, where, requiredSet.has(key), key, jsonQuery));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return parameters;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* We build up the params from both the path and the schema.
|
|
71
|
+
*
|
|
72
|
+
* This builds up the record of name -> parameter object.
|
|
73
|
+
*/
|
|
74
|
+
const getParamsFromPathOnly = (path) => {
|
|
75
|
+
var _a;
|
|
76
|
+
const params = new Map();
|
|
77
|
+
const paramsInPath = ((_a = path.match(/:([^/]+)/g)) === null || _a === void 0 ? void 0 : _a.map((param) => param.slice(1))) || [];
|
|
78
|
+
for (const param of paramsInPath) {
|
|
79
|
+
params.set(param, {
|
|
80
|
+
name: param,
|
|
81
|
+
in: 'path',
|
|
82
|
+
required: true,
|
|
83
|
+
schema: { type: 'string' },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return params;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Should return schema params as priority, and then path params as fallback
|
|
90
|
+
*
|
|
91
|
+
* @param pathParams - params inferred from the path i.e. just from the string
|
|
92
|
+
* @param schemaParams - params from the schema
|
|
93
|
+
*/
|
|
94
|
+
const mergeParams = (pathParams, schemaParams) => {
|
|
95
|
+
const resultMap = new Map();
|
|
96
|
+
for (const [name, param] of pathParams.entries()) {
|
|
97
|
+
resultMap.set(name, param);
|
|
98
|
+
}
|
|
99
|
+
for (const [name, param] of schemaParams.entries()) {
|
|
100
|
+
resultMap.set(name, param);
|
|
101
|
+
}
|
|
102
|
+
return Array.from(resultMap.values());
|
|
103
|
+
};
|
|
104
|
+
const syncFunc$4 = ({ transformSchema, appRoute, id, concatenatedPath, }) => {
|
|
105
|
+
const schema = appRoute.pathParams;
|
|
106
|
+
const paramsMap = getParamsFromPathOnly(appRoute.path);
|
|
107
|
+
const transformedSchema = transformSchema({
|
|
108
|
+
schema,
|
|
109
|
+
appRoute,
|
|
110
|
+
id,
|
|
111
|
+
concatenatedPath,
|
|
112
|
+
type: 'path',
|
|
113
|
+
});
|
|
114
|
+
if (!transformedSchema) {
|
|
115
|
+
return Array.from(paramsMap.values());
|
|
116
|
+
}
|
|
117
|
+
const schemaParams = schemaObjectToParameters(transformedSchema, 'path');
|
|
118
|
+
const schemaParamsMap = new Map(schemaParams.map((param) => [param.name, param]));
|
|
119
|
+
return mergeParams(paramsMap, schemaParamsMap);
|
|
120
|
+
};
|
|
121
|
+
const asyncFunc$4 = async ({ transformSchema, appRoute, id, concatenatedPath, }) => {
|
|
122
|
+
const schema = appRoute.pathParams;
|
|
123
|
+
const paramsMap = getParamsFromPathOnly(appRoute.path);
|
|
124
|
+
const transformedSchema = await transformSchema({
|
|
125
|
+
schema,
|
|
126
|
+
appRoute,
|
|
127
|
+
id,
|
|
128
|
+
concatenatedPath,
|
|
129
|
+
type: 'path',
|
|
130
|
+
});
|
|
131
|
+
if (!transformedSchema) {
|
|
132
|
+
return Array.from(paramsMap.values());
|
|
133
|
+
}
|
|
134
|
+
const schemaParams = schemaObjectToParameters(transformedSchema, 'path');
|
|
135
|
+
const schemaParamsMap = new Map(schemaParams.map((param) => [param.name, param]));
|
|
136
|
+
return mergeParams(paramsMap, schemaParamsMap);
|
|
137
|
+
};
|
|
138
|
+
const getPathParameterSchema = {
|
|
139
|
+
sync: syncFunc$4,
|
|
140
|
+
async: asyncFunc$4,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const syncFunc$3 = ({ transformSchema, appRoute, id, concatenatedPath, }) => {
|
|
144
|
+
const schema = appRoute.headers;
|
|
145
|
+
if (schema === null) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
if (schema === undefined) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
if (typeof schema === 'symbol') {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
if (restCore.isStandardSchema(schema)) {
|
|
155
|
+
const transformedSchema = transformSchema({
|
|
156
|
+
schema,
|
|
157
|
+
appRoute,
|
|
158
|
+
id,
|
|
159
|
+
concatenatedPath,
|
|
160
|
+
type: 'header',
|
|
161
|
+
});
|
|
162
|
+
if (!transformedSchema) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
return schemaObjectToParameters(transformedSchema, 'header');
|
|
166
|
+
}
|
|
167
|
+
const parameters = [];
|
|
168
|
+
for (const [key, subSchema] of Object.entries(schema)) {
|
|
169
|
+
if (restCore.isStandardSchema(subSchema)) {
|
|
170
|
+
const transformedSchema = transformSchema({
|
|
171
|
+
schema: subSchema,
|
|
172
|
+
appRoute,
|
|
173
|
+
id,
|
|
174
|
+
concatenatedPath,
|
|
175
|
+
type: 'header',
|
|
176
|
+
});
|
|
177
|
+
if (!transformedSchema) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
parameters.push(...schemaObjectToParameters(transformedSchema, 'header'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return parameters;
|
|
184
|
+
};
|
|
185
|
+
const asyncFunc$3 = async ({ transformSchema, appRoute, id, concatenatedPath, }) => {
|
|
186
|
+
var _a;
|
|
187
|
+
const schema = appRoute.headers;
|
|
188
|
+
if (schema === null) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
if (schema === undefined) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
if (typeof schema === 'symbol') {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
if (restCore.isStandardSchema(schema)) {
|
|
198
|
+
const transformedSchema = await transformSchema({
|
|
199
|
+
schema,
|
|
200
|
+
appRoute,
|
|
201
|
+
id,
|
|
202
|
+
concatenatedPath,
|
|
203
|
+
type: 'header',
|
|
204
|
+
});
|
|
205
|
+
if (!transformedSchema) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
return schemaObjectToParameters(transformedSchema, 'header');
|
|
209
|
+
}
|
|
210
|
+
const parameters = [];
|
|
211
|
+
for (const [key, subSchema] of Object.entries(schema)) {
|
|
212
|
+
if (restCore.isStandardSchema(subSchema)) {
|
|
213
|
+
const transformedSchema = await transformSchema({
|
|
214
|
+
schema: subSchema,
|
|
215
|
+
appRoute,
|
|
216
|
+
id,
|
|
217
|
+
concatenatedPath,
|
|
218
|
+
type: 'header',
|
|
219
|
+
});
|
|
220
|
+
if (!transformedSchema) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const validateEmptyResult = subSchema['~standard'].validate(undefined);
|
|
224
|
+
if (validateEmptyResult instanceof Promise) {
|
|
225
|
+
throw new Error('Schema validation must be synchronous');
|
|
226
|
+
}
|
|
227
|
+
const isRequired = Boolean((_a = validateEmptyResult.issues) === null || _a === void 0 ? void 0 : _a.length);
|
|
228
|
+
const asParameter = schemaToParameter(transformedSchema, 'header', isRequired, key, false);
|
|
229
|
+
parameters.push(asParameter);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return parameters;
|
|
233
|
+
};
|
|
234
|
+
const getHeaderParameterSchema = {
|
|
235
|
+
sync: syncFunc$3,
|
|
236
|
+
async: asyncFunc$3,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const syncFunc$2 = ({ transformSchema, appRoute, id, concatenatedPath, jsonQuery = false, }) => {
|
|
240
|
+
const schema = appRoute.query;
|
|
241
|
+
const isSchema = restCore.isStandardSchema(schema);
|
|
242
|
+
if (!isSchema) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const transformedSchema = transformSchema({
|
|
246
|
+
schema,
|
|
247
|
+
appRoute,
|
|
248
|
+
id,
|
|
249
|
+
concatenatedPath,
|
|
250
|
+
type: 'query',
|
|
251
|
+
});
|
|
252
|
+
if (!transformedSchema) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
return schemaObjectToParameters(transformedSchema, 'query', jsonQuery);
|
|
256
|
+
};
|
|
257
|
+
const asyncFunc$2 = async ({ transformSchema, appRoute, id, concatenatedPath, jsonQuery = false, }) => {
|
|
258
|
+
const schema = appRoute.query;
|
|
259
|
+
const isSchema = restCore.isStandardSchema(schema);
|
|
260
|
+
if (!isSchema) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const transformedSchema = await transformSchema({
|
|
264
|
+
schema,
|
|
265
|
+
appRoute,
|
|
266
|
+
id,
|
|
267
|
+
concatenatedPath,
|
|
268
|
+
type: 'query',
|
|
269
|
+
});
|
|
270
|
+
if (!transformedSchema) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
return schemaObjectToParameters(transformedSchema, 'query', jsonQuery);
|
|
274
|
+
};
|
|
275
|
+
const getQueryParameterSchema = {
|
|
276
|
+
sync: syncFunc$2,
|
|
277
|
+
async: asyncFunc$2,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const syncFunc$1 = ({ transformSchema, appRoute, id, concatenatedPath, }) => {
|
|
281
|
+
const schema = 'body' in appRoute ? appRoute.body : undefined;
|
|
282
|
+
const transformedSchema = transformSchema({
|
|
283
|
+
schema,
|
|
284
|
+
appRoute,
|
|
285
|
+
id,
|
|
286
|
+
concatenatedPath,
|
|
287
|
+
type: 'body',
|
|
288
|
+
});
|
|
289
|
+
if (!transformedSchema) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return transformedSchema;
|
|
293
|
+
};
|
|
294
|
+
const asyncFunc$1 = async ({ transformSchema, appRoute, id, concatenatedPath, }) => {
|
|
295
|
+
const schema = 'body' in appRoute ? appRoute.body : undefined;
|
|
296
|
+
const transformedSchema = await transformSchema({
|
|
297
|
+
schema,
|
|
298
|
+
appRoute,
|
|
299
|
+
id,
|
|
300
|
+
concatenatedPath,
|
|
301
|
+
type: 'body',
|
|
302
|
+
});
|
|
303
|
+
return transformedSchema;
|
|
304
|
+
};
|
|
305
|
+
const getBodySchema = {
|
|
306
|
+
sync: syncFunc$1,
|
|
307
|
+
async: asyncFunc$1,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Recursively step through the router and get all the individual routes with their paths etc.
|
|
312
|
+
*/
|
|
313
|
+
const getPathsFromRouter = (router, pathHistory) => {
|
|
314
|
+
const paths = [];
|
|
315
|
+
Object.keys(router).forEach((key) => {
|
|
316
|
+
const value = router[key];
|
|
317
|
+
if (restCore.isAppRoute(value)) {
|
|
318
|
+
const pathWithPathParams = value.path.replace(/:(\w+)/g, '{$1}');
|
|
319
|
+
paths.push({
|
|
320
|
+
id: key,
|
|
321
|
+
path: pathWithPathParams,
|
|
322
|
+
route: value,
|
|
323
|
+
paths: pathHistory !== null && pathHistory !== void 0 ? pathHistory : [],
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
paths.push(...getPathsFromRouter(value, [...(pathHistory !== null && pathHistory !== void 0 ? pathHistory : []), key]));
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
return paths;
|
|
331
|
+
};
|
|
332
|
+
const syncFunc = ({ contract, transformSchema, jsonQuery, }) => {
|
|
333
|
+
const paths = getPathsFromRouter(contract);
|
|
334
|
+
const results = [];
|
|
335
|
+
for (const path of paths) {
|
|
336
|
+
const concatenatedPath = [...path.paths, path.id].join('.');
|
|
337
|
+
const pathParams = getPathParameterSchema.sync({
|
|
338
|
+
transformSchema,
|
|
339
|
+
appRoute: path.route,
|
|
340
|
+
id: path.id,
|
|
341
|
+
concatenatedPath,
|
|
342
|
+
});
|
|
343
|
+
const headerParams = getHeaderParameterSchema.sync({
|
|
344
|
+
transformSchema,
|
|
345
|
+
appRoute: path.route,
|
|
346
|
+
id: path.id,
|
|
347
|
+
concatenatedPath,
|
|
348
|
+
});
|
|
349
|
+
const querySchema = getQueryParameterSchema.sync({
|
|
350
|
+
transformSchema,
|
|
351
|
+
appRoute: path.route,
|
|
352
|
+
id: path.id,
|
|
353
|
+
concatenatedPath,
|
|
354
|
+
jsonQuery: !!jsonQuery,
|
|
355
|
+
});
|
|
356
|
+
const bodySchema = getBodySchema.sync({
|
|
357
|
+
transformSchema,
|
|
358
|
+
appRoute: path.route,
|
|
359
|
+
id: path.id,
|
|
360
|
+
concatenatedPath,
|
|
361
|
+
});
|
|
362
|
+
const responses = {};
|
|
363
|
+
for (const [statusCode, _response] of Object.entries(path.route.responses)) {
|
|
364
|
+
const schemaValidator = restCore.isAppRouteOtherResponse(_response)
|
|
365
|
+
? _response.body
|
|
366
|
+
: _response;
|
|
367
|
+
const responseSchema = transformSchema({
|
|
368
|
+
schema: schemaValidator,
|
|
369
|
+
appRoute: path.route,
|
|
370
|
+
id: path.id,
|
|
371
|
+
concatenatedPath,
|
|
372
|
+
type: 'response',
|
|
373
|
+
});
|
|
374
|
+
if (responseSchema) {
|
|
375
|
+
responses[statusCode] = responseSchema;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
results.push({
|
|
379
|
+
...path,
|
|
380
|
+
schemaResults: {
|
|
381
|
+
path: pathParams,
|
|
382
|
+
headers: headerParams,
|
|
383
|
+
query: querySchema,
|
|
384
|
+
body: bodySchema,
|
|
385
|
+
responses,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return results;
|
|
390
|
+
};
|
|
391
|
+
const asyncFunc = async ({ contract, transformSchema, jsonQuery, }) => {
|
|
392
|
+
const paths = getPathsFromRouter(contract);
|
|
393
|
+
const results = [];
|
|
394
|
+
for (const path of paths) {
|
|
395
|
+
const concatenatedPath = [...path.paths, path.id].join('.');
|
|
396
|
+
const pathParams = await getPathParameterSchema.async({
|
|
397
|
+
transformSchema,
|
|
398
|
+
appRoute: path.route,
|
|
399
|
+
id: path.id,
|
|
400
|
+
concatenatedPath,
|
|
401
|
+
});
|
|
402
|
+
const headerParams = await getHeaderParameterSchema.async({
|
|
403
|
+
transformSchema,
|
|
404
|
+
appRoute: path.route,
|
|
405
|
+
id: path.id,
|
|
406
|
+
concatenatedPath,
|
|
407
|
+
});
|
|
408
|
+
const querySchema = await getQueryParameterSchema.async({
|
|
409
|
+
transformSchema,
|
|
410
|
+
appRoute: path.route,
|
|
411
|
+
id: path.id,
|
|
412
|
+
concatenatedPath,
|
|
413
|
+
jsonQuery: !!jsonQuery,
|
|
414
|
+
});
|
|
415
|
+
const bodySchema = await getBodySchema.async({
|
|
416
|
+
transformSchema,
|
|
417
|
+
appRoute: path.route,
|
|
418
|
+
id: path.id,
|
|
419
|
+
concatenatedPath,
|
|
420
|
+
});
|
|
421
|
+
const responses = {};
|
|
422
|
+
for (const [statusCode, _response] of Object.entries(path.route.responses)) {
|
|
423
|
+
const schemaValidator = restCore.isAppRouteOtherResponse(_response)
|
|
424
|
+
? _response.body
|
|
425
|
+
: _response;
|
|
426
|
+
const responseSchema = await transformSchema({
|
|
427
|
+
schema: schemaValidator,
|
|
428
|
+
appRoute: path.route,
|
|
429
|
+
id: path.id,
|
|
430
|
+
concatenatedPath,
|
|
431
|
+
type: 'response',
|
|
432
|
+
});
|
|
433
|
+
if (responseSchema) {
|
|
434
|
+
responses[statusCode] = responseSchema;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
results.push({
|
|
438
|
+
...path,
|
|
439
|
+
schemaResults: {
|
|
440
|
+
path: pathParams,
|
|
441
|
+
headers: headerParams,
|
|
442
|
+
query: querySchema,
|
|
443
|
+
body: bodySchema,
|
|
444
|
+
responses,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return results;
|
|
449
|
+
};
|
|
450
|
+
const performContractTraversal = {
|
|
451
|
+
sync: syncFunc,
|
|
452
|
+
async: asyncFunc,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const convertSchemaObjectToMediaTypeObject = (input) => {
|
|
456
|
+
const { mediaExamples: examples, ...schema } = input;
|
|
457
|
+
return {
|
|
458
|
+
schema,
|
|
459
|
+
...(examples && { examples }),
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
const extractReferenceSchemas = (schema, referenceSchemas) => {
|
|
463
|
+
var _a, _b, _c;
|
|
464
|
+
if (schema.allOf) {
|
|
465
|
+
schema.allOf = (_a = schema.allOf) === null || _a === void 0 ? void 0 : _a.map((subSchema) => extractReferenceSchemas(subSchema, referenceSchemas));
|
|
466
|
+
}
|
|
467
|
+
if (schema.anyOf) {
|
|
468
|
+
schema.anyOf = (_b = schema.anyOf) === null || _b === void 0 ? void 0 : _b.map((subSchema) => extractReferenceSchemas(subSchema, referenceSchemas));
|
|
469
|
+
}
|
|
470
|
+
if (schema.oneOf) {
|
|
471
|
+
schema.oneOf = (_c = schema.oneOf) === null || _c === void 0 ? void 0 : _c.map((subSchema) => extractReferenceSchemas(subSchema, referenceSchemas));
|
|
472
|
+
}
|
|
473
|
+
if (schema.not) {
|
|
474
|
+
schema.not = extractReferenceSchemas(schema.not, referenceSchemas);
|
|
475
|
+
}
|
|
476
|
+
if (schema.items) {
|
|
477
|
+
schema.items = extractReferenceSchemas(schema.items, referenceSchemas);
|
|
478
|
+
}
|
|
479
|
+
if (schema.properties) {
|
|
480
|
+
schema.properties = Object.entries(schema.properties).reduce((prev, [propertyName, schema]) => {
|
|
481
|
+
prev[propertyName] = extractReferenceSchemas(schema, referenceSchemas);
|
|
482
|
+
return prev;
|
|
483
|
+
}, {});
|
|
484
|
+
}
|
|
485
|
+
if (schema.additionalProperties) {
|
|
486
|
+
schema.additionalProperties =
|
|
487
|
+
typeof schema.additionalProperties != 'boolean'
|
|
488
|
+
? extractReferenceSchemas(schema.additionalProperties, referenceSchemas)
|
|
489
|
+
: schema.additionalProperties;
|
|
490
|
+
}
|
|
491
|
+
if (schema.title) {
|
|
492
|
+
const nullable = schema.nullable;
|
|
493
|
+
schema.nullable = undefined;
|
|
494
|
+
if (schema.title in referenceSchemas) {
|
|
495
|
+
if (JSON.stringify(referenceSchemas[schema.title]) !==
|
|
496
|
+
JSON.stringify(schema)) {
|
|
497
|
+
throw new Error(`Schema title '${schema.title}' already exists with a different schema`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
referenceSchemas[schema.title] = schema;
|
|
502
|
+
}
|
|
503
|
+
if (nullable) {
|
|
504
|
+
schema = {
|
|
505
|
+
nullable: true,
|
|
506
|
+
allOf: [
|
|
507
|
+
{
|
|
508
|
+
$ref: `#/components/schemas/${schema.title}`,
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
schema = {
|
|
515
|
+
$ref: `#/components/schemas/${schema.title}`,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return schema;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Generate OpenAPI specification from ts-rest router
|
|
524
|
+
*
|
|
525
|
+
* @param router - The ts-rest router to generate OpenAPI from
|
|
526
|
+
* @param apiDoc - Base OpenAPI document configuration
|
|
527
|
+
* @param options - Generation options
|
|
528
|
+
* @param options.setOperationId - Whether to set operation IDs (true, false, or 'concatenated-path')
|
|
529
|
+
* @param options.jsonQuery - Enable JSON query parameters, [see](/docs/open-api#json-query-params)
|
|
530
|
+
* @param options.operationMapper - Function to customize OpenAPI operations. Receives the operation object, app route, and operation ID
|
|
531
|
+
* @returns OpenAPI specification object
|
|
532
|
+
*/
|
|
533
|
+
function generateOpenApi(router, apiDoc, options) {
|
|
534
|
+
const paths = performContractTraversal.sync({
|
|
535
|
+
contract: router,
|
|
536
|
+
transformSchema: options === null || options === void 0 ? void 0 : options.schemaTransformer,
|
|
537
|
+
jsonQuery: !!(options === null || options === void 0 ? void 0 : options.jsonQuery),
|
|
538
|
+
});
|
|
539
|
+
return traversedPathsToOpenApi(paths, apiDoc, {
|
|
540
|
+
setOperationId: options === null || options === void 0 ? void 0 : options.setOperationId,
|
|
541
|
+
jsonQuery: options === null || options === void 0 ? void 0 : options.jsonQuery,
|
|
542
|
+
operationMapper: options === null || options === void 0 ? void 0 : options.operationMapper,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Generate OpenAPI specification from ts-rest router with custom schema transformer
|
|
547
|
+
*
|
|
548
|
+
* @param router - The ts-rest router to generate OpenAPI from
|
|
549
|
+
* @param apiDoc - Base OpenAPI document configuration
|
|
550
|
+
* @param options - Generation options
|
|
551
|
+
* @param options.setOperationId - Whether to set operation IDs (true, false, or 'concatenated-path')
|
|
552
|
+
* @param options.jsonQuery - Enable JSON query parameters, [see](/docs/open-api#json-query-params)
|
|
553
|
+
* @param options.operationMapper - Function to customize OpenAPI operations. Receives the operation object, app route, and operation ID
|
|
554
|
+
* @param options.schemaTransformer - Custom schema transformer function.
|
|
555
|
+
*/
|
|
556
|
+
async function generateOpenApiAsync(router, apiDoc, options) {
|
|
557
|
+
const paths = await performContractTraversal.async({
|
|
558
|
+
contract: router,
|
|
559
|
+
transformSchema: options.schemaTransformer,
|
|
560
|
+
jsonQuery: !!options.jsonQuery,
|
|
561
|
+
});
|
|
562
|
+
return traversedPathsToOpenApi(paths, apiDoc, {
|
|
563
|
+
setOperationId: options.setOperationId,
|
|
564
|
+
jsonQuery: options.jsonQuery,
|
|
565
|
+
operationMapper: options.operationMapper,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Inner function to be reused by both sync and async functions
|
|
570
|
+
*/
|
|
571
|
+
const traversedPathsToOpenApi = (paths, apiDoc, options) => {
|
|
572
|
+
var _a, _b, _c;
|
|
573
|
+
const mapMethod = {
|
|
574
|
+
GET: 'get',
|
|
575
|
+
POST: 'post',
|
|
576
|
+
PUT: 'put',
|
|
577
|
+
DELETE: 'delete',
|
|
578
|
+
PATCH: 'patch',
|
|
579
|
+
};
|
|
580
|
+
const operationIds = new Map();
|
|
581
|
+
const referenceSchemas = {};
|
|
582
|
+
const pathObject = {};
|
|
583
|
+
for (const path of paths) {
|
|
584
|
+
if (options.setOperationId === true) {
|
|
585
|
+
const existingOp = operationIds.get(path.id);
|
|
586
|
+
if (existingOp) {
|
|
587
|
+
throw new Error(`Route '${path.id}' already defined under ${existingOp.join('.')}`);
|
|
588
|
+
}
|
|
589
|
+
operationIds.set(path.id, path.paths);
|
|
590
|
+
}
|
|
591
|
+
const _bodySchema = path.schemaResults.body;
|
|
592
|
+
const bodySchema = _bodySchema && typeof _bodySchema === 'object' && 'title' in _bodySchema
|
|
593
|
+
? extractReferenceSchemas(path.schemaResults.body, referenceSchemas)
|
|
594
|
+
: path.schemaResults.body;
|
|
595
|
+
const responses = {};
|
|
596
|
+
for (const [statusCode, response] of Object.entries(path.route.responses)) {
|
|
597
|
+
const contentType = restCore.isAppRouteOtherResponse(response)
|
|
598
|
+
? response.contentType
|
|
599
|
+
: 'application/json';
|
|
600
|
+
const responseSchemaObject = path.schemaResults.responses[statusCode];
|
|
601
|
+
const responseSchemaObjectWithReferences = responseSchemaObject
|
|
602
|
+
? extractReferenceSchemas(responseSchemaObject, referenceSchemas)
|
|
603
|
+
: null;
|
|
604
|
+
responses[statusCode] = {
|
|
605
|
+
...(responseSchemaObjectWithReferences
|
|
606
|
+
? {
|
|
607
|
+
content: {
|
|
608
|
+
[contentType]: {
|
|
609
|
+
...convertSchemaObjectToMediaTypeObject(responseSchemaObjectWithReferences),
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
}
|
|
613
|
+
: {}),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
const contentType = ((_a = path.route) === null || _a === void 0 ? void 0 : _a.method) !== 'GET' && 'contentType' in path.route
|
|
617
|
+
? (_c = (_b = path.route) === null || _b === void 0 ? void 0 : _b.contentType) !== null && _c !== void 0 ? _c : 'application/json'
|
|
618
|
+
: 'application/json';
|
|
619
|
+
const pathOperation = {
|
|
620
|
+
description: path.route.description,
|
|
621
|
+
summary: path.route.summary,
|
|
622
|
+
deprecated: path.route.deprecated,
|
|
623
|
+
tags: path.paths,
|
|
624
|
+
parameters: [
|
|
625
|
+
...path.schemaResults.path,
|
|
626
|
+
...path.schemaResults.headers,
|
|
627
|
+
...path.schemaResults.query,
|
|
628
|
+
],
|
|
629
|
+
...(options.setOperationId
|
|
630
|
+
? {
|
|
631
|
+
operationId: options.setOperationId === 'concatenated-path'
|
|
632
|
+
? [...path.paths, path.id].join('.')
|
|
633
|
+
: path.id,
|
|
634
|
+
}
|
|
635
|
+
: {}),
|
|
636
|
+
...(bodySchema
|
|
637
|
+
? {
|
|
638
|
+
requestBody: {
|
|
639
|
+
description: 'Body',
|
|
640
|
+
content: {
|
|
641
|
+
[contentType]: {
|
|
642
|
+
...convertSchemaObjectToMediaTypeObject(bodySchema),
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
}
|
|
647
|
+
: {}),
|
|
648
|
+
responses,
|
|
649
|
+
};
|
|
650
|
+
pathObject[path.path] = {
|
|
651
|
+
...pathObject[path.path],
|
|
652
|
+
[mapMethod[path.route.method]]: options.operationMapper
|
|
653
|
+
? options.operationMapper(pathOperation, path.route, path.id)
|
|
654
|
+
: pathOperation,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (Object.keys(referenceSchemas).length) {
|
|
658
|
+
apiDoc['components'] = {
|
|
659
|
+
schemas: {
|
|
660
|
+
...referenceSchemas,
|
|
661
|
+
},
|
|
662
|
+
...apiDoc['components'],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
openapi: '3.0.2',
|
|
667
|
+
paths: pathObject,
|
|
668
|
+
...apiDoc,
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
exports.generateOpenApi = generateOpenApi;
|
|
673
|
+
exports.generateOpenApiAsync = generateOpenApiAsync;
|
package/index.cjs.mjs
ADDED