nestjs-openapi 0.1.0
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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +139 -0
- package/dist/index.d.mts +896 -0
- package/dist/index.d.ts +896 -0
- package/dist/index.mjs +297 -0
- package/dist/internal.d.mts +2 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.mjs +53 -0
- package/dist/shared/nestjs-openapi.B1bBy_tG.mjs +1529 -0
- package/dist/shared/nestjs-openapi.BYUrTaMo.d.mts +355 -0
- package/dist/shared/nestjs-openapi.BYUrTaMo.d.ts +355 -0
- package/dist/shared/nestjs-openapi.DlNMM8Zq.mjs +1831 -0
- package/package.json +112 -0
|
@@ -0,0 +1,1831 @@
|
|
|
1
|
+
import { Schema, Effect, Logger, Layer, LogLevel } from 'effect';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join, dirname, resolve } from 'node:path';
|
|
4
|
+
import { Project } from 'ts-morph';
|
|
5
|
+
import { globSync, glob } from 'glob';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { C as ConfigNotFoundError, a as ConfigLoadError, b as ConfigValidationError, p as transformMethods, A as extractClassConstraints, B as getRequiredProperties, D as mergeValidationConstraints, E as EntryNotFoundError, g as getModules, o as getControllerMethodInfos } from './nestjs-openapi.B1bBy_tG.mjs';
|
|
8
|
+
import { createGenerator } from 'ts-json-schema-generator';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
|
|
13
|
+
const buildFlow = (flow, includeAuthUrl, includeTokenUrl) => ({
|
|
14
|
+
...includeAuthUrl && flow.authorizationUrl && { authorizationUrl: flow.authorizationUrl },
|
|
15
|
+
...includeTokenUrl && flow.tokenUrl && { tokenUrl: flow.tokenUrl },
|
|
16
|
+
...flow.refreshUrl && { refreshUrl: flow.refreshUrl },
|
|
17
|
+
scopes: flow.scopes ?? {}
|
|
18
|
+
});
|
|
19
|
+
const buildOAuth2Flows = (flows) => ({
|
|
20
|
+
...flows.implicit && { implicit: buildFlow(flows.implicit, true, false) },
|
|
21
|
+
...flows.password && { password: buildFlow(flows.password, false, true) },
|
|
22
|
+
...flows.clientCredentials && {
|
|
23
|
+
clientCredentials: buildFlow(flows.clientCredentials, false, true)
|
|
24
|
+
},
|
|
25
|
+
...flows.authorizationCode && {
|
|
26
|
+
authorizationCode: buildFlow(flows.authorizationCode, true, true)
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const transformSecurityScheme = (config) => {
|
|
30
|
+
const scheme = {
|
|
31
|
+
type: config.type,
|
|
32
|
+
...config.description && { description: config.description }
|
|
33
|
+
};
|
|
34
|
+
switch (config.type) {
|
|
35
|
+
case "http":
|
|
36
|
+
return {
|
|
37
|
+
...scheme,
|
|
38
|
+
...config.scheme && { scheme: config.scheme },
|
|
39
|
+
...config.bearerFormat && { bearerFormat: config.bearerFormat }
|
|
40
|
+
};
|
|
41
|
+
case "apiKey":
|
|
42
|
+
return {
|
|
43
|
+
...scheme,
|
|
44
|
+
...config.in && { in: config.in },
|
|
45
|
+
...config.parameterName && { name: config.parameterName }
|
|
46
|
+
};
|
|
47
|
+
case "oauth2":
|
|
48
|
+
if (!config.flows) return scheme;
|
|
49
|
+
return { ...scheme, flows: buildOAuth2Flows(config.flows) };
|
|
50
|
+
case "openIdConnect":
|
|
51
|
+
return {
|
|
52
|
+
...scheme,
|
|
53
|
+
...config.openIdConnectUrl && {
|
|
54
|
+
openIdConnectUrl: config.openIdConnectUrl
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
default:
|
|
58
|
+
return scheme;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const buildSecuritySchemes = (configs) => {
|
|
62
|
+
const schemes = {};
|
|
63
|
+
for (const config of configs) {
|
|
64
|
+
schemes[config.name] = transformSecurityScheme(config);
|
|
65
|
+
}
|
|
66
|
+
return schemes;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
class SchemaGenerationError extends Schema.TaggedError()(
|
|
70
|
+
"SchemaGenerationError",
|
|
71
|
+
{
|
|
72
|
+
message: Schema.String,
|
|
73
|
+
cause: Schema.optional(Schema.Unknown)
|
|
74
|
+
}
|
|
75
|
+
) {
|
|
76
|
+
static fromError(error, context) {
|
|
77
|
+
const baseMessage = error instanceof Error ? error.message : "Unknown schema generation error";
|
|
78
|
+
const message = context ? `${baseMessage} (${context})` : baseMessage;
|
|
79
|
+
return new SchemaGenerationError({
|
|
80
|
+
message,
|
|
81
|
+
cause: error
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
static noFilesFound(patterns) {
|
|
85
|
+
return new SchemaGenerationError({
|
|
86
|
+
message: `No DTO files found matching patterns: ${patterns.join(", ")}`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const generateSchemas = Effect.fn("SchemaGenerator.generate")(function* (options) {
|
|
91
|
+
yield* Effect.logDebug("Starting schema generation").pipe(
|
|
92
|
+
Effect.annotateLogs({
|
|
93
|
+
dtoGlob: options.dtoGlob.join(", "),
|
|
94
|
+
tsconfig: options.tsconfig
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
const patterns = options.dtoGlob.map((pattern) => ({
|
|
98
|
+
pattern,
|
|
99
|
+
combinable: !pattern.startsWith("/") && !pattern.includes("..")
|
|
100
|
+
}));
|
|
101
|
+
const combinable = patterns.filter((p) => p.combinable);
|
|
102
|
+
const nonCombinable = patterns.filter((p) => !p.combinable);
|
|
103
|
+
const groupedPatterns = [];
|
|
104
|
+
if (combinable.length > 0) {
|
|
105
|
+
groupedPatterns.push(
|
|
106
|
+
combinable.length === 1 ? combinable[0].pattern : `{${combinable.map((p) => p.pattern).join(",")}}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
groupedPatterns.push(...nonCombinable.map((p) => p.pattern));
|
|
110
|
+
const schemaResults = yield* Effect.all(
|
|
111
|
+
groupedPatterns.map(
|
|
112
|
+
(pattern) => generateSchemasFromGlob(pattern, options.tsconfig, options.basePath)
|
|
113
|
+
),
|
|
114
|
+
{ concurrency: "unbounded" }
|
|
115
|
+
);
|
|
116
|
+
const allDefinitions = schemaResults.reduce(
|
|
117
|
+
(acc, schemas) => ({ ...acc, ...schemas.definitions }),
|
|
118
|
+
{}
|
|
119
|
+
);
|
|
120
|
+
yield* Effect.logDebug("Schema generation complete").pipe(
|
|
121
|
+
Effect.annotateLogs({
|
|
122
|
+
definitionCount: Object.keys(allDefinitions).length
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
return { definitions: allDefinitions };
|
|
126
|
+
});
|
|
127
|
+
const generateSchemasFromGlob = Effect.fn("SchemaGenerator.generateFromGlob")(
|
|
128
|
+
function* (pattern, tsconfig, basePath) {
|
|
129
|
+
const isBraceAbsolute = pattern.startsWith("{") && pattern.slice(1, -1).split(",").map((entry) => entry.trim()).every((entry) => entry.startsWith("/"));
|
|
130
|
+
const absolutePattern = pattern.startsWith("/") || isBraceAbsolute ? pattern : join(basePath, pattern);
|
|
131
|
+
yield* Effect.annotateCurrentSpan("pattern", absolutePattern);
|
|
132
|
+
const matchedFiles = globSync(absolutePattern);
|
|
133
|
+
if (matchedFiles.length === 0) {
|
|
134
|
+
yield* Effect.logDebug("No files matched pattern, skipping").pipe(
|
|
135
|
+
Effect.annotateLogs({ pattern: absolutePattern })
|
|
136
|
+
);
|
|
137
|
+
return { definitions: {} };
|
|
138
|
+
}
|
|
139
|
+
return yield* Effect.try({
|
|
140
|
+
try: () => {
|
|
141
|
+
const config = {
|
|
142
|
+
path: absolutePattern,
|
|
143
|
+
tsconfig,
|
|
144
|
+
type: "*",
|
|
145
|
+
// Generate schemas for all exported types
|
|
146
|
+
skipTypeCheck: true,
|
|
147
|
+
// Note: topRef must NOT be set to false, as it prevents interface schemas from being generated
|
|
148
|
+
expose: "export",
|
|
149
|
+
// Only export explicitly exported types
|
|
150
|
+
jsDoc: "extended",
|
|
151
|
+
// Include JSDoc comments
|
|
152
|
+
sortProps: true,
|
|
153
|
+
strictTuples: false,
|
|
154
|
+
encodeRefs: false,
|
|
155
|
+
additionalProperties: false
|
|
156
|
+
};
|
|
157
|
+
const generator = createGenerator(config);
|
|
158
|
+
const schema = generator.createSchema(config.type);
|
|
159
|
+
return convertToGeneratedSchemas(schema);
|
|
160
|
+
},
|
|
161
|
+
catch: (error) => SchemaGenerationError.fromError(error, `pattern: ${pattern}`)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
const convertToGeneratedSchemas = (schema) => {
|
|
166
|
+
const definitions = {};
|
|
167
|
+
const defs = schema.$defs ?? schema.definitions ?? {};
|
|
168
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
169
|
+
definitions[name] = def;
|
|
170
|
+
}
|
|
171
|
+
return { definitions };
|
|
172
|
+
};
|
|
173
|
+
const generateSchemasFromFiles = Effect.fn(
|
|
174
|
+
"SchemaGenerator.generateFromFiles"
|
|
175
|
+
)(function* (filePaths, tsconfig) {
|
|
176
|
+
if (filePaths.length === 0) {
|
|
177
|
+
return { definitions: {} };
|
|
178
|
+
}
|
|
179
|
+
yield* Effect.logDebug("Generating schemas from resolved files").pipe(
|
|
180
|
+
Effect.annotateLogs({
|
|
181
|
+
fileCount: filePaths.length
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
const batchedResult = yield* generateSchemasWithTempTsconfig(
|
|
185
|
+
filePaths,
|
|
186
|
+
tsconfig
|
|
187
|
+
).pipe(
|
|
188
|
+
Effect.catchTag(
|
|
189
|
+
"SchemaGenerationError",
|
|
190
|
+
() => generateSchemasFromFilesIndividual(filePaths, tsconfig)
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
yield* Effect.logDebug("Additional schema generation complete").pipe(
|
|
194
|
+
Effect.annotateLogs({
|
|
195
|
+
definitionCount: Object.keys(batchedResult.definitions).length
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
return batchedResult;
|
|
199
|
+
});
|
|
200
|
+
const generateSchemasWithTempTsconfig = (filePaths, tsconfig) => Effect.try({
|
|
201
|
+
try: () => {
|
|
202
|
+
const originalConfig = JSON.parse(readFileSync(tsconfig, "utf-8"));
|
|
203
|
+
const tempConfig = {
|
|
204
|
+
...originalConfig,
|
|
205
|
+
compilerOptions: {
|
|
206
|
+
...originalConfig.compilerOptions,
|
|
207
|
+
skipLibCheck: true,
|
|
208
|
+
skipDefaultLibCheck: true,
|
|
209
|
+
noEmit: true
|
|
210
|
+
},
|
|
211
|
+
files: [...filePaths],
|
|
212
|
+
// Prevent loading other files from the project
|
|
213
|
+
include: void 0,
|
|
214
|
+
exclude: void 0
|
|
215
|
+
};
|
|
216
|
+
const tempTsconfigPath = join(
|
|
217
|
+
dirname(tsconfig),
|
|
218
|
+
`.tsconfig.schema-gen.${randomUUID()}.json`
|
|
219
|
+
);
|
|
220
|
+
try {
|
|
221
|
+
writeFileSync(tempTsconfigPath, JSON.stringify(tempConfig, null, 2));
|
|
222
|
+
const pattern = filePaths.length === 1 ? filePaths[0] : `{${filePaths.join(",")}}`;
|
|
223
|
+
const config = {
|
|
224
|
+
path: pattern,
|
|
225
|
+
tsconfig: tempTsconfigPath,
|
|
226
|
+
type: "*",
|
|
227
|
+
skipTypeCheck: true,
|
|
228
|
+
expose: "export",
|
|
229
|
+
jsDoc: "extended",
|
|
230
|
+
sortProps: true,
|
|
231
|
+
strictTuples: false,
|
|
232
|
+
encodeRefs: false,
|
|
233
|
+
additionalProperties: false
|
|
234
|
+
};
|
|
235
|
+
const generator = createGenerator(config);
|
|
236
|
+
const schema = generator.createSchema(config.type);
|
|
237
|
+
return convertToGeneratedSchemas(schema);
|
|
238
|
+
} finally {
|
|
239
|
+
if (existsSync(tempTsconfigPath)) {
|
|
240
|
+
unlinkSync(tempTsconfigPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
catch: (error) => SchemaGenerationError.fromError(
|
|
245
|
+
error,
|
|
246
|
+
`files: ${filePaths.slice(0, 3).join(", ")}${filePaths.length > 3 ? ` (+${filePaths.length - 3} more)` : ""}`
|
|
247
|
+
)
|
|
248
|
+
});
|
|
249
|
+
const generateSchemasFromFilesIndividual = Effect.fn(
|
|
250
|
+
"SchemaGenerator.generateFromFilesIndividual"
|
|
251
|
+
)(function* (filePaths, tsconfig) {
|
|
252
|
+
const schemaResults = yield* Effect.all(
|
|
253
|
+
filePaths.map(
|
|
254
|
+
(filePath) => generateSchemasFromFile(filePath, tsconfig).pipe(
|
|
255
|
+
// Intentional: continue even if individual files fail
|
|
256
|
+
Effect.catchTag(
|
|
257
|
+
"SchemaGenerationError",
|
|
258
|
+
() => Effect.succeed({ definitions: {} })
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
),
|
|
262
|
+
{ concurrency: "unbounded" }
|
|
263
|
+
);
|
|
264
|
+
const allDefinitions = schemaResults.reduce(
|
|
265
|
+
(acc, schemas) => ({ ...acc, ...schemas.definitions }),
|
|
266
|
+
{}
|
|
267
|
+
);
|
|
268
|
+
return { definitions: allDefinitions };
|
|
269
|
+
});
|
|
270
|
+
const generateSchemasFromFile = Effect.fn("SchemaGenerator.generateFromFile")(
|
|
271
|
+
function* (filePath, tsconfig) {
|
|
272
|
+
return yield* Effect.try({
|
|
273
|
+
try: () => {
|
|
274
|
+
const config = {
|
|
275
|
+
path: filePath,
|
|
276
|
+
tsconfig,
|
|
277
|
+
type: "*",
|
|
278
|
+
skipTypeCheck: true,
|
|
279
|
+
expose: "export",
|
|
280
|
+
jsDoc: "extended",
|
|
281
|
+
sortProps: true,
|
|
282
|
+
strictTuples: false,
|
|
283
|
+
encodeRefs: false,
|
|
284
|
+
additionalProperties: false
|
|
285
|
+
};
|
|
286
|
+
const generator = createGenerator(config);
|
|
287
|
+
const schema = generator.createSchema(config.type);
|
|
288
|
+
return convertToGeneratedSchemas(schema);
|
|
289
|
+
},
|
|
290
|
+
catch: (error) => SchemaGenerationError.fromError(error, `file: ${filePath}`)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const UGLY_REF_PATTERN = /^(?:structure|class)-\d+(-\d+)*$/;
|
|
296
|
+
const CONTAINS_UGLY_REF_PATTERN = /(?:structure|class)-\d+(-\d+)*/;
|
|
297
|
+
const STRUCTURE_REF_PATTERN = UGLY_REF_PATTERN;
|
|
298
|
+
const CONTAINS_STRUCTURE_REF_PATTERN = CONTAINS_UGLY_REF_PATTERN;
|
|
299
|
+
const containsUglyStructureRef = (name) => {
|
|
300
|
+
const decoded = decodeURIComponent(name);
|
|
301
|
+
return CONTAINS_STRUCTURE_REF_PATTERN.test(decoded);
|
|
302
|
+
};
|
|
303
|
+
const traverseSchema = (schema, transform) => {
|
|
304
|
+
const updated = { ...transform(schema) };
|
|
305
|
+
if (schema.properties) {
|
|
306
|
+
updated.properties = Object.fromEntries(
|
|
307
|
+
Object.entries(schema.properties).map(([key, value]) => [
|
|
308
|
+
key,
|
|
309
|
+
traverseSchema(value, transform)
|
|
310
|
+
])
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (schema.items) {
|
|
314
|
+
updated.items = traverseSchema(schema.items, transform);
|
|
315
|
+
}
|
|
316
|
+
if (schema.oneOf) {
|
|
317
|
+
updated.oneOf = schema.oneOf.map((s) => traverseSchema(s, transform));
|
|
318
|
+
}
|
|
319
|
+
if (schema.anyOf) {
|
|
320
|
+
updated.anyOf = schema.anyOf.map((s) => traverseSchema(s, transform));
|
|
321
|
+
}
|
|
322
|
+
if (schema.allOf) {
|
|
323
|
+
updated.allOf = schema.allOf.map((s) => traverseSchema(s, transform));
|
|
324
|
+
}
|
|
325
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
326
|
+
updated.additionalProperties = traverseSchema(
|
|
327
|
+
schema.additionalProperties,
|
|
328
|
+
transform
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return updated;
|
|
332
|
+
};
|
|
333
|
+
const extractRefName = (ref) => {
|
|
334
|
+
const match = ref.match(/^#\/(?:definitions|components\/schemas)\/(.+)$/);
|
|
335
|
+
return match ? match[1] : ref;
|
|
336
|
+
};
|
|
337
|
+
const toPascalCase = (str) => {
|
|
338
|
+
if (!str) return str;
|
|
339
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
340
|
+
};
|
|
341
|
+
const extractStructureRef = (name) => {
|
|
342
|
+
if (STRUCTURE_REF_PATTERN.test(name)) {
|
|
343
|
+
return name;
|
|
344
|
+
}
|
|
345
|
+
const match = CONTAINS_STRUCTURE_REF_PATTERN.exec(name);
|
|
346
|
+
return match ? match[0] : null;
|
|
347
|
+
};
|
|
348
|
+
const findStructureUsages = (schemas) => {
|
|
349
|
+
const usages = /* @__PURE__ */ new Map();
|
|
350
|
+
const recordUsage = (structureRef, parent, property, fullRef) => {
|
|
351
|
+
const isWrapped = fullRef !== structureRef;
|
|
352
|
+
const wrapper = isWrapped ? fullRef.match(/^([^<]+)</)?.[1] : void 0;
|
|
353
|
+
const usage = {
|
|
354
|
+
parent,
|
|
355
|
+
property,
|
|
356
|
+
fullRef,
|
|
357
|
+
isWrapped,
|
|
358
|
+
wrapper
|
|
359
|
+
};
|
|
360
|
+
const existing = usages.get(structureRef) ?? [];
|
|
361
|
+
existing.push(usage);
|
|
362
|
+
usages.set(structureRef, existing);
|
|
363
|
+
};
|
|
364
|
+
for (const [schemaName, schema] of Object.entries(schemas.definitions)) {
|
|
365
|
+
if (STRUCTURE_REF_PATTERN.test(schemaName)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (schema.properties) {
|
|
369
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
370
|
+
if (propSchema.$ref) {
|
|
371
|
+
const refName = extractRefName(propSchema.$ref);
|
|
372
|
+
const structureRef = extractStructureRef(refName);
|
|
373
|
+
if (structureRef) {
|
|
374
|
+
recordUsage(structureRef, schemaName, propName, refName);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (propSchema.items?.$ref) {
|
|
378
|
+
const refName = extractRefName(propSchema.items.$ref);
|
|
379
|
+
const structureRef = extractStructureRef(refName);
|
|
380
|
+
if (structureRef) {
|
|
381
|
+
recordUsage(structureRef, schemaName, propName, refName);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return usages;
|
|
388
|
+
};
|
|
389
|
+
const replaceStructureInName = (name, structureRef, newName) => {
|
|
390
|
+
return name.replace(structureRef, newName);
|
|
391
|
+
};
|
|
392
|
+
const updateRefsWithMapping = (schema, structureMapping) => traverseSchema(schema, (s) => {
|
|
393
|
+
if (!s.$ref) return s;
|
|
394
|
+
let refName = extractRefName(s.$ref);
|
|
395
|
+
for (const [structureRef, newName] of structureMapping) {
|
|
396
|
+
if (refName.includes(structureRef)) {
|
|
397
|
+
refName = replaceStructureInName(refName, structureRef, newName);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { ...s, $ref: `#/components/schemas/${refName}` };
|
|
401
|
+
});
|
|
402
|
+
const findUniqueName = (propertyName, parentName, usedNames) => {
|
|
403
|
+
if (!usedNames.has(propertyName)) return propertyName;
|
|
404
|
+
const withParent = `${parentName}${propertyName}`;
|
|
405
|
+
if (!usedNames.has(withParent)) return withParent;
|
|
406
|
+
let suffix = 1;
|
|
407
|
+
while (usedNames.has(`${withParent}_${suffix}`)) {
|
|
408
|
+
suffix++;
|
|
409
|
+
}
|
|
410
|
+
return `${withParent}_${suffix}`;
|
|
411
|
+
};
|
|
412
|
+
const normalizeStructureRefs = (schemas) => {
|
|
413
|
+
const reservedNames = /* @__PURE__ */ new Set();
|
|
414
|
+
for (const name of Object.keys(schemas.definitions)) {
|
|
415
|
+
if (!STRUCTURE_REF_PATTERN.test(name) && !containsUglyStructureRef(name)) {
|
|
416
|
+
reservedNames.add(name);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const structureUsages = findStructureUsages(schemas);
|
|
420
|
+
const structureMapping = /* @__PURE__ */ new Map();
|
|
421
|
+
const usedNames = new Set(reservedNames);
|
|
422
|
+
for (const [structureRef, usages] of structureUsages) {
|
|
423
|
+
if (usages.length === 0) continue;
|
|
424
|
+
const { parent, property } = usages[0];
|
|
425
|
+
const propertyPascal = toPascalCase(property);
|
|
426
|
+
const newName = findUniqueName(propertyPascal, parent, usedNames);
|
|
427
|
+
structureMapping.set(structureRef, newName);
|
|
428
|
+
usedNames.add(newName);
|
|
429
|
+
}
|
|
430
|
+
const normalizedDefinitions = {};
|
|
431
|
+
for (const [originalName, schema] of Object.entries(schemas.definitions)) {
|
|
432
|
+
const newName = resolveNewSchemaName(originalName, structureMapping);
|
|
433
|
+
if (newName === null) continue;
|
|
434
|
+
const updatedSchema = updateRefsWithMapping(schema, structureMapping);
|
|
435
|
+
normalizedDefinitions[newName] = updatedSchema;
|
|
436
|
+
}
|
|
437
|
+
return { definitions: normalizedDefinitions };
|
|
438
|
+
};
|
|
439
|
+
const resolveNewSchemaName = (originalName, structureMapping) => {
|
|
440
|
+
if (STRUCTURE_REF_PATTERN.test(originalName)) {
|
|
441
|
+
return structureMapping.get(originalName) ?? null;
|
|
442
|
+
}
|
|
443
|
+
if (containsUglyStructureRef(originalName)) {
|
|
444
|
+
for (const [structureRef, mappedName] of structureMapping) {
|
|
445
|
+
if (originalName.includes(structureRef)) {
|
|
446
|
+
return replaceStructureInName(originalName, structureRef, mappedName);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return originalName;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const extractReferencedSchemas = (paths) => {
|
|
454
|
+
const refs = /* @__PURE__ */ new Set();
|
|
455
|
+
const extractFromSchema = (schema) => {
|
|
456
|
+
if (!schema) return;
|
|
457
|
+
if (schema.$ref) {
|
|
458
|
+
const match = schema.$ref.match(/^#\/components\/schemas\/(.+)$/);
|
|
459
|
+
if (match) {
|
|
460
|
+
refs.add(match[1]);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (schema.items) {
|
|
464
|
+
extractFromSchema(schema.items);
|
|
465
|
+
}
|
|
466
|
+
if (schema.oneOf) {
|
|
467
|
+
schema.oneOf.forEach(extractFromSchema);
|
|
468
|
+
}
|
|
469
|
+
if (schema.properties) {
|
|
470
|
+
Object.values(schema.properties).forEach(extractFromSchema);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
for (const pathMethods of Object.values(paths)) {
|
|
474
|
+
for (const operation of Object.values(pathMethods)) {
|
|
475
|
+
operation.parameters?.forEach((param) => {
|
|
476
|
+
extractFromSchema(param.schema);
|
|
477
|
+
});
|
|
478
|
+
if (operation.requestBody?.content) {
|
|
479
|
+
Object.values(operation.requestBody.content).forEach((content) => {
|
|
480
|
+
extractFromSchema(content.schema);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
Object.values(operation.responses).forEach((response) => {
|
|
484
|
+
if (response.content) {
|
|
485
|
+
Object.values(response.content).forEach((content) => {
|
|
486
|
+
extractFromSchema(content.schema);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return refs;
|
|
493
|
+
};
|
|
494
|
+
const extractNestedReferences = (schemas, knownSchemas) => {
|
|
495
|
+
const refs = /* @__PURE__ */ new Set();
|
|
496
|
+
const extractFromSchema = (schema) => {
|
|
497
|
+
if (!schema) return;
|
|
498
|
+
if (schema.$ref) {
|
|
499
|
+
const match = schema.$ref.match(
|
|
500
|
+
/^#\/(?:components\/schemas|definitions)\/(.+)$/
|
|
501
|
+
);
|
|
502
|
+
if (match && !knownSchemas.has(match[1])) {
|
|
503
|
+
refs.add(match[1]);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (schema.items) {
|
|
507
|
+
extractFromSchema(schema.items);
|
|
508
|
+
}
|
|
509
|
+
if (schema.oneOf) {
|
|
510
|
+
schema.oneOf.forEach(extractFromSchema);
|
|
511
|
+
}
|
|
512
|
+
if (schema.anyOf) {
|
|
513
|
+
schema.anyOf.forEach(extractFromSchema);
|
|
514
|
+
}
|
|
515
|
+
if (schema.allOf) {
|
|
516
|
+
schema.allOf.forEach(extractFromSchema);
|
|
517
|
+
}
|
|
518
|
+
if (schema.properties) {
|
|
519
|
+
Object.values(schema.properties).forEach(extractFromSchema);
|
|
520
|
+
}
|
|
521
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
522
|
+
extractFromSchema(schema.additionalProperties);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
for (const schema of Object.values(schemas)) {
|
|
526
|
+
extractFromSchema(schema);
|
|
527
|
+
}
|
|
528
|
+
return refs;
|
|
529
|
+
};
|
|
530
|
+
const convertToOpenApiSchema = (schema) => {
|
|
531
|
+
const result = {};
|
|
532
|
+
if (schema.type) result["type"] = schema.type;
|
|
533
|
+
if (schema.format) result["format"] = schema.format;
|
|
534
|
+
if (schema.$ref) {
|
|
535
|
+
result["$ref"] = schema.$ref.replace(
|
|
536
|
+
"#/definitions/",
|
|
537
|
+
"#/components/schemas/"
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
if (schema.description) result["description"] = schema.description;
|
|
541
|
+
if (schema.enum) result["enum"] = schema.enum;
|
|
542
|
+
if (schema.const !== void 0) result["const"] = schema.const;
|
|
543
|
+
if (schema.minLength !== void 0) result["minLength"] = schema.minLength;
|
|
544
|
+
if (schema.maxLength !== void 0) result["maxLength"] = schema.maxLength;
|
|
545
|
+
if (schema.pattern !== void 0) result["pattern"] = schema.pattern;
|
|
546
|
+
if (schema.minimum !== void 0) result["minimum"] = schema.minimum;
|
|
547
|
+
if (schema.maximum !== void 0) result["maximum"] = schema.maximum;
|
|
548
|
+
if (schema.exclusiveMinimum !== void 0)
|
|
549
|
+
result["exclusiveMinimum"] = schema.exclusiveMinimum;
|
|
550
|
+
if (schema.exclusiveMaximum !== void 0)
|
|
551
|
+
result["exclusiveMaximum"] = schema.exclusiveMaximum;
|
|
552
|
+
if (schema.minItems !== void 0) result["minItems"] = schema.minItems;
|
|
553
|
+
if (schema.maxItems !== void 0) result["maxItems"] = schema.maxItems;
|
|
554
|
+
if (schema.default !== void 0) result["default"] = schema.default;
|
|
555
|
+
if (schema.items) {
|
|
556
|
+
result["items"] = convertToOpenApiSchema(schema.items);
|
|
557
|
+
}
|
|
558
|
+
if (schema.oneOf) {
|
|
559
|
+
result["oneOf"] = schema.oneOf.map(convertToOpenApiSchema);
|
|
560
|
+
}
|
|
561
|
+
if (schema.anyOf) {
|
|
562
|
+
result["anyOf"] = schema.anyOf.map(convertToOpenApiSchema);
|
|
563
|
+
}
|
|
564
|
+
if (schema.allOf) {
|
|
565
|
+
result["allOf"] = schema.allOf.map(convertToOpenApiSchema);
|
|
566
|
+
}
|
|
567
|
+
if (schema.properties) {
|
|
568
|
+
result["properties"] = Object.fromEntries(
|
|
569
|
+
Object.entries(schema.properties).map(([key, value]) => [
|
|
570
|
+
key,
|
|
571
|
+
convertToOpenApiSchema(value)
|
|
572
|
+
])
|
|
573
|
+
);
|
|
574
|
+
if (schema.additionalProperties === void 0) {
|
|
575
|
+
result["additionalProperties"] = false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (schema.additionalProperties !== void 0) {
|
|
579
|
+
if (typeof schema.additionalProperties === "boolean") {
|
|
580
|
+
result["additionalProperties"] = schema.additionalProperties;
|
|
581
|
+
} else {
|
|
582
|
+
result["additionalProperties"] = convertToOpenApiSchema(
|
|
583
|
+
schema.additionalProperties
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (schema.required) {
|
|
588
|
+
result["required"] = [...schema.required];
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
};
|
|
592
|
+
const mergeSchemas = (paths, generatedSchemas) => {
|
|
593
|
+
const referencedSchemas = extractReferencedSchemas(paths);
|
|
594
|
+
const schemas = {};
|
|
595
|
+
const processedSchemas = /* @__PURE__ */ new Set();
|
|
596
|
+
const toProcess = [...referencedSchemas];
|
|
597
|
+
while (toProcess.length > 0) {
|
|
598
|
+
const schemaName = toProcess.pop();
|
|
599
|
+
if (processedSchemas.has(schemaName)) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
processedSchemas.add(schemaName);
|
|
603
|
+
const jsonSchema = generatedSchemas.definitions[schemaName];
|
|
604
|
+
if (jsonSchema) {
|
|
605
|
+
schemas[schemaName] = convertToOpenApiSchema(jsonSchema);
|
|
606
|
+
const nestedRefs = extractNestedReferences(
|
|
607
|
+
{ [schemaName]: jsonSchema },
|
|
608
|
+
processedSchemas
|
|
609
|
+
);
|
|
610
|
+
for (const ref of nestedRefs) {
|
|
611
|
+
if (!processedSchemas.has(ref)) {
|
|
612
|
+
toProcess.push(ref);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return { paths, schemas };
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const createDecoratorFilter = (excludeDecorators) => {
|
|
621
|
+
if (excludeDecorators.length === 0) {
|
|
622
|
+
return () => true;
|
|
623
|
+
}
|
|
624
|
+
const excludeSet = new Set(excludeDecorators);
|
|
625
|
+
return (method) => {
|
|
626
|
+
const hasExcludedDecorator = method.decorators.some(
|
|
627
|
+
(decorator) => excludeSet.has(decorator)
|
|
628
|
+
);
|
|
629
|
+
return !hasExcludedDecorator;
|
|
630
|
+
};
|
|
631
|
+
};
|
|
632
|
+
const createPathFilter = (pathFilter) => {
|
|
633
|
+
if (typeof pathFilter === "function") {
|
|
634
|
+
return (method) => pathFilter(method.path);
|
|
635
|
+
}
|
|
636
|
+
return (method) => pathFilter.test(method.path);
|
|
637
|
+
};
|
|
638
|
+
const combineFilters = (filters) => {
|
|
639
|
+
if (filters.length === 0) {
|
|
640
|
+
return () => true;
|
|
641
|
+
}
|
|
642
|
+
if (filters.length === 1) {
|
|
643
|
+
return filters[0];
|
|
644
|
+
}
|
|
645
|
+
return (method) => filters.every((filter) => filter(method));
|
|
646
|
+
};
|
|
647
|
+
const createFilterPipeline = (options) => {
|
|
648
|
+
const filters = [];
|
|
649
|
+
if (options.excludeDecorators && options.excludeDecorators.length > 0) {
|
|
650
|
+
filters.push(createDecoratorFilter(options.excludeDecorators));
|
|
651
|
+
}
|
|
652
|
+
if (options.pathFilter) {
|
|
653
|
+
filters.push(createPathFilter(options.pathFilter));
|
|
654
|
+
}
|
|
655
|
+
if (filters.length === 0) {
|
|
656
|
+
return void 0;
|
|
657
|
+
}
|
|
658
|
+
return combineFilters(filters);
|
|
659
|
+
};
|
|
660
|
+
const filterMethods = (methods, options) => {
|
|
661
|
+
const filter = createFilterPipeline(options);
|
|
662
|
+
if (!filter) {
|
|
663
|
+
return methods;
|
|
664
|
+
}
|
|
665
|
+
return methods.filter(filter);
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const transformSchemaToV31 = (schema) => {
|
|
669
|
+
const transformedOneOf = schema.oneOf?.map(transformSchemaToV31);
|
|
670
|
+
const transformedAnyOf = schema.anyOf?.map(transformSchemaToV31);
|
|
671
|
+
const transformedAllOf = schema.allOf?.map(transformSchemaToV31);
|
|
672
|
+
const transformedItems = schema.items ? transformSchemaToV31(schema.items) : void 0;
|
|
673
|
+
const transformedProperties = schema.properties ? Object.fromEntries(
|
|
674
|
+
Object.entries(schema.properties).map(([key, value]) => [
|
|
675
|
+
key,
|
|
676
|
+
transformSchemaToV31(value)
|
|
677
|
+
])
|
|
678
|
+
) : void 0;
|
|
679
|
+
const hasNullable = schema.nullable && schema.type && typeof schema.type === "string";
|
|
680
|
+
const transformedType = hasNullable ? [schema.type, "null"] : schema.type;
|
|
681
|
+
const { nullable: _nullable, ...restWithoutNullable } = schema;
|
|
682
|
+
return {
|
|
683
|
+
...restWithoutNullable,
|
|
684
|
+
type: transformedType,
|
|
685
|
+
...transformedOneOf && { oneOf: transformedOneOf },
|
|
686
|
+
...transformedAnyOf && { anyOf: transformedAnyOf },
|
|
687
|
+
...transformedAllOf && { allOf: transformedAllOf },
|
|
688
|
+
...transformedItems && { items: transformedItems },
|
|
689
|
+
...transformedProperties && { properties: transformedProperties }
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
const transformSchemasForVersion = (schemas, version) => {
|
|
693
|
+
if (version === "3.0.3") {
|
|
694
|
+
return schemas;
|
|
695
|
+
}
|
|
696
|
+
return Object.fromEntries(
|
|
697
|
+
Object.entries(schemas).map(([key, schema]) => [
|
|
698
|
+
key,
|
|
699
|
+
transformSchemaToV31(schema)
|
|
700
|
+
])
|
|
701
|
+
);
|
|
702
|
+
};
|
|
703
|
+
const transformSpecForVersion = (spec, version) => {
|
|
704
|
+
if (version === "3.0.3") {
|
|
705
|
+
return { ...spec, openapi: version };
|
|
706
|
+
}
|
|
707
|
+
const transformedSchemas = spec.components?.schemas ? transformSchemasForVersion(spec.components.schemas, version) : void 0;
|
|
708
|
+
return {
|
|
709
|
+
...spec,
|
|
710
|
+
openapi: version,
|
|
711
|
+
...transformedSchemas && {
|
|
712
|
+
components: {
|
|
713
|
+
...spec.components,
|
|
714
|
+
schemas: transformedSchemas
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const ParameterLocation = Schema.Literal(
|
|
721
|
+
"path",
|
|
722
|
+
"query",
|
|
723
|
+
"header",
|
|
724
|
+
"cookie",
|
|
725
|
+
"body"
|
|
726
|
+
);
|
|
727
|
+
const ResolvedParameter = Schema.Struct({
|
|
728
|
+
name: Schema.String,
|
|
729
|
+
location: ParameterLocation,
|
|
730
|
+
tsType: Schema.String,
|
|
731
|
+
required: Schema.Boolean,
|
|
732
|
+
description: Schema.OptionFromNullOr(Schema.String)
|
|
733
|
+
// Note: constraints uses plain interface to avoid Schema initialization overhead
|
|
734
|
+
});
|
|
735
|
+
const ReturnTypeInfo = Schema.Struct({
|
|
736
|
+
type: Schema.OptionFromNullOr(Schema.String),
|
|
737
|
+
inline: Schema.OptionFromNullOr(Schema.String),
|
|
738
|
+
container: Schema.OptionFromNullOr(Schema.Literal("array")),
|
|
739
|
+
filePath: Schema.OptionFromNullOr(Schema.String)
|
|
740
|
+
});
|
|
741
|
+
const HttpMethod = Schema.Literal(
|
|
742
|
+
"GET",
|
|
743
|
+
"POST",
|
|
744
|
+
"PUT",
|
|
745
|
+
"PATCH",
|
|
746
|
+
"DELETE",
|
|
747
|
+
"OPTIONS",
|
|
748
|
+
"HEAD",
|
|
749
|
+
"ALL"
|
|
750
|
+
);
|
|
751
|
+
const OperationMetadata = Schema.Struct({
|
|
752
|
+
/** Custom summary from @ApiOperation({ summary: '...' }) */
|
|
753
|
+
summary: Schema.OptionFromNullOr(Schema.String),
|
|
754
|
+
/** Description from @ApiOperation({ description: '...' }) */
|
|
755
|
+
description: Schema.OptionFromNullOr(Schema.String),
|
|
756
|
+
/** Custom operationId from @ApiOperation({ operationId: '...' }) */
|
|
757
|
+
operationId: Schema.OptionFromNullOr(Schema.String),
|
|
758
|
+
/** Deprecated flag from @ApiOperation({ deprecated: true }) */
|
|
759
|
+
deprecated: Schema.OptionFromNullOr(Schema.Boolean)
|
|
760
|
+
});
|
|
761
|
+
const ResponseMetadata = Schema.Struct({
|
|
762
|
+
/** HTTP status code (e.g., 200, 201, 400, 404) */
|
|
763
|
+
statusCode: Schema.Number,
|
|
764
|
+
/** Response description */
|
|
765
|
+
description: Schema.OptionFromNullOr(Schema.String),
|
|
766
|
+
/** Response type name (e.g., "UserDto") */
|
|
767
|
+
type: Schema.OptionFromNullOr(Schema.String),
|
|
768
|
+
/** Whether the response type is an array */
|
|
769
|
+
isArray: Schema.Boolean
|
|
770
|
+
});
|
|
771
|
+
const MethodSecurityRequirement = Schema.Struct({
|
|
772
|
+
/** Security scheme name (e.g., 'bearer', 'jwt', 'oauth2') */
|
|
773
|
+
schemeName: Schema.String,
|
|
774
|
+
/** Required scopes (empty array for schemes without scopes) */
|
|
775
|
+
scopes: Schema.Array(Schema.String)
|
|
776
|
+
});
|
|
777
|
+
Schema.Struct({
|
|
778
|
+
httpMethod: HttpMethod,
|
|
779
|
+
path: Schema.String,
|
|
780
|
+
methodName: Schema.String,
|
|
781
|
+
controllerName: Schema.String,
|
|
782
|
+
controllerTags: Schema.Array(Schema.String),
|
|
783
|
+
returnType: ReturnTypeInfo,
|
|
784
|
+
parameters: Schema.Array(ResolvedParameter),
|
|
785
|
+
/** All decorator names on the method (for filtering) */
|
|
786
|
+
decorators: Schema.Array(Schema.String),
|
|
787
|
+
/** Metadata from @ApiOperation decorator */
|
|
788
|
+
operation: OperationMetadata,
|
|
789
|
+
/** Response metadata from @ApiResponse decorators */
|
|
790
|
+
responses: Schema.Array(ResponseMetadata),
|
|
791
|
+
/** Custom HTTP code from @HttpCode decorator */
|
|
792
|
+
httpCode: Schema.OptionFromNullOr(Schema.Number),
|
|
793
|
+
/** Content types from @ApiConsumes decorator (request body content types) */
|
|
794
|
+
consumes: Schema.Array(Schema.String),
|
|
795
|
+
/** Content types from @ApiProduces decorator (response content types) */
|
|
796
|
+
produces: Schema.Array(Schema.String),
|
|
797
|
+
/**
|
|
798
|
+
* Security requirements from decorators (@ApiBearerAuth, @ApiSecurity, etc.)
|
|
799
|
+
* Combines controller-level and method-level security.
|
|
800
|
+
* Multiple requirements = AND logic (all required).
|
|
801
|
+
* Empty array = no security decorators found (inherits global security).
|
|
802
|
+
*/
|
|
803
|
+
security: Schema.Array(MethodSecurityRequirement)
|
|
804
|
+
});
|
|
805
|
+
const OpenApiSchemaObject = Schema.suspend(
|
|
806
|
+
() => Schema.Struct({
|
|
807
|
+
type: Schema.optional(Schema.String),
|
|
808
|
+
format: Schema.optional(Schema.String),
|
|
809
|
+
$ref: Schema.optional(Schema.String),
|
|
810
|
+
oneOf: Schema.optional(Schema.Array(OpenApiSchemaObject)),
|
|
811
|
+
items: Schema.optional(OpenApiSchemaObject),
|
|
812
|
+
properties: Schema.optional(
|
|
813
|
+
Schema.Record({ key: Schema.String, value: OpenApiSchemaObject })
|
|
814
|
+
),
|
|
815
|
+
required: Schema.optional(Schema.Array(Schema.String))
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
const OpenApiParameter = Schema.Struct({
|
|
819
|
+
name: Schema.String,
|
|
820
|
+
in: Schema.Literal("path", "query", "header", "cookie"),
|
|
821
|
+
description: Schema.optional(Schema.String),
|
|
822
|
+
required: Schema.Boolean,
|
|
823
|
+
schema: OpenApiSchemaObject
|
|
824
|
+
});
|
|
825
|
+
const OpenApiRequestBody = Schema.Struct({
|
|
826
|
+
description: Schema.optional(Schema.String),
|
|
827
|
+
required: Schema.optional(Schema.Boolean),
|
|
828
|
+
content: Schema.Record({
|
|
829
|
+
key: Schema.String,
|
|
830
|
+
value: Schema.Struct({
|
|
831
|
+
schema: Schema.Unknown
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
});
|
|
835
|
+
const OpenApiResponse = Schema.Struct({
|
|
836
|
+
description: Schema.String,
|
|
837
|
+
content: Schema.optional(
|
|
838
|
+
Schema.Record({
|
|
839
|
+
key: Schema.String,
|
|
840
|
+
value: Schema.Struct({
|
|
841
|
+
schema: Schema.Unknown
|
|
842
|
+
})
|
|
843
|
+
})
|
|
844
|
+
)
|
|
845
|
+
});
|
|
846
|
+
Schema.Struct({
|
|
847
|
+
operationId: Schema.String,
|
|
848
|
+
summary: Schema.optional(Schema.String),
|
|
849
|
+
description: Schema.optional(Schema.String),
|
|
850
|
+
deprecated: Schema.optional(Schema.Boolean),
|
|
851
|
+
parameters: Schema.optional(Schema.Array(OpenApiParameter)),
|
|
852
|
+
requestBody: Schema.optional(OpenApiRequestBody),
|
|
853
|
+
responses: Schema.Record({
|
|
854
|
+
key: Schema.String,
|
|
855
|
+
value: OpenApiResponse
|
|
856
|
+
}),
|
|
857
|
+
tags: Schema.optional(Schema.Array(Schema.String)),
|
|
858
|
+
/** Per-operation security requirements */
|
|
859
|
+
security: Schema.optional(
|
|
860
|
+
Schema.Array(
|
|
861
|
+
Schema.Record({ key: Schema.String, value: Schema.Array(Schema.String) })
|
|
862
|
+
)
|
|
863
|
+
)
|
|
864
|
+
});
|
|
865
|
+
const OpenApiContactConfig = Schema.Struct({
|
|
866
|
+
name: Schema.optional(Schema.String),
|
|
867
|
+
email: Schema.optional(Schema.String),
|
|
868
|
+
url: Schema.optional(Schema.String)
|
|
869
|
+
});
|
|
870
|
+
const OpenApiLicenseConfig = Schema.Struct({
|
|
871
|
+
name: Schema.String,
|
|
872
|
+
url: Schema.optional(Schema.String)
|
|
873
|
+
});
|
|
874
|
+
const OpenApiInfoConfig = Schema.Struct({
|
|
875
|
+
title: Schema.String,
|
|
876
|
+
version: Schema.String,
|
|
877
|
+
description: Schema.optional(Schema.String),
|
|
878
|
+
contact: Schema.optional(OpenApiContactConfig),
|
|
879
|
+
license: Schema.optional(OpenApiLicenseConfig)
|
|
880
|
+
});
|
|
881
|
+
const OpenApiServerConfig = Schema.Struct({
|
|
882
|
+
url: Schema.String,
|
|
883
|
+
description: Schema.optional(Schema.String)
|
|
884
|
+
});
|
|
885
|
+
const OpenApiTagConfig = Schema.Struct({
|
|
886
|
+
name: Schema.String,
|
|
887
|
+
description: Schema.optional(Schema.String)
|
|
888
|
+
});
|
|
889
|
+
const SecuritySchemeType = Schema.Literal(
|
|
890
|
+
"apiKey",
|
|
891
|
+
"http",
|
|
892
|
+
"oauth2",
|
|
893
|
+
"openIdConnect"
|
|
894
|
+
);
|
|
895
|
+
const SecuritySchemeIn = Schema.Literal("query", "header", "cookie");
|
|
896
|
+
const SecuritySchemeConfig = Schema.Struct({
|
|
897
|
+
name: Schema.String,
|
|
898
|
+
type: SecuritySchemeType,
|
|
899
|
+
scheme: Schema.optional(Schema.String),
|
|
900
|
+
bearerFormat: Schema.optional(Schema.String),
|
|
901
|
+
in: Schema.optional(SecuritySchemeIn),
|
|
902
|
+
parameterName: Schema.optional(Schema.String),
|
|
903
|
+
description: Schema.optional(Schema.String)
|
|
904
|
+
});
|
|
905
|
+
const SecurityRequirement = Schema.Record({
|
|
906
|
+
key: Schema.String,
|
|
907
|
+
value: Schema.Array(Schema.String)
|
|
908
|
+
});
|
|
909
|
+
const OutputFormat = Schema.Literal("json", "yaml");
|
|
910
|
+
const FilesConfig = Schema.Struct({
|
|
911
|
+
entry: Schema.optional(
|
|
912
|
+
Schema.Union(Schema.String, Schema.Array(Schema.String))
|
|
913
|
+
),
|
|
914
|
+
tsconfig: Schema.optional(Schema.String),
|
|
915
|
+
dtoGlob: Schema.optional(
|
|
916
|
+
Schema.Union(Schema.String, Schema.Array(Schema.String))
|
|
917
|
+
),
|
|
918
|
+
include: Schema.optional(Schema.Array(Schema.String)),
|
|
919
|
+
exclude: Schema.optional(Schema.Array(Schema.String))
|
|
920
|
+
});
|
|
921
|
+
const SecurityConfig = Schema.Struct({
|
|
922
|
+
schemes: Schema.optional(Schema.Array(SecuritySchemeConfig)),
|
|
923
|
+
global: Schema.optional(Schema.Array(SecurityRequirement))
|
|
924
|
+
});
|
|
925
|
+
const OpenApiVersion = Schema.Literal("3.0.3", "3.1.0", "3.2.0");
|
|
926
|
+
const OpenApiConfig = Schema.Struct({
|
|
927
|
+
version: Schema.optional(OpenApiVersion),
|
|
928
|
+
info: OpenApiInfoConfig,
|
|
929
|
+
servers: Schema.optional(Schema.Array(OpenApiServerConfig)),
|
|
930
|
+
tags: Schema.optional(Schema.Array(OpenApiTagConfig)),
|
|
931
|
+
security: Schema.optional(SecurityConfig)
|
|
932
|
+
});
|
|
933
|
+
const QueryOptionsConfig = Schema.Struct({
|
|
934
|
+
style: Schema.optional(Schema.Literal("inline", "ref"))
|
|
935
|
+
});
|
|
936
|
+
const PathFilterFunction = Schema.declare(
|
|
937
|
+
(input) => typeof input === "function",
|
|
938
|
+
{
|
|
939
|
+
identifier: "PathFilterFunction",
|
|
940
|
+
description: "A function that takes a path string and returns a boolean"
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
const PathFilter = Schema.Union(
|
|
944
|
+
Schema.instanceOf(RegExp),
|
|
945
|
+
PathFilterFunction
|
|
946
|
+
);
|
|
947
|
+
const OptionsConfig = Schema.Struct({
|
|
948
|
+
basePath: Schema.optional(Schema.String),
|
|
949
|
+
extractValidation: Schema.optional(Schema.Boolean),
|
|
950
|
+
excludeDecorators: Schema.optional(Schema.Array(Schema.String)),
|
|
951
|
+
query: Schema.optional(QueryOptionsConfig),
|
|
952
|
+
pathFilter: Schema.optional(PathFilter)
|
|
953
|
+
});
|
|
954
|
+
const OpenApiGeneratorConfig = Schema.Struct({
|
|
955
|
+
extends: Schema.optional(Schema.String),
|
|
956
|
+
files: Schema.optional(FilesConfig),
|
|
957
|
+
output: Schema.String,
|
|
958
|
+
format: Schema.optional(OutputFormat),
|
|
959
|
+
openapi: OpenApiConfig,
|
|
960
|
+
options: Schema.optional(OptionsConfig)
|
|
961
|
+
});
|
|
962
|
+
Schema.Struct({
|
|
963
|
+
tsconfig: Schema.String,
|
|
964
|
+
entry: Schema.Array(Schema.String),
|
|
965
|
+
include: Schema.Array(Schema.String),
|
|
966
|
+
exclude: Schema.Array(Schema.String),
|
|
967
|
+
excludeDecorators: Schema.Array(Schema.String),
|
|
968
|
+
dtoGlob: Schema.Array(Schema.String),
|
|
969
|
+
extractValidation: Schema.Boolean,
|
|
970
|
+
basePath: Schema.optional(Schema.String),
|
|
971
|
+
pathFilter: Schema.optional(PathFilter),
|
|
972
|
+
version: Schema.optional(Schema.String),
|
|
973
|
+
info: OpenApiInfoConfig,
|
|
974
|
+
servers: Schema.Array(OpenApiServerConfig),
|
|
975
|
+
securitySchemes: Schema.Array(SecuritySchemeConfig),
|
|
976
|
+
securityRequirements: Schema.Array(SecurityRequirement),
|
|
977
|
+
tags: Schema.Array(OpenApiTagConfig),
|
|
978
|
+
output: Schema.String,
|
|
979
|
+
format: OutputFormat
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const deepMerge = (parent, child) => {
|
|
983
|
+
const result = { ...parent };
|
|
984
|
+
for (const key of Object.keys(child)) {
|
|
985
|
+
const childValue = child[key];
|
|
986
|
+
const parentValue = parent[key];
|
|
987
|
+
if (childValue === void 0) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (childValue !== null && typeof childValue === "object" && !Array.isArray(childValue) && parentValue !== null && typeof parentValue === "object" && !Array.isArray(parentValue)) {
|
|
991
|
+
result[key] = deepMerge(
|
|
992
|
+
parentValue,
|
|
993
|
+
childValue
|
|
994
|
+
);
|
|
995
|
+
} else {
|
|
996
|
+
result[key] = childValue;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return result;
|
|
1000
|
+
};
|
|
1001
|
+
function defineConfig(config) {
|
|
1002
|
+
return config;
|
|
1003
|
+
}
|
|
1004
|
+
const CONFIG_FILE_NAMES = [
|
|
1005
|
+
"openapi.config.ts",
|
|
1006
|
+
"openapi.config.js",
|
|
1007
|
+
"openapi.config.mjs",
|
|
1008
|
+
"openapi.config.cjs"
|
|
1009
|
+
];
|
|
1010
|
+
const DEFAULT_ENTRY$1 = "src/app.module.ts";
|
|
1011
|
+
const DEFAULT_DTO_GLOB = [
|
|
1012
|
+
"**/*.dto.ts",
|
|
1013
|
+
"**/*.entity.ts",
|
|
1014
|
+
"**/*.model.ts",
|
|
1015
|
+
"**/*.schema.ts"
|
|
1016
|
+
];
|
|
1017
|
+
const DEFAULT_CONFIG = {
|
|
1018
|
+
files: {
|
|
1019
|
+
include: [],
|
|
1020
|
+
exclude: ["**/*.spec.ts", "**/*.test.ts", "**/node_modules/**"]
|
|
1021
|
+
},
|
|
1022
|
+
options: {
|
|
1023
|
+
excludeDecorators: ["ApiExcludeEndpoint", "ApiExcludeController"],
|
|
1024
|
+
extractValidation: true
|
|
1025
|
+
},
|
|
1026
|
+
format: "json",
|
|
1027
|
+
openapi: {
|
|
1028
|
+
servers: [],
|
|
1029
|
+
tags: [],
|
|
1030
|
+
security: {
|
|
1031
|
+
schemes: [],
|
|
1032
|
+
global: []
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
const findConfigFile = (startDir = process.cwd()) => Effect.gen(function* () {
|
|
1037
|
+
let currentDir = resolve(startDir);
|
|
1038
|
+
const root = dirname(currentDir);
|
|
1039
|
+
while (currentDir !== root) {
|
|
1040
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
1041
|
+
const configPath = resolve(currentDir, fileName);
|
|
1042
|
+
if (existsSync(configPath)) {
|
|
1043
|
+
return configPath;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const parentDir = dirname(currentDir);
|
|
1047
|
+
if (parentDir === currentDir) break;
|
|
1048
|
+
currentDir = parentDir;
|
|
1049
|
+
}
|
|
1050
|
+
return yield* ConfigNotFoundError.notFound(startDir);
|
|
1051
|
+
});
|
|
1052
|
+
const validateConfig = (config, filePath) => Schema.decodeUnknown(OpenApiGeneratorConfig)(config).pipe(
|
|
1053
|
+
Effect.mapError((parseError) => {
|
|
1054
|
+
const issues = parseError.message.split("\n").filter((line) => line.trim());
|
|
1055
|
+
return ConfigValidationError.fromIssues(filePath, issues);
|
|
1056
|
+
})
|
|
1057
|
+
);
|
|
1058
|
+
const unwrapTsxDoubleDefault = (value) => value && typeof value === "object" && "default" in value && Object.keys(value).length === 1 ? value.default : value;
|
|
1059
|
+
const loadRawConfigFromFile = (configPath) => Effect.gen(function* () {
|
|
1060
|
+
const absolutePath = resolve(configPath);
|
|
1061
|
+
if (!existsSync(absolutePath)) {
|
|
1062
|
+
return yield* ConfigNotFoundError.pathNotFound(absolutePath);
|
|
1063
|
+
}
|
|
1064
|
+
yield* Effect.logDebug("Loading config file").pipe(
|
|
1065
|
+
Effect.annotateLogs({ path: absolutePath })
|
|
1066
|
+
);
|
|
1067
|
+
const module = yield* Effect.tryPromise({
|
|
1068
|
+
try: async () => {
|
|
1069
|
+
const fileUrl = pathToFileURL(absolutePath).href;
|
|
1070
|
+
const cacheBustUrl = `${fileUrl}?t=${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1071
|
+
return await import(cacheBustUrl);
|
|
1072
|
+
},
|
|
1073
|
+
catch: (error) => ConfigLoadError.importFailed(absolutePath, error)
|
|
1074
|
+
});
|
|
1075
|
+
const rawConfig = unwrapTsxDoubleDefault(module.default ?? module.config);
|
|
1076
|
+
if (!rawConfig) {
|
|
1077
|
+
return yield* ConfigLoadError.noExport(absolutePath);
|
|
1078
|
+
}
|
|
1079
|
+
return rawConfig;
|
|
1080
|
+
});
|
|
1081
|
+
const resolveConfigExtends = (rawConfig, configPath, visited = /* @__PURE__ */ new Set()) => Effect.gen(function* () {
|
|
1082
|
+
const absolutePath = resolve(configPath);
|
|
1083
|
+
if (visited.has(absolutePath)) {
|
|
1084
|
+
return yield* ConfigLoadError.importFailed(
|
|
1085
|
+
absolutePath,
|
|
1086
|
+
new Error(`Circular extends detected: ${absolutePath}`)
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
visited.add(absolutePath);
|
|
1090
|
+
const extendsPath = rawConfig.extends;
|
|
1091
|
+
if (!extendsPath) {
|
|
1092
|
+
return rawConfig;
|
|
1093
|
+
}
|
|
1094
|
+
const parentConfigPath = resolve(dirname(absolutePath), extendsPath);
|
|
1095
|
+
yield* Effect.logDebug("Resolving config extends").pipe(
|
|
1096
|
+
Effect.annotateLogs({ parent: parentConfigPath })
|
|
1097
|
+
);
|
|
1098
|
+
const parentRawConfig = yield* loadRawConfigFromFile(parentConfigPath);
|
|
1099
|
+
const resolvedParent = yield* resolveConfigExtends(
|
|
1100
|
+
parentRawConfig,
|
|
1101
|
+
parentConfigPath,
|
|
1102
|
+
visited
|
|
1103
|
+
);
|
|
1104
|
+
const { extends: _, ...childWithoutExtends } = rawConfig;
|
|
1105
|
+
return deepMerge(
|
|
1106
|
+
resolvedParent,
|
|
1107
|
+
childWithoutExtends
|
|
1108
|
+
);
|
|
1109
|
+
});
|
|
1110
|
+
const loadConfigFromFile = (configPath) => Effect.gen(function* () {
|
|
1111
|
+
const rawConfig = yield* loadRawConfigFromFile(configPath);
|
|
1112
|
+
const mergedConfig = yield* resolveConfigExtends(rawConfig, configPath);
|
|
1113
|
+
return yield* validateConfig(mergedConfig, configPath);
|
|
1114
|
+
});
|
|
1115
|
+
const loadConfig = (configPath, cwd = process.cwd()) => Effect.gen(function* () {
|
|
1116
|
+
const resolvedPath = configPath ? Effect.succeed(configPath) : findConfigFile(cwd);
|
|
1117
|
+
const path = yield* resolvedPath;
|
|
1118
|
+
return yield* loadConfigFromFile(path);
|
|
1119
|
+
});
|
|
1120
|
+
const resolveConfig = (config) => {
|
|
1121
|
+
const files = config.files ?? {};
|
|
1122
|
+
const options = config.options ?? {};
|
|
1123
|
+
const openapi = config.openapi;
|
|
1124
|
+
const security = openapi.security ?? {};
|
|
1125
|
+
const rawEntry = files.entry ?? DEFAULT_ENTRY$1;
|
|
1126
|
+
const entry = Array.isArray(rawEntry) ? rawEntry : [rawEntry];
|
|
1127
|
+
const rawDtoGlob = files.dtoGlob;
|
|
1128
|
+
const dtoGlob = rawDtoGlob ? Array.isArray(rawDtoGlob) ? rawDtoGlob : [rawDtoGlob] : [...DEFAULT_DTO_GLOB];
|
|
1129
|
+
const tsconfig = files.tsconfig;
|
|
1130
|
+
if (!tsconfig) {
|
|
1131
|
+
throw new Error("tsconfig is required in files configuration");
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
tsconfig,
|
|
1135
|
+
entry,
|
|
1136
|
+
include: files.include ?? DEFAULT_CONFIG.files.include,
|
|
1137
|
+
exclude: files.exclude ?? DEFAULT_CONFIG.files.exclude,
|
|
1138
|
+
excludeDecorators: options.excludeDecorators ?? DEFAULT_CONFIG.options.excludeDecorators,
|
|
1139
|
+
dtoGlob,
|
|
1140
|
+
extractValidation: options.extractValidation ?? DEFAULT_CONFIG.options.extractValidation,
|
|
1141
|
+
basePath: options.basePath,
|
|
1142
|
+
version: openapi.version,
|
|
1143
|
+
info: openapi.info,
|
|
1144
|
+
servers: openapi.servers ?? DEFAULT_CONFIG.openapi.servers,
|
|
1145
|
+
securitySchemes: security.schemes ?? DEFAULT_CONFIG.openapi.security.schemes,
|
|
1146
|
+
securityRequirements: security.global ?? DEFAULT_CONFIG.openapi.security.global,
|
|
1147
|
+
tags: openapi.tags ?? DEFAULT_CONFIG.openapi.tags,
|
|
1148
|
+
output: config.output,
|
|
1149
|
+
format: config.format ?? DEFAULT_CONFIG.format
|
|
1150
|
+
};
|
|
1151
|
+
};
|
|
1152
|
+
const loadAndResolveConfig = (configPath, cwd = process.cwd()) => Effect.gen(function* () {
|
|
1153
|
+
const config = yield* loadConfig(configPath, cwd);
|
|
1154
|
+
return yield* Effect.try({
|
|
1155
|
+
try: () => resolveConfig(config),
|
|
1156
|
+
catch: (error) => ConfigValidationError.fromIssues(configPath ?? "unknown", [
|
|
1157
|
+
error instanceof Error ? error.message : String(error)
|
|
1158
|
+
])
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
function findRefs(obj, path = "") {
|
|
1163
|
+
const refs = [];
|
|
1164
|
+
if (!obj || typeof obj !== "object") {
|
|
1165
|
+
return refs;
|
|
1166
|
+
}
|
|
1167
|
+
const record = obj;
|
|
1168
|
+
if (typeof record.$ref === "string") {
|
|
1169
|
+
refs.push({ ref: record.$ref, path });
|
|
1170
|
+
}
|
|
1171
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1172
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
1173
|
+
refs.push(...findRefs(value, newPath));
|
|
1174
|
+
}
|
|
1175
|
+
return refs;
|
|
1176
|
+
}
|
|
1177
|
+
function validateSpec(spec) {
|
|
1178
|
+
const definedSchemas = new Set(Object.keys(spec.components?.schemas ?? {}));
|
|
1179
|
+
const allRefs = findRefs(spec);
|
|
1180
|
+
const schemaRefs = allRefs.filter(
|
|
1181
|
+
(r) => r.ref.startsWith("#/components/schemas/")
|
|
1182
|
+
);
|
|
1183
|
+
const brokenRefs = [];
|
|
1184
|
+
const missingSchemas = /* @__PURE__ */ new Map();
|
|
1185
|
+
for (const { ref, path } of schemaRefs) {
|
|
1186
|
+
const schemaName = ref.replace("#/components/schemas/", "");
|
|
1187
|
+
if (!definedSchemas.has(schemaName)) {
|
|
1188
|
+
brokenRefs.push({
|
|
1189
|
+
ref,
|
|
1190
|
+
path,
|
|
1191
|
+
missingSchema: schemaName
|
|
1192
|
+
});
|
|
1193
|
+
missingSchemas.set(schemaName, (missingSchemas.get(schemaName) ?? 0) + 1);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
valid: brokenRefs.length === 0,
|
|
1198
|
+
totalRefs: schemaRefs.length,
|
|
1199
|
+
brokenRefCount: brokenRefs.length,
|
|
1200
|
+
brokenRefs,
|
|
1201
|
+
missingSchemas
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function categorizeBrokenRefs(missingSchemas) {
|
|
1205
|
+
const primitives = [];
|
|
1206
|
+
const unionTypes = [];
|
|
1207
|
+
const queryParams = [];
|
|
1208
|
+
const other = [];
|
|
1209
|
+
const primitiveTypes = /* @__PURE__ */ new Set([
|
|
1210
|
+
"string",
|
|
1211
|
+
"number",
|
|
1212
|
+
"boolean",
|
|
1213
|
+
"object",
|
|
1214
|
+
"null",
|
|
1215
|
+
"undefined",
|
|
1216
|
+
"void",
|
|
1217
|
+
"any",
|
|
1218
|
+
"unknown",
|
|
1219
|
+
"never"
|
|
1220
|
+
]);
|
|
1221
|
+
for (const schema of missingSchemas.keys()) {
|
|
1222
|
+
if (primitiveTypes.has(schema.toLowerCase())) {
|
|
1223
|
+
primitives.push(schema);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (schema.includes(" | ") || schema.includes("|")) {
|
|
1227
|
+
unionTypes.push(schema);
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (schema.endsWith("QueryParams") || schema.endsWith("PathParams") || schema.endsWith("Params")) {
|
|
1231
|
+
queryParams.push(schema);
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
other.push(schema);
|
|
1235
|
+
}
|
|
1236
|
+
return {
|
|
1237
|
+
primitives,
|
|
1238
|
+
unionTypes,
|
|
1239
|
+
queryParams,
|
|
1240
|
+
other
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function formatValidationResult(result) {
|
|
1244
|
+
if (result.valid) {
|
|
1245
|
+
return `Spec is valid: ${result.totalRefs} schema refs, all resolved`;
|
|
1246
|
+
}
|
|
1247
|
+
const lines = [
|
|
1248
|
+
`Found ${result.brokenRefCount} broken refs (${result.missingSchemas.size} missing schemas):`
|
|
1249
|
+
];
|
|
1250
|
+
const categories = categorizeBrokenRefs(result.missingSchemas);
|
|
1251
|
+
if (categories.primitives.length > 0) {
|
|
1252
|
+
lines.push(`
|
|
1253
|
+
Primitive types (should not be $refs):`);
|
|
1254
|
+
for (const name of categories.primitives) {
|
|
1255
|
+
const count = result.missingSchemas.get(name) ?? 0;
|
|
1256
|
+
lines.push(` - ${name} (${count} usages)`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (categories.unionTypes.length > 0) {
|
|
1260
|
+
lines.push(`
|
|
1261
|
+
Union types (need special handling):`);
|
|
1262
|
+
for (const name of categories.unionTypes) {
|
|
1263
|
+
const count = result.missingSchemas.get(name) ?? 0;
|
|
1264
|
+
lines.push(` - ${name} (${count} usages)`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (categories.queryParams.length > 0) {
|
|
1268
|
+
lines.push(`
|
|
1269
|
+
Query/Path params (may need dtoGlob coverage):`);
|
|
1270
|
+
for (const name of categories.queryParams) {
|
|
1271
|
+
const count = result.missingSchemas.get(name) ?? 0;
|
|
1272
|
+
lines.push(` - ${name} (${count} usages)`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (categories.other.length > 0) {
|
|
1276
|
+
lines.push(`
|
|
1277
|
+
Other missing schemas (check dtoGlob patterns):`);
|
|
1278
|
+
for (const name of categories.other.slice(0, 20)) {
|
|
1279
|
+
const count = result.missingSchemas.get(name) ?? 0;
|
|
1280
|
+
lines.push(` - ${name} (${count} usages)`);
|
|
1281
|
+
}
|
|
1282
|
+
if (categories.other.length > 20) {
|
|
1283
|
+
lines.push(` ... and ${categories.other.length - 20} more`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return lines.join("\n");
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function resolveTypeLocations(project, missingTypes) {
|
|
1290
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1291
|
+
const typeIndex = buildTypeIndex(project);
|
|
1292
|
+
for (const typeName of missingTypes) {
|
|
1293
|
+
const baseTypeName = typeName.replace(/<.*>$/, "");
|
|
1294
|
+
const filePath = typeIndex.get(baseTypeName);
|
|
1295
|
+
if (filePath && !filePath.includes("node_modules")) {
|
|
1296
|
+
resolved.set(typeName, filePath);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return resolved;
|
|
1300
|
+
}
|
|
1301
|
+
function buildTypeIndex(project) {
|
|
1302
|
+
const index = /* @__PURE__ */ new Map();
|
|
1303
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
1304
|
+
const filePath = sourceFile.getFilePath();
|
|
1305
|
+
if (filePath.includes("node_modules")) continue;
|
|
1306
|
+
for (const cls of sourceFile.getClasses()) {
|
|
1307
|
+
if (cls.isExported()) {
|
|
1308
|
+
const name = cls.getName();
|
|
1309
|
+
if (name) {
|
|
1310
|
+
index.set(name, filePath);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
for (const iface of sourceFile.getInterfaces()) {
|
|
1315
|
+
if (iface.isExported()) {
|
|
1316
|
+
const name = iface.getName();
|
|
1317
|
+
index.set(name, filePath);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
1321
|
+
if (typeAlias.isExported()) {
|
|
1322
|
+
const name = typeAlias.getName();
|
|
1323
|
+
index.set(name, filePath);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
for (const enumDecl of sourceFile.getEnums()) {
|
|
1327
|
+
if (enumDecl.isExported()) {
|
|
1328
|
+
const name = enumDecl.getName();
|
|
1329
|
+
index.set(name, filePath);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return index;
|
|
1334
|
+
}
|
|
1335
|
+
function createTypeResolverProject(tsconfig) {
|
|
1336
|
+
return new Project({
|
|
1337
|
+
tsConfigFilePath: tsconfig,
|
|
1338
|
+
skipAddingFilesFromTsConfig: false,
|
|
1339
|
+
// Need all files for type resolution
|
|
1340
|
+
compilerOptions: {
|
|
1341
|
+
skipLibCheck: true,
|
|
1342
|
+
skipDefaultLibCheck: true,
|
|
1343
|
+
declaration: false,
|
|
1344
|
+
noEmit: true
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
function hasRipgrep() {
|
|
1349
|
+
try {
|
|
1350
|
+
execSync("rg --version", { encoding: "utf-8", stdio: "pipe" });
|
|
1351
|
+
return true;
|
|
1352
|
+
} catch {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function resolveTypeLocationsFast(baseDir, missingTypes) {
|
|
1357
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1358
|
+
const typeNames = [...missingTypes].map((t) => t.replace(/<.*>$/, ""));
|
|
1359
|
+
const typePattern = typeNames.join("|");
|
|
1360
|
+
const pattern = `export\\s+(class|interface|type|enum)\\s+(${typePattern})\\b`;
|
|
1361
|
+
const excludeDirs = [
|
|
1362
|
+
"node_modules",
|
|
1363
|
+
"dist",
|
|
1364
|
+
".git",
|
|
1365
|
+
"coverage",
|
|
1366
|
+
"__snapshots__",
|
|
1367
|
+
".turbo",
|
|
1368
|
+
".next",
|
|
1369
|
+
"build"
|
|
1370
|
+
];
|
|
1371
|
+
try {
|
|
1372
|
+
let result;
|
|
1373
|
+
if (hasRipgrep()) {
|
|
1374
|
+
const excludeArgs = excludeDirs.map((d) => `-g '!${d}/'`).join(" ");
|
|
1375
|
+
result = execSync(
|
|
1376
|
+
`rg -H --no-heading -t ts ${excludeArgs} '${pattern}' "${baseDir}" 2>/dev/null`,
|
|
1377
|
+
{ encoding: "utf-8", timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
|
|
1378
|
+
);
|
|
1379
|
+
} else {
|
|
1380
|
+
const excludeArgs = excludeDirs.map((d) => `--exclude-dir=${d}`).join(" ");
|
|
1381
|
+
result = execSync(
|
|
1382
|
+
`grep -r -H -E "${pattern}" --include="*.ts" ${excludeArgs} "${baseDir}" 2>/dev/null`,
|
|
1383
|
+
{ encoding: "utf-8", timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
for (const line of result.split("\n")) {
|
|
1387
|
+
if (!line.trim()) continue;
|
|
1388
|
+
const colonIdx = line.indexOf(":");
|
|
1389
|
+
if (colonIdx === -1) continue;
|
|
1390
|
+
const filePath = line.substring(0, colonIdx);
|
|
1391
|
+
if (filePath.includes("node_modules")) continue;
|
|
1392
|
+
for (const typeName of typeNames) {
|
|
1393
|
+
const typeRegex = new RegExp(
|
|
1394
|
+
`export\\s+(class|interface|type|enum)\\s+${typeName}\\b`
|
|
1395
|
+
);
|
|
1396
|
+
if (typeRegex.test(line) && !resolved.has(typeName)) {
|
|
1397
|
+
resolved.set(typeName, filePath);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
return resolved;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const DEFAULT_ENTRY = "src/app.module.ts";
|
|
1407
|
+
const mergeSecurityWithGlobal = (paths, globalSecurity) => {
|
|
1408
|
+
if (!globalSecurity || globalSecurity.length === 0) {
|
|
1409
|
+
return paths;
|
|
1410
|
+
}
|
|
1411
|
+
const mergedPaths = {};
|
|
1412
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
1413
|
+
const mergedMethods = {};
|
|
1414
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
1415
|
+
if (operation.security && operation.security.length > 0) {
|
|
1416
|
+
const merged = {};
|
|
1417
|
+
for (const globalReq of globalSecurity) {
|
|
1418
|
+
for (const [scheme, scopes] of Object.entries(globalReq)) {
|
|
1419
|
+
merged[scheme] = [...merged[scheme] ?? [], ...scopes];
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
for (const decoratorReq of operation.security) {
|
|
1423
|
+
for (const [scheme, scopes] of Object.entries(decoratorReq)) {
|
|
1424
|
+
merged[scheme] = [...merged[scheme] ?? [], ...scopes];
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
for (const scheme of Object.keys(merged)) {
|
|
1428
|
+
merged[scheme] = [...new Set(merged[scheme])];
|
|
1429
|
+
}
|
|
1430
|
+
mergedMethods[method] = {
|
|
1431
|
+
...operation,
|
|
1432
|
+
security: [merged]
|
|
1433
|
+
};
|
|
1434
|
+
} else {
|
|
1435
|
+
mergedMethods[method] = operation;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
mergedPaths[path] = mergedMethods;
|
|
1439
|
+
}
|
|
1440
|
+
return mergedPaths;
|
|
1441
|
+
};
|
|
1442
|
+
const OPENAPI_FIELD_ORDER = [
|
|
1443
|
+
"openapi",
|
|
1444
|
+
"info",
|
|
1445
|
+
"servers",
|
|
1446
|
+
"paths",
|
|
1447
|
+
"components",
|
|
1448
|
+
"tags",
|
|
1449
|
+
"security"
|
|
1450
|
+
];
|
|
1451
|
+
const sortObjectKeysDeep = (obj, isTopLevel = false) => {
|
|
1452
|
+
if (obj === null || typeof obj !== "object") {
|
|
1453
|
+
return obj;
|
|
1454
|
+
}
|
|
1455
|
+
if (Array.isArray(obj)) {
|
|
1456
|
+
return obj.map((item) => sortObjectKeysDeep(item, false));
|
|
1457
|
+
}
|
|
1458
|
+
const sorted = {};
|
|
1459
|
+
const objRecord = obj;
|
|
1460
|
+
const keys = Object.keys(objRecord);
|
|
1461
|
+
const sortedKeys = isTopLevel ? keys.sort((a, b) => {
|
|
1462
|
+
const aIndex = OPENAPI_FIELD_ORDER.indexOf(a);
|
|
1463
|
+
const bIndex = OPENAPI_FIELD_ORDER.indexOf(b);
|
|
1464
|
+
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
|
1465
|
+
if (aIndex !== -1) return -1;
|
|
1466
|
+
if (bIndex !== -1) return 1;
|
|
1467
|
+
return a.localeCompare(b);
|
|
1468
|
+
}) : keys.sort();
|
|
1469
|
+
for (const key of sortedKeys) {
|
|
1470
|
+
sorted[key] = sortObjectKeysDeep(objRecord[key], false);
|
|
1471
|
+
}
|
|
1472
|
+
return sorted;
|
|
1473
|
+
};
|
|
1474
|
+
const findMissingSchemaRefs = (paths, schemas) => {
|
|
1475
|
+
const defined = new Set(Object.keys(schemas));
|
|
1476
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1477
|
+
const findRefs = (obj) => {
|
|
1478
|
+
if (!obj || typeof obj !== "object") return;
|
|
1479
|
+
const record = obj;
|
|
1480
|
+
if (typeof record.$ref === "string") {
|
|
1481
|
+
const ref = record.$ref;
|
|
1482
|
+
if (ref.startsWith("#/components/schemas/")) {
|
|
1483
|
+
const schemaName = ref.replace("#/components/schemas/", "");
|
|
1484
|
+
if (!defined.has(schemaName)) {
|
|
1485
|
+
missing.add(schemaName);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
for (const value of Object.values(record)) {
|
|
1490
|
+
findRefs(value);
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
findRefs(paths);
|
|
1494
|
+
return missing;
|
|
1495
|
+
};
|
|
1496
|
+
const extractValidationConstraints = async (dtoGlobPatterns, basePath, tsconfig, schemas) => {
|
|
1497
|
+
const absolutePatterns = dtoGlobPatterns.map(
|
|
1498
|
+
(pattern) => pattern.startsWith("/") ? pattern : join(basePath, pattern)
|
|
1499
|
+
);
|
|
1500
|
+
const fileArrays = await Promise.all(
|
|
1501
|
+
absolutePatterns.map(
|
|
1502
|
+
(pattern) => glob(pattern, { absolute: true, nodir: true })
|
|
1503
|
+
)
|
|
1504
|
+
);
|
|
1505
|
+
const dtoFiles = fileArrays.flat();
|
|
1506
|
+
if (dtoFiles.length === 0) {
|
|
1507
|
+
return schemas;
|
|
1508
|
+
}
|
|
1509
|
+
const project = new Project({
|
|
1510
|
+
tsConfigFilePath: tsconfig,
|
|
1511
|
+
skipAddingFilesFromTsConfig: true,
|
|
1512
|
+
compilerOptions: {
|
|
1513
|
+
// Skip type checking for performance - we only need AST structure
|
|
1514
|
+
skipLibCheck: true,
|
|
1515
|
+
skipDefaultLibCheck: true,
|
|
1516
|
+
allowJs: false,
|
|
1517
|
+
declaration: false,
|
|
1518
|
+
noEmit: true
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
project.addSourceFilesAtPaths(dtoFiles);
|
|
1522
|
+
const classConstraints = /* @__PURE__ */ new Map();
|
|
1523
|
+
const classRequired = /* @__PURE__ */ new Map();
|
|
1524
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
1525
|
+
for (const classDecl of sourceFile.getClasses()) {
|
|
1526
|
+
const className = classDecl.getName();
|
|
1527
|
+
if (!className) continue;
|
|
1528
|
+
const constraints = extractClassConstraints(classDecl);
|
|
1529
|
+
const required = getRequiredProperties(classDecl);
|
|
1530
|
+
if (Object.keys(constraints).length > 0) {
|
|
1531
|
+
classConstraints.set(className, constraints);
|
|
1532
|
+
}
|
|
1533
|
+
if (required.length > 0) {
|
|
1534
|
+
classRequired.set(className, required);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return mergeValidationConstraints(schemas, classConstraints, classRequired);
|
|
1539
|
+
};
|
|
1540
|
+
const findTsConfig = (startDir) => {
|
|
1541
|
+
let currentDir = resolve(startDir);
|
|
1542
|
+
const root = dirname(currentDir);
|
|
1543
|
+
while (currentDir !== root) {
|
|
1544
|
+
const tsconfigPath = join(currentDir, "tsconfig.json");
|
|
1545
|
+
if (existsSync(tsconfigPath)) {
|
|
1546
|
+
return tsconfigPath;
|
|
1547
|
+
}
|
|
1548
|
+
const parentDir = dirname(currentDir);
|
|
1549
|
+
if (parentDir === currentDir) break;
|
|
1550
|
+
currentDir = parentDir;
|
|
1551
|
+
}
|
|
1552
|
+
return void 0;
|
|
1553
|
+
};
|
|
1554
|
+
const extractMethodInfosFromEntry = (tsconfig, entry, extractOptions = {}) => Effect.gen(function* () {
|
|
1555
|
+
const project = new Project({
|
|
1556
|
+
tsConfigFilePath: tsconfig,
|
|
1557
|
+
skipAddingFilesFromTsConfig: true,
|
|
1558
|
+
compilerOptions: {
|
|
1559
|
+
skipLibCheck: true,
|
|
1560
|
+
skipDefaultLibCheck: true,
|
|
1561
|
+
allowJs: false,
|
|
1562
|
+
declaration: false,
|
|
1563
|
+
noEmit: true
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
project.addSourceFilesAtPaths(entry);
|
|
1567
|
+
const entrySourceFile = project.getSourceFile(entry);
|
|
1568
|
+
if (!entrySourceFile) {
|
|
1569
|
+
return yield* EntryNotFoundError.fileNotFound(entry);
|
|
1570
|
+
}
|
|
1571
|
+
let entryClass = entrySourceFile.getClass("AppModule");
|
|
1572
|
+
if (!entryClass) {
|
|
1573
|
+
for (const cls of entrySourceFile.getClasses()) {
|
|
1574
|
+
const hasModuleDecorator = cls.getDecorators().some((d) => d.getName() === "Module");
|
|
1575
|
+
if (hasModuleDecorator) {
|
|
1576
|
+
entryClass = cls;
|
|
1577
|
+
break;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (!entryClass) {
|
|
1582
|
+
return yield* EntryNotFoundError.classNotFound(entry, "Module");
|
|
1583
|
+
}
|
|
1584
|
+
const modules = yield* getModules(entryClass);
|
|
1585
|
+
const methodInfos = modules.flatMap(
|
|
1586
|
+
(mod) => mod.controllers.flatMap(
|
|
1587
|
+
(controller) => getControllerMethodInfos(controller, extractOptions)
|
|
1588
|
+
)
|
|
1589
|
+
);
|
|
1590
|
+
return methodInfos;
|
|
1591
|
+
});
|
|
1592
|
+
const extractMethodInfosEffect = (tsconfig, entries, extractOptions = {}) => Effect.gen(function* () {
|
|
1593
|
+
const allMethodInfos = yield* Effect.forEach(
|
|
1594
|
+
entries,
|
|
1595
|
+
(entry) => extractMethodInfosFromEntry(tsconfig, entry, extractOptions),
|
|
1596
|
+
{ concurrency: "unbounded" }
|
|
1597
|
+
);
|
|
1598
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1599
|
+
const deduped = [];
|
|
1600
|
+
for (const methodInfos of allMethodInfos) {
|
|
1601
|
+
for (const info of methodInfos) {
|
|
1602
|
+
const key = `${info.httpMethod}:${info.path}`;
|
|
1603
|
+
if (!seen.has(key)) {
|
|
1604
|
+
seen.add(key);
|
|
1605
|
+
deduped.push(info);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return deduped;
|
|
1610
|
+
});
|
|
1611
|
+
const generate = async (configPath, overrides) => {
|
|
1612
|
+
const debug = overrides?.debug ?? false;
|
|
1613
|
+
const loggerLayer = debug ? Logger.replace(Logger.defaultLogger, Logger.prettyLoggerDefault).pipe(
|
|
1614
|
+
Layer.merge(Logger.minimumLogLevel(LogLevel.Debug))
|
|
1615
|
+
) : Logger.minimumLogLevel(LogLevel.Info);
|
|
1616
|
+
const runEffect = (effect) => Effect.runPromise(effect.pipe(Effect.provide(loggerLayer)));
|
|
1617
|
+
const absoluteConfigPath = resolve(configPath);
|
|
1618
|
+
const configDir = dirname(absoluteConfigPath);
|
|
1619
|
+
const config = await runEffect(
|
|
1620
|
+
loadConfigFromFile(absoluteConfigPath).pipe(
|
|
1621
|
+
Effect.tap(
|
|
1622
|
+
() => Effect.logDebug("Config loaded").pipe(
|
|
1623
|
+
Effect.annotateLogs({ configPath: absoluteConfigPath })
|
|
1624
|
+
)
|
|
1625
|
+
),
|
|
1626
|
+
Effect.mapError((e) => new Error(e.message))
|
|
1627
|
+
)
|
|
1628
|
+
);
|
|
1629
|
+
const files = config.files ?? {};
|
|
1630
|
+
const options = config.options ?? {};
|
|
1631
|
+
const openapi = config.openapi;
|
|
1632
|
+
const security = openapi.security ?? {};
|
|
1633
|
+
const rawEntry = files.entry ?? DEFAULT_ENTRY;
|
|
1634
|
+
const entries = (Array.isArray(rawEntry) ? rawEntry : [rawEntry]).map(
|
|
1635
|
+
(e) => resolve(configDir, e)
|
|
1636
|
+
);
|
|
1637
|
+
const output = resolve(configDir, config.output);
|
|
1638
|
+
const tsconfig = files.tsconfig ? resolve(configDir, files.tsconfig) : findTsConfig(dirname(entries[0]));
|
|
1639
|
+
if (!tsconfig) {
|
|
1640
|
+
throw new Error(
|
|
1641
|
+
`Could not find tsconfig.json. Please specify files.tsconfig in your config file.`
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
if (!existsSync(tsconfig)) {
|
|
1645
|
+
throw new Error(`tsconfig.json not found at: ${tsconfig}`);
|
|
1646
|
+
}
|
|
1647
|
+
const extractOptions = {
|
|
1648
|
+
query: options.query
|
|
1649
|
+
};
|
|
1650
|
+
const dtoGlobArray = files.dtoGlob ? Array.isArray(files.dtoGlob) ? files.dtoGlob : [files.dtoGlob] : null;
|
|
1651
|
+
const [extractedMethodInfos, initialSchemas] = await Promise.all([
|
|
1652
|
+
runEffect(
|
|
1653
|
+
extractMethodInfosEffect(tsconfig, entries, extractOptions).pipe(
|
|
1654
|
+
Effect.tap(
|
|
1655
|
+
(methods) => Effect.logDebug("Method extraction complete").pipe(
|
|
1656
|
+
Effect.annotateLogs({ methodCount: methods.length, entries })
|
|
1657
|
+
)
|
|
1658
|
+
),
|
|
1659
|
+
Effect.mapError((error) => new Error(error.message))
|
|
1660
|
+
)
|
|
1661
|
+
),
|
|
1662
|
+
dtoGlobArray ? runEffect(
|
|
1663
|
+
generateSchemas({
|
|
1664
|
+
dtoGlob: dtoGlobArray,
|
|
1665
|
+
tsconfig,
|
|
1666
|
+
basePath: configDir
|
|
1667
|
+
}).pipe(
|
|
1668
|
+
Effect.tap(
|
|
1669
|
+
(schemas2) => Effect.logDebug("Schema generation complete").pipe(
|
|
1670
|
+
Effect.annotateLogs({
|
|
1671
|
+
schemaCount: Object.keys(schemas2.definitions).length,
|
|
1672
|
+
dtoGlob: dtoGlobArray
|
|
1673
|
+
})
|
|
1674
|
+
)
|
|
1675
|
+
),
|
|
1676
|
+
Effect.mapError((error) => new Error(error.message))
|
|
1677
|
+
)
|
|
1678
|
+
) : Promise.resolve(null)
|
|
1679
|
+
]);
|
|
1680
|
+
const filteredMethodInfos = filterMethods(extractedMethodInfos, {
|
|
1681
|
+
excludeDecorators: options.excludeDecorators,
|
|
1682
|
+
pathFilter: options.pathFilter
|
|
1683
|
+
});
|
|
1684
|
+
let paths = transformMethods(filteredMethodInfos);
|
|
1685
|
+
if (options.basePath) {
|
|
1686
|
+
const prefix = options.basePath.startsWith("/") ? options.basePath : `/${options.basePath}`;
|
|
1687
|
+
const prefixedPaths = {};
|
|
1688
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
1689
|
+
const prefixedPath = path.startsWith("/") ? `${prefix}${path}` : `${prefix}/${path}`;
|
|
1690
|
+
prefixedPaths[prefixedPath] = methods;
|
|
1691
|
+
}
|
|
1692
|
+
paths = prefixedPaths;
|
|
1693
|
+
}
|
|
1694
|
+
paths = mergeSecurityWithGlobal(
|
|
1695
|
+
paths,
|
|
1696
|
+
security.global
|
|
1697
|
+
);
|
|
1698
|
+
let schemas = {};
|
|
1699
|
+
if (initialSchemas && dtoGlobArray) {
|
|
1700
|
+
let generatedSchemas = initialSchemas;
|
|
1701
|
+
const shouldExtractValidation = options.extractValidation !== false;
|
|
1702
|
+
if (shouldExtractValidation) {
|
|
1703
|
+
generatedSchemas = await extractValidationConstraints(
|
|
1704
|
+
dtoGlobArray,
|
|
1705
|
+
configDir,
|
|
1706
|
+
tsconfig,
|
|
1707
|
+
generatedSchemas
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
generatedSchemas = normalizeStructureRefs(generatedSchemas);
|
|
1711
|
+
let mergeResult = mergeSchemas(
|
|
1712
|
+
paths,
|
|
1713
|
+
generatedSchemas
|
|
1714
|
+
);
|
|
1715
|
+
schemas = mergeResult.schemas;
|
|
1716
|
+
const missingRefs = findMissingSchemaRefs(
|
|
1717
|
+
paths,
|
|
1718
|
+
schemas
|
|
1719
|
+
);
|
|
1720
|
+
if (missingRefs.size > 0) {
|
|
1721
|
+
const tsconfigDir = dirname(tsconfig);
|
|
1722
|
+
const resolvedLocations = resolveTypeLocationsFast(
|
|
1723
|
+
tsconfigDir,
|
|
1724
|
+
missingRefs
|
|
1725
|
+
);
|
|
1726
|
+
const unresolvedTypes = new Set(
|
|
1727
|
+
[...missingRefs].filter(
|
|
1728
|
+
(t) => !resolvedLocations.has(t.replace(/<.*>$/, ""))
|
|
1729
|
+
)
|
|
1730
|
+
);
|
|
1731
|
+
if (unresolvedTypes.size > 0) {
|
|
1732
|
+
const project = createTypeResolverProject(tsconfig);
|
|
1733
|
+
const morphResolved = resolveTypeLocations(project, unresolvedTypes);
|
|
1734
|
+
for (const [type, path] of morphResolved) {
|
|
1735
|
+
resolvedLocations.set(type, path);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (resolvedLocations.size > 0) {
|
|
1739
|
+
const additionalFiles = [...new Set(resolvedLocations.values())];
|
|
1740
|
+
const additionalSchemas = await runEffect(
|
|
1741
|
+
generateSchemasFromFiles(additionalFiles, tsconfig)
|
|
1742
|
+
);
|
|
1743
|
+
if (Object.keys(additionalSchemas.definitions).length > 0) {
|
|
1744
|
+
const normalizedAdditional = normalizeStructureRefs(additionalSchemas);
|
|
1745
|
+
const combinedSchemas = {
|
|
1746
|
+
definitions: {
|
|
1747
|
+
...generatedSchemas.definitions,
|
|
1748
|
+
...normalizedAdditional.definitions
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
mergeResult = mergeSchemas(
|
|
1752
|
+
paths,
|
|
1753
|
+
combinedSchemas
|
|
1754
|
+
);
|
|
1755
|
+
schemas = mergeResult.schemas;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const securitySchemes = security.schemes && security.schemes.length > 0 ? buildSecuritySchemes(security.schemes) : void 0;
|
|
1761
|
+
const hasSchemas = Object.keys(schemas).length > 0;
|
|
1762
|
+
const hasSecuritySchemes = securitySchemes && Object.keys(securitySchemes).length > 0;
|
|
1763
|
+
const components = hasSchemas || hasSecuritySchemes ? {
|
|
1764
|
+
...hasSchemas && { schemas },
|
|
1765
|
+
...hasSecuritySchemes && { securitySchemes }
|
|
1766
|
+
} : void 0;
|
|
1767
|
+
const openApiVersion = openapi.version ?? "3.0.3";
|
|
1768
|
+
let spec = {
|
|
1769
|
+
openapi: openApiVersion,
|
|
1770
|
+
info: {
|
|
1771
|
+
title: openapi.info.title,
|
|
1772
|
+
version: openapi.info.version,
|
|
1773
|
+
...openapi.info.description && {
|
|
1774
|
+
description: openapi.info.description
|
|
1775
|
+
},
|
|
1776
|
+
...openapi.info.contact && { contact: openapi.info.contact },
|
|
1777
|
+
...openapi.info.license && { license: openapi.info.license }
|
|
1778
|
+
},
|
|
1779
|
+
servers: openapi.servers ?? [],
|
|
1780
|
+
paths,
|
|
1781
|
+
...components && { components },
|
|
1782
|
+
tags: openapi.tags ?? [],
|
|
1783
|
+
...security.global && security.global.length > 0 && {
|
|
1784
|
+
security: security.global
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
if (openApiVersion !== "3.0.3") {
|
|
1788
|
+
spec = transformSpecForVersion(spec, openApiVersion);
|
|
1789
|
+
}
|
|
1790
|
+
const sortedSpec = sortObjectKeysDeep(spec, true);
|
|
1791
|
+
const outputDir = dirname(output);
|
|
1792
|
+
if (!existsSync(outputDir)) {
|
|
1793
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1794
|
+
}
|
|
1795
|
+
const format = overrides?.format ?? config.format ?? "json";
|
|
1796
|
+
if (format === "json") {
|
|
1797
|
+
writeFileSync(output, JSON.stringify(sortedSpec, null, 2), "utf-8");
|
|
1798
|
+
} else {
|
|
1799
|
+
writeFileSync(
|
|
1800
|
+
output,
|
|
1801
|
+
yaml.dump(sortedSpec, {
|
|
1802
|
+
indent: 2,
|
|
1803
|
+
lineWidth: -1,
|
|
1804
|
+
// Disable line wrapping
|
|
1805
|
+
noRefs: true,
|
|
1806
|
+
// Disable anchor/alias references
|
|
1807
|
+
quotingType: '"',
|
|
1808
|
+
// Use double quotes for strings
|
|
1809
|
+
forceQuotes: false
|
|
1810
|
+
// Only quote when necessary
|
|
1811
|
+
}),
|
|
1812
|
+
"utf-8"
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
const pathCount = Object.keys(paths).length;
|
|
1816
|
+
const operationCount = Object.values(paths).reduce(
|
|
1817
|
+
(acc, methods) => acc + Object.keys(methods).length,
|
|
1818
|
+
0
|
|
1819
|
+
);
|
|
1820
|
+
const schemaCount = Object.keys(schemas).length;
|
|
1821
|
+
const validation = validateSpec(sortedSpec);
|
|
1822
|
+
return {
|
|
1823
|
+
outputPath: output,
|
|
1824
|
+
pathCount,
|
|
1825
|
+
operationCount,
|
|
1826
|
+
schemaCount,
|
|
1827
|
+
validation
|
|
1828
|
+
};
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
export { loadConfig as a, loadAndResolveConfig as b, categorizeBrokenRefs as c, defineConfig as d, formatValidationResult as e, findConfigFile as f, generate as g, loadConfigFromFile as l, resolveConfig as r, validateSpec as v };
|