adorn-api 1.0.12 → 1.0.13
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/dist/adapter/express/bootstrap.d.ts.map +1 -1
- package/dist/adapter/express/merge.d.ts.map +1 -1
- package/dist/adapter/express/router.d.ts.map +1 -1
- package/dist/cli.cjs +799 -409
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +799 -409
- package/dist/cli.js.map +1 -1
- package/dist/compiler/cache/isStale.d.ts.map +1 -1
- package/dist/compiler/cache/writeCache.d.ts.map +1 -1
- package/dist/compiler/schema/intersectionHandler.d.ts +7 -0
- package/dist/compiler/schema/intersectionHandler.d.ts.map +1 -0
- package/dist/compiler/schema/objectHandler.d.ts +20 -0
- package/dist/compiler/schema/objectHandler.d.ts.map +1 -0
- package/dist/compiler/schema/openapi.d.ts +1 -1
- package/dist/compiler/schema/openapi.d.ts.map +1 -1
- package/dist/compiler/schema/parameters.d.ts +18 -0
- package/dist/compiler/schema/parameters.d.ts.map +1 -0
- package/dist/compiler/schema/primitives.d.ts +10 -0
- package/dist/compiler/schema/primitives.d.ts.map +1 -0
- package/dist/compiler/schema/typeToJsonSchema.d.ts +2 -51
- package/dist/compiler/schema/typeToJsonSchema.d.ts.map +1 -1
- package/dist/compiler/schema/types.d.ts +54 -0
- package/dist/compiler/schema/types.d.ts.map +1 -0
- package/dist/compiler/schema/unionHandler.d.ts +10 -0
- package/dist/compiler/schema/unionHandler.d.ts.map +1 -0
- package/dist/express.cjs +41 -44
- package/dist/express.cjs.map +1 -1
- package/dist/express.js +41 -44
- package/dist/express.js.map +1 -1
- package/dist/http.d.ts +1 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metal/index.cjs +2 -2
- package/dist/metal/index.cjs.map +1 -1
- package/dist/metal/index.js +2 -2
- package/dist/metal/index.js.map +1 -1
- package/dist/metal/listQuery.d.ts.map +1 -1
- package/dist/metal/queryOptions.d.ts.map +1 -1
- package/dist/metal/registerMetalEntities.d.ts.map +1 -1
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -86,7 +86,7 @@ function analyzeClass(node, sourceFile, checker) {
|
|
|
86
86
|
produces
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
function extractClassConsumes(node,
|
|
89
|
+
function extractClassConsumes(node, _checker) {
|
|
90
90
|
const decorator = findDecorator(node, "Consumes");
|
|
91
91
|
if (!decorator) return void 0;
|
|
92
92
|
const callExpr = decorator.expression;
|
|
@@ -102,7 +102,7 @@ function extractClassConsumes(node, checker) {
|
|
|
102
102
|
}
|
|
103
103
|
return void 0;
|
|
104
104
|
}
|
|
105
|
-
function extractClassProduces(node,
|
|
105
|
+
function extractClassProduces(node, _checker) {
|
|
106
106
|
const decorator = findDecorator(node, "Produces");
|
|
107
107
|
if (!decorator) return void 0;
|
|
108
108
|
const callExpr = decorator.expression;
|
|
@@ -284,7 +284,7 @@ function extractDecoratorStringArg(decorator) {
|
|
|
284
284
|
}
|
|
285
285
|
return null;
|
|
286
286
|
}
|
|
287
|
-
function unwrapPromise(type,
|
|
287
|
+
function unwrapPromise(type, _checker) {
|
|
288
288
|
const symbol = type.getSymbol();
|
|
289
289
|
if (symbol?.getName() === "Promise") {
|
|
290
290
|
const typeArgs = type.typeArguments;
|
|
@@ -305,12 +305,15 @@ function unwrapPromiseTypeNode(typeNode) {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
// src/compiler/schema/openapi.ts
|
|
308
|
-
import
|
|
308
|
+
import ts9 from "typescript";
|
|
309
309
|
|
|
310
310
|
// src/compiler/schema/typeToJsonSchema.ts
|
|
311
|
+
import ts7 from "typescript";
|
|
312
|
+
|
|
313
|
+
// src/compiler/schema/primitives.ts
|
|
311
314
|
import ts3 from "typescript";
|
|
312
|
-
function
|
|
313
|
-
const { checker } = ctx;
|
|
315
|
+
function handlePrimitiveType(type, ctx, typeNode) {
|
|
316
|
+
const { checker, propertyName } = ctx;
|
|
314
317
|
if (type.flags & ts3.TypeFlags.Undefined) {
|
|
315
318
|
return {};
|
|
316
319
|
}
|
|
@@ -324,7 +327,7 @@ function typeToJsonSchema(type, ctx, typeNode) {
|
|
|
324
327
|
return { type: "string" };
|
|
325
328
|
}
|
|
326
329
|
if (type.flags & ts3.TypeFlags.Number) {
|
|
327
|
-
return normalizeNumericType(type, checker, typeNode);
|
|
330
|
+
return normalizeNumericType(type, checker, typeNode, propertyName);
|
|
328
331
|
}
|
|
329
332
|
if (type.flags & ts3.TypeFlags.Boolean) {
|
|
330
333
|
return { type: "boolean" };
|
|
@@ -348,26 +351,7 @@ function typeToJsonSchema(type, ctx, typeNode) {
|
|
|
348
351
|
const intrinsic = type.intrinsicName;
|
|
349
352
|
return { type: "boolean", enum: [intrinsic === "true"] };
|
|
350
353
|
}
|
|
351
|
-
|
|
352
|
-
return handleUnion(type, ctx, typeNode);
|
|
353
|
-
}
|
|
354
|
-
if (type.isIntersection()) {
|
|
355
|
-
return handleIntersection(type, ctx, typeNode);
|
|
356
|
-
}
|
|
357
|
-
if (checker.isArrayType(type)) {
|
|
358
|
-
const typeArgs = type.typeArguments;
|
|
359
|
-
const itemType = typeArgs?.[0];
|
|
360
|
-
const items = itemType ? typeToJsonSchema(itemType, ctx) : {};
|
|
361
|
-
return {
|
|
362
|
-
type: "array",
|
|
363
|
-
items,
|
|
364
|
-
uniqueItems: isSetType(type, checker) ? true : void 0
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
if (type.flags & ts3.TypeFlags.Object) {
|
|
368
|
-
return handleObjectType(type, ctx, typeNode);
|
|
369
|
-
}
|
|
370
|
-
return {};
|
|
354
|
+
return null;
|
|
371
355
|
}
|
|
372
356
|
function isDateType(type, checker) {
|
|
373
357
|
const symbol = type.getSymbol();
|
|
@@ -382,53 +366,51 @@ function isDateType(type, checker) {
|
|
|
382
366
|
}
|
|
383
367
|
return symbol?.getName() === "Date";
|
|
384
368
|
}
|
|
385
|
-
function
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
function getSchemaName(type, typeNode) {
|
|
393
|
-
const aliasSymbol = type.aliasSymbol ?? type.aliasSymbol;
|
|
394
|
-
const aliasName = aliasSymbol?.getName();
|
|
395
|
-
if (aliasName && aliasName !== "__type") {
|
|
396
|
-
return aliasName;
|
|
369
|
+
function normalizeNumericType(type, checker, typeNode, propertyName) {
|
|
370
|
+
const typeName = getExplicitTypeNameFromNode(typeNode) ?? null;
|
|
371
|
+
const symbol = getEffectiveSymbol(type, checker);
|
|
372
|
+
const symbolName = symbol?.getName() ?? null;
|
|
373
|
+
if (shouldBeIntegerType(typeName) || shouldBeIntegerType(symbolName) || shouldBeIntegerType(propertyName ?? null)) {
|
|
374
|
+
return { type: "integer" };
|
|
397
375
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
376
|
+
return { type: "number" };
|
|
377
|
+
}
|
|
378
|
+
function shouldBeIntegerType(typeName) {
|
|
379
|
+
if (!typeName) return false;
|
|
380
|
+
const lower = typeName.toLowerCase();
|
|
381
|
+
return lower === "id" || lower.endsWith("id") || lower === "primarykey" || lower === "pk" || lower === "page" || lower === "pagesize" || lower === "totalitems" || lower === "limit" || lower === "offset";
|
|
382
|
+
}
|
|
383
|
+
function getExplicitTypeNameFromNode(typeNode) {
|
|
384
|
+
if (!typeNode) return null;
|
|
385
|
+
if (ts3.isTypeReferenceNode(typeNode)) {
|
|
386
|
+
if (ts3.isIdentifier(typeNode.typeName)) {
|
|
387
|
+
return typeNode.typeName.text;
|
|
388
|
+
}
|
|
402
389
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
390
|
+
if (ts3.isTypeAliasDeclaration(typeNode.parent)) {
|
|
391
|
+
if (ts3.isIdentifier(typeNode.parent.name)) {
|
|
392
|
+
return typeNode.parent.name.text;
|
|
393
|
+
}
|
|
406
394
|
}
|
|
407
395
|
return null;
|
|
408
396
|
}
|
|
409
|
-
function
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
412
|
-
return
|
|
413
|
-
}
|
|
414
|
-
const { components, typeStack } = ctx;
|
|
415
|
-
if (components.has(name) || typeStack.has(type)) {
|
|
416
|
-
return { $ref: `#/components/schemas/${name}` };
|
|
417
|
-
}
|
|
418
|
-
typeStack.add(type);
|
|
419
|
-
const schema = build();
|
|
420
|
-
typeStack.delete(type);
|
|
421
|
-
if (!components.has(name)) {
|
|
422
|
-
components.set(name, schema);
|
|
397
|
+
function getEffectiveSymbol(type, checker) {
|
|
398
|
+
const aliasSymbol = type.aliasSymbol ?? type.aliasSymbol;
|
|
399
|
+
if (aliasSymbol && aliasSymbol.flags & ts3.SymbolFlags.Alias) {
|
|
400
|
+
return checker.getAliasedSymbol(aliasSymbol);
|
|
423
401
|
}
|
|
424
|
-
return
|
|
402
|
+
return type.getSymbol() ?? null;
|
|
425
403
|
}
|
|
404
|
+
|
|
405
|
+
// src/compiler/schema/unionHandler.ts
|
|
406
|
+
import ts4 from "typescript";
|
|
426
407
|
function handleUnion(type, ctx, typeNode) {
|
|
427
408
|
return buildNamedSchema(type, ctx, typeNode, () => {
|
|
428
409
|
const types = type.types;
|
|
429
|
-
const nullType = types.find((t) => t.flags &
|
|
430
|
-
const
|
|
431
|
-
const
|
|
410
|
+
const nullType = types.find((t) => t.flags & ts4.TypeFlags.Null);
|
|
411
|
+
const undefinedType = types.find((t) => t.flags & ts4.TypeFlags.Undefined);
|
|
412
|
+
const otherTypes = types.filter((t) => !(t.flags & ts4.TypeFlags.Null) && !(t.flags & ts4.TypeFlags.Undefined));
|
|
413
|
+
const allStringLiterals = otherTypes.every((t) => t.flags & ts4.TypeFlags.StringLiteral);
|
|
432
414
|
if (allStringLiterals && otherTypes.length > 0) {
|
|
433
415
|
const enumValues = otherTypes.map((t) => t.value);
|
|
434
416
|
const schema = { type: "string", enum: enumValues };
|
|
@@ -437,6 +419,14 @@ function handleUnion(type, ctx, typeNode) {
|
|
|
437
419
|
}
|
|
438
420
|
return schema;
|
|
439
421
|
}
|
|
422
|
+
const allBooleanLiterals = otherTypes.length > 0 && otherTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
|
|
423
|
+
if (allBooleanLiterals) {
|
|
424
|
+
const schema = { type: "boolean" };
|
|
425
|
+
if (nullType || undefinedType) {
|
|
426
|
+
schema.type = ["boolean", "null"];
|
|
427
|
+
}
|
|
428
|
+
return schema;
|
|
429
|
+
}
|
|
440
430
|
if (otherTypes.length === 1 && nullType) {
|
|
441
431
|
const innerSchema = typeToJsonSchema(otherTypes[0], ctx);
|
|
442
432
|
if (typeof innerSchema.type === "string") {
|
|
@@ -466,49 +456,7 @@ function handleUnion(type, ctx, typeNode) {
|
|
|
466
456
|
return {};
|
|
467
457
|
});
|
|
468
458
|
}
|
|
469
|
-
function
|
|
470
|
-
return buildNamedSchema(type, ctx, typeNode, () => {
|
|
471
|
-
const types = type.types;
|
|
472
|
-
const brandCollapsed = tryCollapseBrandedIntersection(types, ctx, typeNode);
|
|
473
|
-
if (brandCollapsed) {
|
|
474
|
-
return brandCollapsed;
|
|
475
|
-
}
|
|
476
|
-
const allOf = [];
|
|
477
|
-
for (const t of types) {
|
|
478
|
-
allOf.push(typeToJsonSchema(t, ctx));
|
|
479
|
-
}
|
|
480
|
-
return { allOf };
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
function tryCollapseBrandedIntersection(types, ctx, typeNode) {
|
|
484
|
-
const { checker } = ctx;
|
|
485
|
-
const parts = [...types];
|
|
486
|
-
const prim = parts.find(isPrimitiveLike);
|
|
487
|
-
if (!prim) return null;
|
|
488
|
-
const rest = parts.filter((p) => p !== prim);
|
|
489
|
-
if (rest.every((r) => isBrandObject(checker, r, ctx))) {
|
|
490
|
-
return typeToJsonSchema(prim, ctx);
|
|
491
|
-
}
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
function isPrimitiveLike(t) {
|
|
495
|
-
return (t.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.Boolean | ts3.TypeFlags.BigInt)) !== 0 || (t.flags & ts3.TypeFlags.StringLiteral) !== 0 || (t.flags & ts3.TypeFlags.NumberLiteral) !== 0;
|
|
496
|
-
}
|
|
497
|
-
function isBrandObject(checker, t, ctx) {
|
|
498
|
-
if (!(t.flags & ts3.TypeFlags.Object)) return false;
|
|
499
|
-
const props = t.getProperties();
|
|
500
|
-
if (props.length === 0) return false;
|
|
501
|
-
const allowed = /* @__PURE__ */ new Set(["__brand", "__type", "__tag", "brand"]);
|
|
502
|
-
for (const p of props) {
|
|
503
|
-
if (!allowed.has(p.getName())) return false;
|
|
504
|
-
}
|
|
505
|
-
const callSigs = t.getCallSignatures?.();
|
|
506
|
-
if (callSigs && callSigs.length > 0) return false;
|
|
507
|
-
const constructSigs = t.getConstructSignatures?.();
|
|
508
|
-
if (constructSigs && constructSigs.length > 0) return false;
|
|
509
|
-
return true;
|
|
510
|
-
}
|
|
511
|
-
function detectDiscriminatedUnion(types, ctx, branches) {
|
|
459
|
+
function detectDiscriminatedUnion(types, ctx, _branches) {
|
|
512
460
|
if (types.length < 2) return null;
|
|
513
461
|
const candidates = findCommonPropertyNames(ctx.checker, types);
|
|
514
462
|
for (const propName of candidates) {
|
|
@@ -539,10 +487,10 @@ function findCommonPropertyNames(checker, types) {
|
|
|
539
487
|
function isRequiredProperty(checker, type, propName) {
|
|
540
488
|
const sym = checker.getPropertyOfType(type, propName);
|
|
541
489
|
if (!sym) return false;
|
|
542
|
-
if (sym.flags &
|
|
490
|
+
if (sym.flags & ts4.SymbolFlags.Optional) return false;
|
|
543
491
|
const propType = checker.getTypeOfSymbol(sym);
|
|
544
492
|
if (propType.isUnion?.()) {
|
|
545
|
-
const hasUndefined = propType.types.some((t) => (t.flags &
|
|
493
|
+
const hasUndefined = propType.types.some((t) => (t.flags & ts4.TypeFlags.Undefined) !== 0);
|
|
546
494
|
if (hasUndefined) return false;
|
|
547
495
|
}
|
|
548
496
|
return true;
|
|
@@ -585,14 +533,187 @@ function getBranchSchemaName(type, ctx) {
|
|
|
585
533
|
}
|
|
586
534
|
return `Anonymous_${ctx.typeNameStack.length}`;
|
|
587
535
|
}
|
|
536
|
+
function buildNamedSchema(type, ctx, typeNode, build) {
|
|
537
|
+
const name = getSchemaName(type, typeNode);
|
|
538
|
+
if (!name) {
|
|
539
|
+
return build();
|
|
540
|
+
}
|
|
541
|
+
const { components, typeStack } = ctx;
|
|
542
|
+
if (components.has(name) || typeStack.has(type)) {
|
|
543
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
544
|
+
}
|
|
545
|
+
typeStack.add(type);
|
|
546
|
+
const schema = build();
|
|
547
|
+
typeStack.delete(type);
|
|
548
|
+
if (!components.has(name)) {
|
|
549
|
+
components.set(name, schema);
|
|
550
|
+
}
|
|
551
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
552
|
+
}
|
|
553
|
+
function getSchemaName(type, typeNode) {
|
|
554
|
+
const aliasSymbol = type.aliasSymbol ?? type.aliasSymbol;
|
|
555
|
+
const aliasName = aliasSymbol?.getName();
|
|
556
|
+
if (aliasName && aliasName !== "__type") {
|
|
557
|
+
return aliasName;
|
|
558
|
+
}
|
|
559
|
+
const symbol = type.getSymbol();
|
|
560
|
+
const symbolName = symbol?.getName?.();
|
|
561
|
+
if (symbolName && symbolName !== "__type") {
|
|
562
|
+
return symbolName;
|
|
563
|
+
}
|
|
564
|
+
const nodeName = getExplicitTypeNameFromNode2(typeNode);
|
|
565
|
+
if (nodeName && nodeName !== "__type") {
|
|
566
|
+
return nodeName;
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
function getExplicitTypeNameFromNode2(typeNode) {
|
|
571
|
+
if (!typeNode) return null;
|
|
572
|
+
if (ts4.isTypeReferenceNode(typeNode)) {
|
|
573
|
+
if (ts4.isIdentifier(typeNode.typeName)) {
|
|
574
|
+
return typeNode.typeName.text;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (ts4.isTypeAliasDeclaration(typeNode.parent)) {
|
|
578
|
+
if (ts4.isIdentifier(typeNode.parent.name)) {
|
|
579
|
+
return typeNode.parent.name.text;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/compiler/schema/intersectionHandler.ts
|
|
586
|
+
import ts5 from "typescript";
|
|
587
|
+
function handleIntersection(type, ctx, typeNode) {
|
|
588
|
+
return buildNamedSchema2(type, ctx, typeNode, () => {
|
|
589
|
+
const types = type.types;
|
|
590
|
+
const brandCollapsed = tryCollapseBrandedIntersection(types, ctx, typeNode);
|
|
591
|
+
if (brandCollapsed) {
|
|
592
|
+
return brandCollapsed;
|
|
593
|
+
}
|
|
594
|
+
const allOf = [];
|
|
595
|
+
for (const t of types) {
|
|
596
|
+
const schema = typeToJsonSchema(t, ctx);
|
|
597
|
+
if (Object.keys(schema).length > 0) {
|
|
598
|
+
if (isEmptyObjectSchema(schema)) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
allOf.push(schema);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (allOf.length === 0) {
|
|
605
|
+
return {};
|
|
606
|
+
}
|
|
607
|
+
if (allOf.length === 1) {
|
|
608
|
+
return allOf[0];
|
|
609
|
+
}
|
|
610
|
+
return { allOf };
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function tryCollapseBrandedIntersection(types, ctx, _typeNode) {
|
|
614
|
+
const { checker } = ctx;
|
|
615
|
+
const parts = [...types];
|
|
616
|
+
const prim = parts.find(isPrimitiveLike);
|
|
617
|
+
if (!prim) return null;
|
|
618
|
+
const rest = parts.filter((p) => p !== prim);
|
|
619
|
+
if (rest.every((r) => isBrandObject(checker, r, ctx))) {
|
|
620
|
+
return typeToJsonSchema(prim, ctx);
|
|
621
|
+
}
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
function isPrimitiveLike(t) {
|
|
625
|
+
return (t.flags & (ts5.TypeFlags.String | ts5.TypeFlags.Number | ts5.TypeFlags.Boolean | ts5.TypeFlags.BigInt)) !== 0 || (t.flags & ts5.TypeFlags.StringLiteral) !== 0 || (t.flags & ts5.TypeFlags.NumberLiteral) !== 0;
|
|
626
|
+
}
|
|
627
|
+
function isBrandObject(checker, t, _ctx) {
|
|
628
|
+
if (!(t.flags & ts5.TypeFlags.Object)) return false;
|
|
629
|
+
const props = t.getProperties();
|
|
630
|
+
if (props.length === 0) return false;
|
|
631
|
+
const allowed = /* @__PURE__ */ new Set(["__brand", "__type", "__tag", "brand"]);
|
|
632
|
+
for (const p of props) {
|
|
633
|
+
if (!allowed.has(p.getName())) return false;
|
|
634
|
+
}
|
|
635
|
+
const callSigs = t.getCallSignatures?.();
|
|
636
|
+
if (callSigs && callSigs.length > 0) return false;
|
|
637
|
+
const constructSigs = t.getConstructSignatures?.();
|
|
638
|
+
if (constructSigs && constructSigs.length > 0) return false;
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
function isEmptyObjectSchema(schema) {
|
|
642
|
+
if (schema.type !== "object") {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
646
|
+
if (!schema.additionalProperties) {
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
function buildNamedSchema2(type, ctx, typeNode, build) {
|
|
653
|
+
const name = getSchemaName2(type, typeNode);
|
|
654
|
+
if (!name) {
|
|
655
|
+
return build();
|
|
656
|
+
}
|
|
657
|
+
const { components, typeStack } = ctx;
|
|
658
|
+
if (components.has(name) || typeStack.has(type)) {
|
|
659
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
660
|
+
}
|
|
661
|
+
typeStack.add(type);
|
|
662
|
+
const schema = build();
|
|
663
|
+
typeStack.delete(type);
|
|
664
|
+
if (!components.has(name)) {
|
|
665
|
+
components.set(name, schema);
|
|
666
|
+
}
|
|
667
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
668
|
+
}
|
|
669
|
+
function getSchemaName2(type, typeNode) {
|
|
670
|
+
const aliasSymbol = type.aliasSymbol ?? type.aliasSymbol;
|
|
671
|
+
const aliasName = aliasSymbol?.getName();
|
|
672
|
+
if (aliasName && aliasName !== "__type") {
|
|
673
|
+
return aliasName;
|
|
674
|
+
}
|
|
675
|
+
const symbol = type.getSymbol();
|
|
676
|
+
const symbolName = symbol?.getName?.();
|
|
677
|
+
if (symbolName && symbolName !== "__type") {
|
|
678
|
+
return symbolName;
|
|
679
|
+
}
|
|
680
|
+
const nodeName = getExplicitTypeNameFromNode3(typeNode);
|
|
681
|
+
if (nodeName && nodeName !== "__type") {
|
|
682
|
+
return nodeName;
|
|
683
|
+
}
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
function getExplicitTypeNameFromNode3(typeNode) {
|
|
687
|
+
if (!typeNode) return null;
|
|
688
|
+
if (ts5.isTypeReferenceNode(typeNode)) {
|
|
689
|
+
if (ts5.isIdentifier(typeNode.typeName)) {
|
|
690
|
+
return typeNode.typeName.text;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (ts5.isTypeAliasDeclaration(typeNode.parent)) {
|
|
694
|
+
if (ts5.isIdentifier(typeNode.parent.name)) {
|
|
695
|
+
return typeNode.parent.name.text;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/compiler/schema/objectHandler.ts
|
|
702
|
+
import ts6 from "typescript";
|
|
588
703
|
function handleObjectType(type, ctx, typeNode) {
|
|
589
|
-
const { checker, components, typeStack
|
|
704
|
+
const { checker, components, typeStack } = ctx;
|
|
590
705
|
const symbol = type.getSymbol();
|
|
591
706
|
const typeName = symbol?.getName?.() ?? getTypeNameFromNode(typeNode, ctx);
|
|
592
707
|
if (isMetalOrmWrapperType(type, checker)) {
|
|
593
708
|
return handleMetalOrmWrapper(type, ctx);
|
|
594
709
|
}
|
|
595
710
|
if (typeName && typeName !== "__type") {
|
|
711
|
+
const isMetalOrmGeneric = METAL_ORM_WRAPPER_NAMES.some(
|
|
712
|
+
(name) => typeName === name || typeName.endsWith("Api")
|
|
713
|
+
);
|
|
714
|
+
if (isMetalOrmGeneric) {
|
|
715
|
+
return {};
|
|
716
|
+
}
|
|
596
717
|
if (components.has(typeName)) {
|
|
597
718
|
return { $ref: `#/components/schemas/${typeName}` };
|
|
598
719
|
}
|
|
@@ -613,40 +734,7 @@ function handleObjectType(type, ctx, typeNode) {
|
|
|
613
734
|
typeStack.delete(type);
|
|
614
735
|
return schema;
|
|
615
736
|
}
|
|
616
|
-
function
|
|
617
|
-
if (!typeNode) return null;
|
|
618
|
-
if (ts3.isTypeReferenceNode(typeNode)) {
|
|
619
|
-
if (ts3.isIdentifier(typeNode.typeName)) {
|
|
620
|
-
return typeNode.typeName.text;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
if (ts3.isTypeAliasDeclaration(typeNode.parent)) {
|
|
624
|
-
if (ts3.isIdentifier(typeNode.parent.name)) {
|
|
625
|
-
return typeNode.parent.name.text;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
function shouldBeIntegerType(typeName) {
|
|
631
|
-
if (!typeName) return false;
|
|
632
|
-
const lower = typeName.toLowerCase();
|
|
633
|
-
return lower === "id" || lower.endsWith("id") || lower === "primarykey" || lower === "pk";
|
|
634
|
-
}
|
|
635
|
-
function normalizeNumericType(type, checker, typeNode) {
|
|
636
|
-
const typeName = getExplicitTypeNameFromNode(typeNode) ?? null;
|
|
637
|
-
const symbol = getEffectiveSymbol(type, checker);
|
|
638
|
-
const symbolName = symbol?.getName() ?? null;
|
|
639
|
-
if (shouldBeIntegerType(typeName) || shouldBeIntegerType(symbolName)) {
|
|
640
|
-
return { type: "integer" };
|
|
641
|
-
}
|
|
642
|
-
return { type: "number" };
|
|
643
|
-
}
|
|
644
|
-
function getTypeNameFromNode(typeNode, ctx) {
|
|
645
|
-
const explicitName = getExplicitTypeNameFromNode(typeNode);
|
|
646
|
-
if (explicitName) return explicitName;
|
|
647
|
-
return `Anonymous_${ctx.typeNameStack.length}`;
|
|
648
|
-
}
|
|
649
|
-
function buildObjectSchema(type, ctx, typeNode) {
|
|
737
|
+
function buildObjectSchema(type, ctx, _typeNode) {
|
|
650
738
|
const { checker, mode } = ctx;
|
|
651
739
|
const properties = {};
|
|
652
740
|
const required = [];
|
|
@@ -660,9 +748,10 @@ function buildObjectSchema(type, ctx, typeNode) {
|
|
|
660
748
|
if (isMethodLike(propType)) {
|
|
661
749
|
continue;
|
|
662
750
|
}
|
|
663
|
-
const isOptional = !!(prop.flags &
|
|
751
|
+
const isOptional = !!(prop.flags & ts6.SymbolFlags.Optional);
|
|
664
752
|
const isRelation = isMetalOrmWrapperType(propType, checker);
|
|
665
|
-
|
|
753
|
+
const propCtx = { ...ctx, propertyName: propName };
|
|
754
|
+
properties[propName] = typeToJsonSchema(propType, propCtx);
|
|
666
755
|
const shouldRequire = mode === "response" ? !isRelation && !isOptional : !isOptional;
|
|
667
756
|
if (shouldRequire) {
|
|
668
757
|
required.push(propName);
|
|
@@ -683,14 +772,14 @@ function buildObjectSchema(type, ctx, typeNode) {
|
|
|
683
772
|
}
|
|
684
773
|
return schema;
|
|
685
774
|
}
|
|
686
|
-
function isRecordType(type,
|
|
775
|
+
function isRecordType(type, _checker) {
|
|
687
776
|
const symbol = type.getSymbol();
|
|
688
777
|
if (!symbol) return false;
|
|
689
778
|
const name = symbol.getName();
|
|
690
779
|
if (name === "Record") return true;
|
|
691
780
|
return false;
|
|
692
781
|
}
|
|
693
|
-
function getRecordValueType(type,
|
|
782
|
+
function getRecordValueType(type, _checker) {
|
|
694
783
|
const symbol = type.getSymbol();
|
|
695
784
|
if (!symbol) return null;
|
|
696
785
|
const name = symbol.getName();
|
|
@@ -703,24 +792,8 @@ function getRecordValueType(type, checker) {
|
|
|
703
792
|
}
|
|
704
793
|
return null;
|
|
705
794
|
}
|
|
706
|
-
var METAL_ORM_WRAPPER_NAMES = ["HasManyCollection", "ManyToManyCollection", "BelongsToReference", "HasOneReference"];
|
|
707
795
|
function isMetalOrmWrapperType(type, checker) {
|
|
708
|
-
|
|
709
|
-
if (!aliasSymbol) return false;
|
|
710
|
-
return METAL_ORM_WRAPPER_NAMES.includes(aliasSymbol.getName());
|
|
711
|
-
}
|
|
712
|
-
function getWrapperTypeName(type, checker) {
|
|
713
|
-
const symbol = getEffectiveSymbol(type, checker);
|
|
714
|
-
if (!symbol) return null;
|
|
715
|
-
const name = symbol.getName();
|
|
716
|
-
return METAL_ORM_WRAPPER_NAMES.includes(name) ? name : null;
|
|
717
|
-
}
|
|
718
|
-
function getEffectiveSymbol(type, checker) {
|
|
719
|
-
const aliasSymbol = type.aliasSymbol ?? type.aliasSymbol;
|
|
720
|
-
if (aliasSymbol && aliasSymbol.flags & ts3.SymbolFlags.Alias) {
|
|
721
|
-
return checker.getAliasedSymbol(aliasSymbol);
|
|
722
|
-
}
|
|
723
|
-
return type.getSymbol() ?? null;
|
|
796
|
+
return !!findMetalOrmWrapper(type, checker);
|
|
724
797
|
}
|
|
725
798
|
function isMethodLike(type) {
|
|
726
799
|
const callSigs = type.getCallSignatures?.();
|
|
@@ -729,6 +802,69 @@ function isMethodLike(type) {
|
|
|
729
802
|
function isIteratorOrSymbolProperty(propName) {
|
|
730
803
|
return propName.startsWith("__@") || propName.startsWith("[") || propName === Symbol.iterator.toString();
|
|
731
804
|
}
|
|
805
|
+
function getTypeNameFromNode(typeNode, _ctx) {
|
|
806
|
+
const explicitName = getExplicitTypeNameFromNode4(typeNode);
|
|
807
|
+
if (explicitName) return explicitName;
|
|
808
|
+
return "Anonymous_${ctx.typeNameStack.length}";
|
|
809
|
+
}
|
|
810
|
+
function getExplicitTypeNameFromNode4(typeNode) {
|
|
811
|
+
if (!typeNode) return null;
|
|
812
|
+
if (ts6.isTypeReferenceNode(typeNode)) {
|
|
813
|
+
if (ts6.isIdentifier(typeNode.typeName)) {
|
|
814
|
+
return typeNode.typeName.text;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (ts6.isTypeAliasDeclaration(typeNode.parent)) {
|
|
818
|
+
if (ts6.isIdentifier(typeNode.parent.name)) {
|
|
819
|
+
return typeNode.parent.name.text;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
var METAL_ORM_WRAPPER_NAMES = ["HasManyCollection", "ManyToManyCollection", "BelongsToReference", "HasOneReference"];
|
|
825
|
+
function findMetalOrmWrapper(type, checker) {
|
|
826
|
+
if (type.isIntersection()) {
|
|
827
|
+
let wrapperInfo = null;
|
|
828
|
+
let hasReadonlyArray = false;
|
|
829
|
+
for (const constituent of type.types) {
|
|
830
|
+
const result = findWrapperInType(constituent, checker);
|
|
831
|
+
if (result) {
|
|
832
|
+
wrapperInfo = result;
|
|
833
|
+
}
|
|
834
|
+
if (!(constituent.flags & ts6.TypeFlags.Object)) continue;
|
|
835
|
+
const symbol = constituent.getSymbol();
|
|
836
|
+
if (symbol?.getName() === "ReadonlyArray") {
|
|
837
|
+
hasReadonlyArray = true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (wrapperInfo) {
|
|
841
|
+
return { ...wrapperInfo, isReadonlyArray: hasReadonlyArray };
|
|
842
|
+
}
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
return findWrapperInType(type, checker);
|
|
846
|
+
}
|
|
847
|
+
function findWrapperInType(type, checker) {
|
|
848
|
+
const aliasSymbol = type.aliasSymbol ?? type.aliasSymbol;
|
|
849
|
+
const symbol = type.getSymbol();
|
|
850
|
+
const effectiveSymbol = aliasSymbol && aliasSymbol.flags & ts6.SymbolFlags.Alias ? checker.getAliasedSymbol(aliasSymbol) : symbol;
|
|
851
|
+
if (!effectiveSymbol) return null;
|
|
852
|
+
const name = effectiveSymbol.getName();
|
|
853
|
+
if (!METAL_ORM_WRAPPER_NAMES.includes(name)) return null;
|
|
854
|
+
const typeRef = type;
|
|
855
|
+
const typeArgs = typeRef.typeArguments || [];
|
|
856
|
+
return {
|
|
857
|
+
wrapperName: name,
|
|
858
|
+
targetTypeArgs: typeArgs,
|
|
859
|
+
isReadonlyArray: false
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function getWrapperTypeName(type, _checker) {
|
|
863
|
+
const symbol = type.getSymbol();
|
|
864
|
+
if (!symbol) return null;
|
|
865
|
+
const name = symbol.getName();
|
|
866
|
+
return METAL_ORM_WRAPPER_NAMES.includes(name) ? name : null;
|
|
867
|
+
}
|
|
732
868
|
function handleMetalOrmWrapper(type, ctx) {
|
|
733
869
|
const typeRef = type;
|
|
734
870
|
const typeArgs = typeRef.typeArguments;
|
|
@@ -747,30 +883,138 @@ function handleMetalOrmWrapper(type, ctx) {
|
|
|
747
883
|
"x-metal-orm-rel": wrapperRel
|
|
748
884
|
};
|
|
749
885
|
}
|
|
750
|
-
|
|
886
|
+
if (!targetType) {
|
|
887
|
+
return { "x-metal-orm-rel": wrapperRel };
|
|
888
|
+
}
|
|
889
|
+
if (wrapperName === "BelongsToReference" || wrapperName === "HasOneReference") {
|
|
890
|
+
return handleBelongsToReference(targetType, ctx, wrapperRel);
|
|
891
|
+
}
|
|
892
|
+
const targetSchema = typeToJsonSchema(targetType, ctx);
|
|
751
893
|
return {
|
|
752
894
|
...targetSchema,
|
|
753
895
|
"x-metal-orm-rel": wrapperRel
|
|
754
896
|
};
|
|
755
897
|
}
|
|
898
|
+
function handleBelongsToReference(targetType, ctx, wrapperRel) {
|
|
899
|
+
const { components } = ctx;
|
|
900
|
+
const targetSymbol = targetType.getSymbol();
|
|
901
|
+
const typeName = targetSymbol?.getName();
|
|
902
|
+
if (!typeName) {
|
|
903
|
+
return {
|
|
904
|
+
type: "object",
|
|
905
|
+
properties: {},
|
|
906
|
+
"x-metal-orm-rel": wrapperRel
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const refSchemaName = `${typeName}Ref`;
|
|
910
|
+
if (components.has(refSchemaName)) {
|
|
911
|
+
return {
|
|
912
|
+
$ref: `#/components/schemas/${refSchemaName}`,
|
|
913
|
+
"x-metal-orm-rel": wrapperRel
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const refSchema = buildRefSchema(targetType, ctx);
|
|
917
|
+
components.set(refSchemaName, refSchema);
|
|
918
|
+
return {
|
|
919
|
+
$ref: `#/components/schemas/${refSchemaName}`,
|
|
920
|
+
"x-metal-orm-rel": wrapperRel
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function buildRefSchema(type, ctx) {
|
|
924
|
+
const { checker } = ctx;
|
|
925
|
+
if (!(type.flags & ts6.TypeFlags.Object)) {
|
|
926
|
+
return { type: "object", properties: {} };
|
|
927
|
+
}
|
|
928
|
+
const objectType = type;
|
|
929
|
+
const properties = {};
|
|
930
|
+
const required = [];
|
|
931
|
+
const props = checker.getPropertiesOfType(objectType);
|
|
932
|
+
for (const prop of props) {
|
|
933
|
+
const propName = prop.getName();
|
|
934
|
+
if (isIteratorOrSymbolProperty(propName)) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
const propType = checker.getTypeOfSymbol(prop);
|
|
938
|
+
if (isMethodLike(propType)) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const isOptional = !!(prop.flags & ts6.SymbolFlags.Optional);
|
|
942
|
+
const isRelation = isMetalOrmWrapperType(propType, checker);
|
|
943
|
+
if (isRelation) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const propCtx = { ...ctx, propertyName: propName };
|
|
947
|
+
properties[propName] = typeToJsonSchema(propType, propCtx);
|
|
948
|
+
if (!isOptional) {
|
|
949
|
+
required.push(propName);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const schema = {
|
|
953
|
+
type: "object",
|
|
954
|
+
properties
|
|
955
|
+
};
|
|
956
|
+
if (required.length > 0) {
|
|
957
|
+
schema.required = required;
|
|
958
|
+
}
|
|
959
|
+
return schema;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/compiler/schema/typeToJsonSchema.ts
|
|
963
|
+
function typeToJsonSchema(type, ctx, typeNode) {
|
|
964
|
+
const primitiveResult = handlePrimitiveType(type, ctx, typeNode);
|
|
965
|
+
if (primitiveResult) {
|
|
966
|
+
return primitiveResult;
|
|
967
|
+
}
|
|
968
|
+
if (type.isUnion()) {
|
|
969
|
+
return handleUnion(type, ctx, typeNode);
|
|
970
|
+
}
|
|
971
|
+
if (type.isIntersection()) {
|
|
972
|
+
return handleIntersection(type, ctx, typeNode);
|
|
973
|
+
}
|
|
974
|
+
if (ctx.checker.isArrayType(type)) {
|
|
975
|
+
const typeArgs = type.typeArguments;
|
|
976
|
+
const itemType = typeArgs?.[0];
|
|
977
|
+
const items = itemType ? typeToJsonSchema(itemType, ctx) : {};
|
|
978
|
+
return {
|
|
979
|
+
type: "array",
|
|
980
|
+
items,
|
|
981
|
+
uniqueItems: isSetType(type, ctx.checker) ? true : void 0
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
if (type.flags & ts7.TypeFlags.Object) {
|
|
985
|
+
const objectType = type;
|
|
986
|
+
if (isMetalOrmWrapperType(type, ctx.checker)) {
|
|
987
|
+
return handleMetalOrmWrapper(objectType, ctx);
|
|
988
|
+
}
|
|
989
|
+
return handleObjectType(objectType, ctx, typeNode);
|
|
990
|
+
}
|
|
991
|
+
return {};
|
|
992
|
+
}
|
|
993
|
+
function isSetType(type, _checker) {
|
|
994
|
+
const symbol = type.getSymbol();
|
|
995
|
+
if (!symbol) return false;
|
|
996
|
+
const name = symbol.getName();
|
|
997
|
+
if (name === "Set") return true;
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
756
1000
|
|
|
757
1001
|
// src/compiler/schema/extractAnnotations.ts
|
|
758
|
-
import
|
|
1002
|
+
import ts8 from "typescript";
|
|
759
1003
|
function extractPropertySchemaFragments(checker, prop) {
|
|
760
|
-
if (!
|
|
761
|
-
const decs =
|
|
1004
|
+
if (!ts8.canHaveDecorators(prop)) return [];
|
|
1005
|
+
const decs = ts8.getDecorators(prop);
|
|
762
1006
|
if (!decs || decs.length === 0) return [];
|
|
763
1007
|
const frags = [];
|
|
764
1008
|
for (const d of decs) {
|
|
765
1009
|
const expr = d.expression;
|
|
766
1010
|
let callee;
|
|
767
1011
|
let args;
|
|
768
|
-
if (
|
|
1012
|
+
if (ts8.isCallExpression(expr)) {
|
|
769
1013
|
callee = expr.expression;
|
|
770
1014
|
args = expr.arguments;
|
|
771
1015
|
} else {
|
|
772
1016
|
callee = expr;
|
|
773
|
-
args =
|
|
1017
|
+
args = ts8.factory.createNodeArray([]);
|
|
774
1018
|
}
|
|
775
1019
|
const sym = checker.getSymbolAtLocation(callee);
|
|
776
1020
|
if (!sym) continue;
|
|
@@ -779,7 +1023,7 @@ function extractPropertySchemaFragments(checker, prop) {
|
|
|
779
1023
|
const name = resolved.name;
|
|
780
1024
|
if (name === "Schema") {
|
|
781
1025
|
const obj = args[0];
|
|
782
|
-
if (obj &&
|
|
1026
|
+
if (obj && ts8.isObjectLiteralExpression(obj)) {
|
|
783
1027
|
const frag = objectLiteralToJson(obj);
|
|
784
1028
|
if (frag) frags.push(frag);
|
|
785
1029
|
}
|
|
@@ -801,7 +1045,7 @@ function extractPropertySchemaFragments(checker, prop) {
|
|
|
801
1045
|
frags.push({ format: args[0].text });
|
|
802
1046
|
} else if (name === "Pattern") {
|
|
803
1047
|
const arg = args[0];
|
|
804
|
-
if (arg &&
|
|
1048
|
+
if (arg && ts8.isRegularExpressionLiteral(arg)) {
|
|
805
1049
|
frags.push({ pattern: extractRegexPattern(arg.text) });
|
|
806
1050
|
} else if (isStringLiteral(arg)) {
|
|
807
1051
|
frags.push({ pattern: arg.text });
|
|
@@ -818,11 +1062,11 @@ function extractPropertySchemaFragments(checker, prop) {
|
|
|
818
1062
|
frags.push({ multipleOf: Number(args[0].text) });
|
|
819
1063
|
} else if (name === "Example") {
|
|
820
1064
|
frags.push({ example: literalToJson(args[0]) });
|
|
821
|
-
} else if (name === "Examples" &&
|
|
1065
|
+
} else if (name === "Examples" && ts8.isArrayLiteralExpression(args[0])) {
|
|
822
1066
|
frags.push({ examples: args[0].elements.map((e) => literalToJson(e)) });
|
|
823
1067
|
} else if (name === "Description" && isStringLiteral(args[0])) {
|
|
824
1068
|
frags.push({ description: args[0].text });
|
|
825
|
-
} else if (name === "Enum" &&
|
|
1069
|
+
} else if (name === "Enum" && ts8.isArrayLiteralExpression(args[0])) {
|
|
826
1070
|
frags.push({ enum: args[0].elements.map((e) => literalToJson(e)) });
|
|
827
1071
|
} else if (name === "Const") {
|
|
828
1072
|
frags.push({ const: literalToJson(args[0]) });
|
|
@@ -830,9 +1074,9 @@ function extractPropertySchemaFragments(checker, prop) {
|
|
|
830
1074
|
frags.push({ default: literalToJson(args[0]) });
|
|
831
1075
|
} else if (name === "AdditionalProperties") {
|
|
832
1076
|
const arg = args[0];
|
|
833
|
-
if (arg && (arg.kind ===
|
|
834
|
-
frags.push({ additionalProperties: arg.kind ===
|
|
835
|
-
} else if (arg &&
|
|
1077
|
+
if (arg && (arg.kind === ts8.SyntaxKind.FalseKeyword || arg.kind === ts8.SyntaxKind.TrueKeyword)) {
|
|
1078
|
+
frags.push({ additionalProperties: arg.kind === ts8.SyntaxKind.TrueKeyword });
|
|
1079
|
+
} else if (arg && ts8.isObjectLiteralExpression(arg)) {
|
|
836
1080
|
const obj = objectLiteralToJson(arg);
|
|
837
1081
|
if (obj) frags.push({ additionalProperties: obj });
|
|
838
1082
|
}
|
|
@@ -845,7 +1089,7 @@ function extractPropertySchemaFragments(checker, prop) {
|
|
|
845
1089
|
return frags;
|
|
846
1090
|
}
|
|
847
1091
|
function resolveImportedDecorator(checker, sym) {
|
|
848
|
-
const target = sym.flags &
|
|
1092
|
+
const target = sym.flags & ts8.SymbolFlags.Alias ? checker.getAliasedSymbol(sym) : sym;
|
|
849
1093
|
const name = target.getName();
|
|
850
1094
|
const decl = target.declarations?.[0];
|
|
851
1095
|
if (!decl) return null;
|
|
@@ -856,10 +1100,10 @@ function resolveImportedDecorator(checker, sym) {
|
|
|
856
1100
|
return null;
|
|
857
1101
|
}
|
|
858
1102
|
function isNumberLiteral(node) {
|
|
859
|
-
return !!node &&
|
|
1103
|
+
return !!node && ts8.isNumericLiteral(node);
|
|
860
1104
|
}
|
|
861
1105
|
function isStringLiteral(node) {
|
|
862
|
-
return !!node &&
|
|
1106
|
+
return !!node && ts8.isStringLiteral(node);
|
|
863
1107
|
}
|
|
864
1108
|
function extractRegexPattern(text) {
|
|
865
1109
|
const match = text.match(/^\/(.+)\/[gimsuy]*$/);
|
|
@@ -868,12 +1112,12 @@ function extractRegexPattern(text) {
|
|
|
868
1112
|
function objectLiteralToJson(obj) {
|
|
869
1113
|
const out = {};
|
|
870
1114
|
for (const prop of obj.properties) {
|
|
871
|
-
if (!
|
|
1115
|
+
if (!ts8.isPropertyAssignment(prop)) continue;
|
|
872
1116
|
const name = prop.name;
|
|
873
1117
|
let key;
|
|
874
|
-
if (
|
|
1118
|
+
if (ts8.isIdentifier(name)) {
|
|
875
1119
|
key = name.text;
|
|
876
|
-
} else if (
|
|
1120
|
+
} else if (ts8.isStringLiteral(name)) {
|
|
877
1121
|
key = name.text;
|
|
878
1122
|
} else {
|
|
879
1123
|
continue;
|
|
@@ -883,13 +1127,13 @@ function objectLiteralToJson(obj) {
|
|
|
883
1127
|
return out;
|
|
884
1128
|
}
|
|
885
1129
|
function literalToJson(node) {
|
|
886
|
-
if (
|
|
887
|
-
if (
|
|
888
|
-
if (node.kind ===
|
|
889
|
-
if (node.kind ===
|
|
890
|
-
if (node.kind ===
|
|
891
|
-
if (
|
|
892
|
-
if (
|
|
1130
|
+
if (ts8.isStringLiteral(node)) return node.text;
|
|
1131
|
+
if (ts8.isNumericLiteral(node)) return Number(node.text);
|
|
1132
|
+
if (node.kind === ts8.SyntaxKind.TrueKeyword) return true;
|
|
1133
|
+
if (node.kind === ts8.SyntaxKind.FalseKeyword) return false;
|
|
1134
|
+
if (node.kind === ts8.SyntaxKind.NullKeyword) return null;
|
|
1135
|
+
if (ts8.isObjectLiteralExpression(node)) return objectLiteralToJson(node);
|
|
1136
|
+
if (ts8.isArrayLiteralExpression(node)) return node.elements.map((e) => literalToJson(e));
|
|
893
1137
|
return void 0;
|
|
894
1138
|
}
|
|
895
1139
|
function mergeFragments(base, ...frags) {
|
|
@@ -900,120 +1144,7 @@ function mergeFragments(base, ...frags) {
|
|
|
900
1144
|
return result;
|
|
901
1145
|
}
|
|
902
1146
|
|
|
903
|
-
// src/compiler/schema/
|
|
904
|
-
function generateOpenAPI(controllers, checker, options = {}) {
|
|
905
|
-
const components = /* @__PURE__ */ new Map();
|
|
906
|
-
const ctx = {
|
|
907
|
-
checker,
|
|
908
|
-
components,
|
|
909
|
-
typeStack: /* @__PURE__ */ new Set(),
|
|
910
|
-
typeNameStack: [],
|
|
911
|
-
mode: "response"
|
|
912
|
-
};
|
|
913
|
-
const paths = {};
|
|
914
|
-
for (const controller of controllers) {
|
|
915
|
-
for (const operation of controller.operations) {
|
|
916
|
-
const fullPath = convertToOpenApiPath(controller.basePath, operation.path);
|
|
917
|
-
if (!paths[fullPath]) {
|
|
918
|
-
paths[fullPath] = {};
|
|
919
|
-
}
|
|
920
|
-
const method = operation.httpMethod.toLowerCase();
|
|
921
|
-
paths[fullPath][method] = buildOperation(operation, ctx, controller.consumes);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
return {
|
|
925
|
-
openapi: "3.1.0",
|
|
926
|
-
info: {
|
|
927
|
-
title: options.title ?? "API",
|
|
928
|
-
version: options.version ?? "1.0.0"
|
|
929
|
-
},
|
|
930
|
-
components: {
|
|
931
|
-
schemas: Object.fromEntries(components)
|
|
932
|
-
},
|
|
933
|
-
paths
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
function convertToOpenApiPath(basePath, path4) {
|
|
937
|
-
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
938
|
-
const converted = path4.replace(/:([^/]+)/g, "{$1}");
|
|
939
|
-
let fullPath = base + converted || "/";
|
|
940
|
-
if (fullPath.endsWith("/") && fullPath !== "/") {
|
|
941
|
-
fullPath = fullPath.slice(0, -1);
|
|
942
|
-
}
|
|
943
|
-
return fullPath;
|
|
944
|
-
}
|
|
945
|
-
function buildOperation(operation, ctx, controllerConsumes) {
|
|
946
|
-
const op = {
|
|
947
|
-
operationId: operation.operationId,
|
|
948
|
-
responses: {}
|
|
949
|
-
};
|
|
950
|
-
const parameters = [];
|
|
951
|
-
buildPathParameters(operation, ctx, parameters);
|
|
952
|
-
buildQueryParameters(operation, ctx, parameters);
|
|
953
|
-
buildHeaderParameters(operation, ctx, parameters);
|
|
954
|
-
buildCookieParameters(operation, ctx, parameters);
|
|
955
|
-
if (parameters.length > 0) {
|
|
956
|
-
op.parameters = parameters;
|
|
957
|
-
}
|
|
958
|
-
const responseCtx = { ...ctx, mode: "response" };
|
|
959
|
-
const responseSchema = typeToJsonSchema(operation.returnType, responseCtx, operation.returnTypeNode);
|
|
960
|
-
const status = operation.httpMethod === "POST" ? 201 : 200;
|
|
961
|
-
op.responses[status] = {
|
|
962
|
-
description: status === 201 ? "Created" : "OK",
|
|
963
|
-
content: {
|
|
964
|
-
"application/json": {
|
|
965
|
-
schema: responseSchema
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
};
|
|
969
|
-
if (["POST", "PUT", "PATCH"].includes(operation.httpMethod) && operation.bodyParamIndex !== null) {
|
|
970
|
-
const bodyParam = operation.parameters[operation.bodyParamIndex];
|
|
971
|
-
if (bodyParam) {
|
|
972
|
-
const requestCtx = { ...ctx, mode: "request" };
|
|
973
|
-
let bodySchema = typeToJsonSchema(bodyParam.type, requestCtx);
|
|
974
|
-
bodySchema = mergeBodySchemaAnnotations(bodyParam, requestCtx, bodySchema);
|
|
975
|
-
const contentType = operation.bodyContentType ?? controllerConsumes?.[0] ?? "application/json";
|
|
976
|
-
const requestBody = {
|
|
977
|
-
required: !bodyParam.isOptional,
|
|
978
|
-
content: {}
|
|
979
|
-
};
|
|
980
|
-
if (contentType === "multipart/form-data") {
|
|
981
|
-
requestBody.content["multipart/form-data"] = {
|
|
982
|
-
schema: bodySchema
|
|
983
|
-
};
|
|
984
|
-
} else {
|
|
985
|
-
requestBody.content[contentType] = {
|
|
986
|
-
schema: bodySchema
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
op.requestBody = requestBody;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
return op;
|
|
993
|
-
}
|
|
994
|
-
function mergeBodySchemaAnnotations(bodyParam, ctx, schema) {
|
|
995
|
-
if (!schema.properties) return schema;
|
|
996
|
-
const typeSymbol = bodyParam.type.getSymbol();
|
|
997
|
-
if (!typeSymbol) return schema;
|
|
998
|
-
const declarations = typeSymbol.getDeclarations();
|
|
999
|
-
if (!declarations || declarations.length === 0) return schema;
|
|
1000
|
-
const classDecl = declarations[0];
|
|
1001
|
-
if (!ts5.isClassDeclaration(classDecl)) return schema;
|
|
1002
|
-
const result = { ...schema };
|
|
1003
|
-
const props = { ...result.properties };
|
|
1004
|
-
for (const member of classDecl.members) {
|
|
1005
|
-
if (!ts5.isPropertyDeclaration(member) || !member.name) continue;
|
|
1006
|
-
const propName = ts5.isIdentifier(member.name) ? member.name.text : null;
|
|
1007
|
-
if (!propName) continue;
|
|
1008
|
-
if (!props[propName]) continue;
|
|
1009
|
-
const frags = extractPropertySchemaFragments(ctx.checker, member);
|
|
1010
|
-
if (frags.length > 0) {
|
|
1011
|
-
props[propName] = mergeFragments(props[propName], ...frags);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
result.properties = props;
|
|
1015
|
-
return result;
|
|
1016
|
-
}
|
|
1147
|
+
// src/compiler/schema/parameters.ts
|
|
1017
1148
|
function buildPathParameters(operation, ctx, parameters) {
|
|
1018
1149
|
for (const paramIndex of operation.pathParamIndices) {
|
|
1019
1150
|
const param = operation.parameters[paramIndex];
|
|
@@ -1026,71 +1157,22 @@ function buildPathParameters(operation, ctx, parameters) {
|
|
|
1026
1157
|
}
|
|
1027
1158
|
}
|
|
1028
1159
|
const schema = paramSchema.$ref ? { $ref: paramSchema.$ref } : paramSchema;
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
schema
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
function isObjectLikeSchema(schema, ctx) {
|
|
1039
|
-
const resolved = resolveSchemaRef(schema, ctx.components);
|
|
1040
|
-
if (resolved.type === "object" || resolved.properties || resolved.additionalProperties) {
|
|
1041
|
-
return true;
|
|
1042
|
-
}
|
|
1043
|
-
if (resolved.allOf) {
|
|
1044
|
-
for (const branch of resolved.allOf) {
|
|
1045
|
-
if (isObjectLikeSchema(branch, ctx)) {
|
|
1046
|
-
return true;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
if (resolved.type === "array" && resolved.items) {
|
|
1051
|
-
const itemsSchema = resolveSchemaRef(resolved.items, ctx.components);
|
|
1052
|
-
return isObjectLikeSchema(itemsSchema, ctx);
|
|
1053
|
-
}
|
|
1054
|
-
return false;
|
|
1055
|
-
}
|
|
1056
|
-
function resolveSchemaRef(schema, components) {
|
|
1057
|
-
const ref = schema.$ref;
|
|
1058
|
-
if (typeof ref !== "string" || !ref.startsWith("#/components/schemas/")) {
|
|
1059
|
-
return schema;
|
|
1060
|
-
}
|
|
1061
|
-
const name = ref.replace("#/components/schemas/", "");
|
|
1062
|
-
const next = components.get(name);
|
|
1063
|
-
if (!next) return schema;
|
|
1064
|
-
return resolveSchemaRef(next, components);
|
|
1065
|
-
}
|
|
1066
|
-
function resolveAndCollectObjectProps(schema, components) {
|
|
1067
|
-
const resolved = resolveSchemaRef(schema, components);
|
|
1068
|
-
const properties = {};
|
|
1069
|
-
const required = [];
|
|
1070
|
-
const processSchema = (s) => {
|
|
1071
|
-
const current = resolveSchemaRef(s, components);
|
|
1072
|
-
if (current.properties) {
|
|
1073
|
-
for (const [key, val] of Object.entries(current.properties)) {
|
|
1074
|
-
if (!properties[key]) {
|
|
1075
|
-
properties[key] = val;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
if (current.required) {
|
|
1080
|
-
for (const req of current.required) {
|
|
1081
|
-
if (!required.includes(req)) {
|
|
1082
|
-
required.push(req);
|
|
1160
|
+
const paramName = param.name.toLowerCase();
|
|
1161
|
+
const isIdParam = paramName === "id" || paramName.endsWith("id");
|
|
1162
|
+
if (!schema.$ref && schema.type === "number" && isIdParam) {
|
|
1163
|
+
schema.type = "integer";
|
|
1164
|
+
if (!schema.minimum) {
|
|
1165
|
+
schema.minimum = 1;
|
|
1083
1166
|
}
|
|
1084
1167
|
}
|
|
1168
|
+
parameters.push({
|
|
1169
|
+
name: param.name,
|
|
1170
|
+
in: "path",
|
|
1171
|
+
required: !param.isOptional,
|
|
1172
|
+
schema
|
|
1173
|
+
});
|
|
1085
1174
|
}
|
|
1086
|
-
|
|
1087
|
-
for (const branch of current.allOf) {
|
|
1088
|
-
processSchema(branch);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
};
|
|
1092
|
-
processSchema(resolved);
|
|
1093
|
-
return { properties, required };
|
|
1175
|
+
}
|
|
1094
1176
|
}
|
|
1095
1177
|
function buildQueryParameters(operation, ctx, parameters) {
|
|
1096
1178
|
if (operation.queryObjectParamIndex !== null) {
|
|
@@ -1099,20 +1181,22 @@ function buildQueryParameters(operation, ctx, parameters) {
|
|
|
1099
1181
|
const querySchema = typeToJsonSchema(queryParam.type, ctx);
|
|
1100
1182
|
const { properties: queryObjProps, required: queryRequired } = resolveAndCollectObjectProps(querySchema, ctx.components);
|
|
1101
1183
|
for (const [propName, propSchema] of Object.entries(queryObjProps)) {
|
|
1102
|
-
const isRequired = queryRequired.includes(propName)
|
|
1184
|
+
const isRequired = queryRequired.includes(propName);
|
|
1103
1185
|
const isObjectLike = isObjectLikeSchema(propSchema, ctx);
|
|
1104
1186
|
const serialization = determineQuerySerialization(propSchema.type);
|
|
1187
|
+
const exampleValue = generateExampleValue(propSchema, propName);
|
|
1105
1188
|
if (isObjectLike) {
|
|
1189
|
+
const schemaRef = propSchema.$ref || "#/components/schemas/InlineQueryParam";
|
|
1106
1190
|
parameters.push({
|
|
1107
1191
|
name: propName,
|
|
1108
1192
|
in: "query",
|
|
1109
1193
|
required: isRequired,
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
}
|
|
1194
|
+
schema: { type: "string" },
|
|
1195
|
+
description: `JSON-encoded object. ${exampleValue}`,
|
|
1196
|
+
examples: {
|
|
1197
|
+
default: { value: parseExampleValue(exampleValue) }
|
|
1114
1198
|
},
|
|
1115
|
-
|
|
1199
|
+
"x-adorn-jsonSchemaRef": schemaRef
|
|
1116
1200
|
});
|
|
1117
1201
|
} else {
|
|
1118
1202
|
const paramDef = {
|
|
@@ -1158,15 +1242,18 @@ function buildQueryParameters(operation, ctx, parameters) {
|
|
|
1158
1242
|
}
|
|
1159
1243
|
const isObjectLike = isObjectLikeSchema(paramSchema, ctx);
|
|
1160
1244
|
if (isObjectLike) {
|
|
1245
|
+
const schemaRef = paramSchema.$ref || "#/components/schemas/InlineQueryParam";
|
|
1246
|
+
const exampleValue = generateExampleValue(paramSchema, param.name);
|
|
1161
1247
|
parameters.push({
|
|
1162
1248
|
name: param.name,
|
|
1163
1249
|
in: "query",
|
|
1164
1250
|
required: !param.isOptional,
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1251
|
+
schema: { type: "string" },
|
|
1252
|
+
description: `JSON-encoded object. ${exampleValue}`,
|
|
1253
|
+
examples: {
|
|
1254
|
+
default: { value: parseExampleValue(exampleValue) }
|
|
1255
|
+
},
|
|
1256
|
+
"x-adorn-jsonSchemaRef": schemaRef
|
|
1170
1257
|
});
|
|
1171
1258
|
} else {
|
|
1172
1259
|
const serialization = determineQuerySerialization(paramSchema.type);
|
|
@@ -1181,14 +1268,6 @@ function buildQueryParameters(operation, ctx, parameters) {
|
|
|
1181
1268
|
}
|
|
1182
1269
|
}
|
|
1183
1270
|
}
|
|
1184
|
-
function determineQuerySerialization(schemaType) {
|
|
1185
|
-
const typeArray = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
|
|
1186
|
-
const isArray = typeArray.includes("array");
|
|
1187
|
-
if (isArray) {
|
|
1188
|
-
return { style: "form", explode: true };
|
|
1189
|
-
}
|
|
1190
|
-
return {};
|
|
1191
|
-
}
|
|
1192
1271
|
function buildHeaderParameters(operation, ctx, parameters) {
|
|
1193
1272
|
if (operation.headerObjectParamIndex === null) return;
|
|
1194
1273
|
const headerParam = operation.parameters[operation.headerObjectParamIndex];
|
|
@@ -1225,9 +1304,324 @@ function buildCookieParameters(operation, ctx, parameters) {
|
|
|
1225
1304
|
});
|
|
1226
1305
|
}
|
|
1227
1306
|
}
|
|
1307
|
+
function determineQuerySerialization(schemaType) {
|
|
1308
|
+
const typeArray = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
|
|
1309
|
+
const isArray = typeArray.includes("array");
|
|
1310
|
+
if (isArray) {
|
|
1311
|
+
return { style: "form", explode: true };
|
|
1312
|
+
}
|
|
1313
|
+
return {};
|
|
1314
|
+
}
|
|
1315
|
+
function generateExampleValue(schema, propName) {
|
|
1316
|
+
const resolved = resolveSchemaRef(schema, /* @__PURE__ */ new Map());
|
|
1317
|
+
if (resolved.type === "object" && resolved.properties) {
|
|
1318
|
+
const example = {};
|
|
1319
|
+
for (const [key, prop] of Object.entries(resolved.properties)) {
|
|
1320
|
+
const propResolved = resolveSchemaRef(prop, /* @__PURE__ */ new Map());
|
|
1321
|
+
if (propResolved.type === "string") {
|
|
1322
|
+
example[key] = "value";
|
|
1323
|
+
} else if (propResolved.type === "number" || propResolved.type === "integer") {
|
|
1324
|
+
example[key] = 1;
|
|
1325
|
+
} else if (propResolved.type === "boolean") {
|
|
1326
|
+
example[key] = true;
|
|
1327
|
+
} else if (Array.isArray(propResolved.type) && propResolved.type.includes("null")) {
|
|
1328
|
+
example[key] = null;
|
|
1329
|
+
} else if (propResolved.enum) {
|
|
1330
|
+
example[key] = propResolved.enum[0];
|
|
1331
|
+
} else {
|
|
1332
|
+
example[key] = "value";
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return `Example: ${propName}=${JSON.stringify(example)}`;
|
|
1336
|
+
}
|
|
1337
|
+
return `Example: ${propName}=${JSON.stringify({ key: "value" })}`;
|
|
1338
|
+
}
|
|
1339
|
+
function parseExampleValue(description) {
|
|
1340
|
+
const match = description.match(/Example:\s*\w+=(\{[^}]+\})/);
|
|
1341
|
+
if (match) {
|
|
1342
|
+
return match[1];
|
|
1343
|
+
}
|
|
1344
|
+
return JSON.stringify({ key: "value" });
|
|
1345
|
+
}
|
|
1346
|
+
function isObjectLikeSchema(schema, ctx) {
|
|
1347
|
+
const resolved = resolveSchemaRef(schema, ctx.components);
|
|
1348
|
+
if (resolved.type === "object" || resolved.properties || resolved.additionalProperties) {
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
if (resolved.allOf) {
|
|
1352
|
+
for (const branch of resolved.allOf) {
|
|
1353
|
+
if (isObjectLikeSchema(branch, ctx)) {
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (resolved.type === "array" && resolved.items) {
|
|
1359
|
+
const itemsSchema = resolveSchemaRef(resolved.items, ctx.components);
|
|
1360
|
+
return isObjectLikeSchema(itemsSchema, ctx);
|
|
1361
|
+
}
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
function resolveSchemaRef(schema, components) {
|
|
1365
|
+
const ref = schema.$ref;
|
|
1366
|
+
if (typeof ref !== "string" || !ref.startsWith("#/components/schemas/")) {
|
|
1367
|
+
return schema;
|
|
1368
|
+
}
|
|
1369
|
+
const name = ref.replace("#/components/schemas/", "");
|
|
1370
|
+
const next = components.get(name);
|
|
1371
|
+
if (!next) return schema;
|
|
1372
|
+
return resolveSchemaRef(next, components);
|
|
1373
|
+
}
|
|
1374
|
+
function resolveAndCollectObjectProps(schema, components) {
|
|
1375
|
+
const resolved = resolveSchemaRef(schema, components);
|
|
1376
|
+
const properties = {};
|
|
1377
|
+
const required = [];
|
|
1378
|
+
const processSchema = (s) => {
|
|
1379
|
+
const current = resolveSchemaRef(s, components);
|
|
1380
|
+
if (current.properties) {
|
|
1381
|
+
for (const [key, val] of Object.entries(current.properties)) {
|
|
1382
|
+
if (!properties[key]) {
|
|
1383
|
+
properties[key] = val;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (current.required) {
|
|
1388
|
+
for (const req of current.required) {
|
|
1389
|
+
if (!required.includes(req)) {
|
|
1390
|
+
required.push(req);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (current.allOf) {
|
|
1395
|
+
for (const branch of current.allOf) {
|
|
1396
|
+
processSchema(branch);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
processSchema(resolved);
|
|
1401
|
+
return { properties, required };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/compiler/schema/openapi.ts
|
|
1405
|
+
var METAL_ORM_WRAPPER_NAMES2 = ["BelongsToReference", "HasOneReference", "HasManyCollection", "ManyToManyCollection"];
|
|
1406
|
+
function generateOpenAPI(controllers, checker, options = {}) {
|
|
1407
|
+
const components = /* @__PURE__ */ new Map();
|
|
1408
|
+
const ctx = {
|
|
1409
|
+
checker,
|
|
1410
|
+
components,
|
|
1411
|
+
typeStack: /* @__PURE__ */ new Set(),
|
|
1412
|
+
typeNameStack: [],
|
|
1413
|
+
mode: "response"
|
|
1414
|
+
};
|
|
1415
|
+
const paths = {};
|
|
1416
|
+
for (const controller of controllers) {
|
|
1417
|
+
for (const operation of controller.operations) {
|
|
1418
|
+
const fullPath = convertToOpenApiPath(controller.basePath, operation.path);
|
|
1419
|
+
if (!paths[fullPath]) {
|
|
1420
|
+
paths[fullPath] = {};
|
|
1421
|
+
}
|
|
1422
|
+
const method = operation.httpMethod.toLowerCase();
|
|
1423
|
+
paths[fullPath][method] = buildOperation(operation, ctx, controller.consumes);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const schemas = Object.fromEntries(components);
|
|
1427
|
+
cleanupMetalOrmWrappers(schemas, paths);
|
|
1428
|
+
return {
|
|
1429
|
+
openapi: "3.1.0",
|
|
1430
|
+
info: {
|
|
1431
|
+
title: options.title ?? "API",
|
|
1432
|
+
version: options.version ?? "1.0.0"
|
|
1433
|
+
},
|
|
1434
|
+
components: {
|
|
1435
|
+
schemas
|
|
1436
|
+
},
|
|
1437
|
+
paths
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function cleanupMetalOrmWrappers(schemas, paths) {
|
|
1441
|
+
const schemasToDelete = /* @__PURE__ */ new Set();
|
|
1442
|
+
for (const wrapperName of METAL_ORM_WRAPPER_NAMES2) {
|
|
1443
|
+
if (schemas[wrapperName]) {
|
|
1444
|
+
schemasToDelete.add(wrapperName);
|
|
1445
|
+
}
|
|
1446
|
+
if (schemas[`${wrapperName}Api`]) {
|
|
1447
|
+
schemasToDelete.add(`${wrapperName}Api`);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
for (const schema of Object.values(schemas)) {
|
|
1451
|
+
cleanupSchemaRefs(schema, schemasToDelete);
|
|
1452
|
+
}
|
|
1453
|
+
for (const pathItem of Object.values(paths)) {
|
|
1454
|
+
cleanupPathItemRefs(pathItem, schemasToDelete);
|
|
1455
|
+
}
|
|
1456
|
+
for (const schemaName of schemasToDelete) {
|
|
1457
|
+
delete schemas[schemaName];
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function cleanupSchemaRefs(schema, schemasToDelete) {
|
|
1461
|
+
if (typeof schema !== "object" || schema === null) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (schema.properties) {
|
|
1465
|
+
for (const propName of Object.keys(schema.properties)) {
|
|
1466
|
+
const propSchema = schema.properties[propName];
|
|
1467
|
+
if (propSchema.$ref && typeof propSchema.$ref === "string") {
|
|
1468
|
+
const refName = propSchema.$ref.replace("#/components/schemas/", "");
|
|
1469
|
+
if (schemasToDelete.has(refName)) {
|
|
1470
|
+
delete schema.properties[propName];
|
|
1471
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
1472
|
+
schema.required = schema.required.filter((r) => r !== propName);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
} else {
|
|
1476
|
+
cleanupSchemaRefs(propSchema, schemasToDelete);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (schema.items) {
|
|
1481
|
+
cleanupSchemaRefs(schema.items, schemasToDelete);
|
|
1482
|
+
}
|
|
1483
|
+
if (schema.allOf) {
|
|
1484
|
+
for (const item of schema.allOf) {
|
|
1485
|
+
cleanupSchemaRefs(item, schemasToDelete);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function cleanupPathItemRefs(pathItem, schemasToDelete) {
|
|
1490
|
+
if (typeof pathItem !== "object" || pathItem === null) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
for (const method of Object.keys(pathItem)) {
|
|
1494
|
+
const operation = pathItem[method];
|
|
1495
|
+
if (typeof operation !== "object" || operation === null) continue;
|
|
1496
|
+
if (operation.requestBody) {
|
|
1497
|
+
cleanupRequestBodyRefs(operation.requestBody, schemasToDelete);
|
|
1498
|
+
}
|
|
1499
|
+
if (operation.responses) {
|
|
1500
|
+
const responses = Object.values(operation.responses);
|
|
1501
|
+
for (const response of responses) {
|
|
1502
|
+
if (response.content) {
|
|
1503
|
+
const contentTypes = Object.values(response.content);
|
|
1504
|
+
for (const contentType of contentTypes) {
|
|
1505
|
+
if (contentType.schema) {
|
|
1506
|
+
cleanupSchemaRefs(contentType.schema, schemasToDelete);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function cleanupRequestBodyRefs(requestBody, schemasToDelete) {
|
|
1515
|
+
if (typeof requestBody !== "object" || requestBody === null) return;
|
|
1516
|
+
if (requestBody.content) {
|
|
1517
|
+
const contentTypes = Object.values(requestBody.content);
|
|
1518
|
+
for (const contentType of contentTypes) {
|
|
1519
|
+
if (contentType.schema) {
|
|
1520
|
+
cleanupSchemaRefs(contentType.schema, schemasToDelete);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (requestBody.properties) {
|
|
1525
|
+
for (const propName of Object.keys(requestBody.properties)) {
|
|
1526
|
+
const propSchema = requestBody.properties[propName];
|
|
1527
|
+
if (propSchema.$ref && typeof propSchema.$ref === "string") {
|
|
1528
|
+
const refName = propSchema.$ref.replace("#/components/schemas/", "");
|
|
1529
|
+
if (schemasToDelete.has(refName)) {
|
|
1530
|
+
delete requestBody.properties[propName];
|
|
1531
|
+
if (requestBody.required && Array.isArray(requestBody.required)) {
|
|
1532
|
+
requestBody.required = requestBody.required.filter((r) => r !== propName);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
} else {
|
|
1536
|
+
cleanupSchemaRefs(propSchema, schemasToDelete);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function convertToOpenApiPath(basePath, path4) {
|
|
1542
|
+
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
1543
|
+
const converted = path4.replace(/:([^/]+)/g, "{$1}");
|
|
1544
|
+
let fullPath = base + converted || "/";
|
|
1545
|
+
if (fullPath.endsWith("/") && fullPath !== "/") {
|
|
1546
|
+
fullPath = fullPath.slice(0, -1);
|
|
1547
|
+
}
|
|
1548
|
+
return fullPath;
|
|
1549
|
+
}
|
|
1550
|
+
function buildOperation(operation, ctx, controllerConsumes) {
|
|
1551
|
+
const op = {
|
|
1552
|
+
operationId: operation.operationId,
|
|
1553
|
+
responses: {}
|
|
1554
|
+
};
|
|
1555
|
+
const parameters = [];
|
|
1556
|
+
buildPathParameters(operation, ctx, parameters);
|
|
1557
|
+
buildQueryParameters(operation, ctx, parameters);
|
|
1558
|
+
buildHeaderParameters(operation, ctx, parameters);
|
|
1559
|
+
buildCookieParameters(operation, ctx, parameters);
|
|
1560
|
+
if (parameters.length > 0) {
|
|
1561
|
+
op.parameters = parameters;
|
|
1562
|
+
}
|
|
1563
|
+
const responseCtx = { ...ctx, mode: "response" };
|
|
1564
|
+
const responseSchema = typeToJsonSchema(operation.returnType, responseCtx, operation.returnTypeNode);
|
|
1565
|
+
const status = operation.httpMethod === "POST" ? 201 : 200;
|
|
1566
|
+
op.responses[status] = {
|
|
1567
|
+
description: status === 201 ? "Created" : "OK",
|
|
1568
|
+
content: {
|
|
1569
|
+
"application/json": {
|
|
1570
|
+
schema: responseSchema
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
if (["POST", "PUT", "PATCH"].includes(operation.httpMethod) && operation.bodyParamIndex !== null) {
|
|
1575
|
+
const bodyParam = operation.parameters[operation.bodyParamIndex];
|
|
1576
|
+
if (bodyParam) {
|
|
1577
|
+
const requestCtx = { ...ctx, mode: "request" };
|
|
1578
|
+
let bodySchema = typeToJsonSchema(bodyParam.type, requestCtx);
|
|
1579
|
+
bodySchema = mergeBodySchemaAnnotations(bodyParam, requestCtx, bodySchema);
|
|
1580
|
+
const contentType = operation.bodyContentType ?? controllerConsumes?.[0] ?? "application/json";
|
|
1581
|
+
const requestBody = {
|
|
1582
|
+
required: !bodyParam.isOptional,
|
|
1583
|
+
content: {}
|
|
1584
|
+
};
|
|
1585
|
+
if (contentType === "multipart/form-data") {
|
|
1586
|
+
requestBody.content["multipart/form-data"] = {
|
|
1587
|
+
schema: bodySchema
|
|
1588
|
+
};
|
|
1589
|
+
} else {
|
|
1590
|
+
requestBody.content[contentType] = {
|
|
1591
|
+
schema: bodySchema
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
op.requestBody = requestBody;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return op;
|
|
1598
|
+
}
|
|
1599
|
+
function mergeBodySchemaAnnotations(bodyParam, ctx, schema) {
|
|
1600
|
+
if (!schema.properties) return schema;
|
|
1601
|
+
const typeSymbol = bodyParam.type.getSymbol();
|
|
1602
|
+
if (!typeSymbol) return schema;
|
|
1603
|
+
const declarations = typeSymbol.getDeclarations();
|
|
1604
|
+
if (!declarations || declarations.length === 0) return schema;
|
|
1605
|
+
const classDecl = declarations[0];
|
|
1606
|
+
if (!ts9.isClassDeclaration(classDecl)) return schema;
|
|
1607
|
+
const result = { ...schema };
|
|
1608
|
+
const props = { ...result.properties };
|
|
1609
|
+
for (const member of classDecl.members) {
|
|
1610
|
+
if (!ts9.isPropertyDeclaration(member) || !member.name) continue;
|
|
1611
|
+
const propName = ts9.isIdentifier(member.name) ? member.name.text : null;
|
|
1612
|
+
if (!propName) continue;
|
|
1613
|
+
if (!props[propName]) continue;
|
|
1614
|
+
const frags = extractPropertySchemaFragments(ctx.checker, member);
|
|
1615
|
+
if (frags.length > 0) {
|
|
1616
|
+
props[propName] = mergeFragments(props[propName], ...frags);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
result.properties = props;
|
|
1620
|
+
return result;
|
|
1621
|
+
}
|
|
1228
1622
|
|
|
1229
1623
|
// src/compiler/manifest/emit.ts
|
|
1230
|
-
import
|
|
1624
|
+
import ts10 from "typescript";
|
|
1231
1625
|
function generateManifest(controllers, checker, version, validationMode = "ajv-runtime") {
|
|
1232
1626
|
const components = /* @__PURE__ */ new Map();
|
|
1233
1627
|
const ctx = {
|
|
@@ -1249,7 +1643,7 @@ function generateManifest(controllers, checker, version, validationMode = "ajv-r
|
|
|
1249
1643
|
generator: {
|
|
1250
1644
|
name: "adorn-api",
|
|
1251
1645
|
version,
|
|
1252
|
-
typescript:
|
|
1646
|
+
typescript: ts10.version
|
|
1253
1647
|
},
|
|
1254
1648
|
schemas: {
|
|
1255
1649
|
kind: "openapi-3.1",
|
|
@@ -1403,7 +1797,7 @@ function buildQueryArgs(op, ctx, args) {
|
|
|
1403
1797
|
args.query.push({
|
|
1404
1798
|
name: propName,
|
|
1405
1799
|
index: queryParam.index,
|
|
1406
|
-
required:
|
|
1800
|
+
required: isRequired,
|
|
1407
1801
|
schemaRef,
|
|
1408
1802
|
schemaType: propSchema.type,
|
|
1409
1803
|
content: isObjectLike ? "application/json" : void 0
|
|
@@ -1442,7 +1836,7 @@ function buildHeaderArgs(op, ctx, args) {
|
|
|
1442
1836
|
args.headers.push({
|
|
1443
1837
|
name: propName,
|
|
1444
1838
|
index: headerParam.index,
|
|
1445
|
-
required:
|
|
1839
|
+
required: isRequired,
|
|
1446
1840
|
schemaRef,
|
|
1447
1841
|
schemaType: propSchema.type
|
|
1448
1842
|
});
|
|
@@ -1463,7 +1857,7 @@ function buildCookieArgs(op, ctx, args) {
|
|
|
1463
1857
|
args.cookies.push({
|
|
1464
1858
|
name: propName,
|
|
1465
1859
|
index: cookieParam.index,
|
|
1466
|
-
required:
|
|
1860
|
+
required: isRequired,
|
|
1467
1861
|
schemaRef,
|
|
1468
1862
|
schemaType: propSchema.type,
|
|
1469
1863
|
serialization: { style: "form", explode: true }
|
|
@@ -1479,7 +1873,7 @@ import Ajv from "ajv";
|
|
|
1479
1873
|
import addFormats from "ajv-formats";
|
|
1480
1874
|
var OAS_SCHEMA_ONLY = /* @__PURE__ */ new Set(["discriminator", "xml", "externalDocs", "example"]);
|
|
1481
1875
|
function sanitizeSchemaForAjv(schema) {
|
|
1482
|
-
if (schema
|
|
1876
|
+
if (schema === null || typeof schema !== "object") return schema;
|
|
1483
1877
|
if (Array.isArray(schema)) return schema.map(sanitizeSchemaForAjv);
|
|
1484
1878
|
const out = {};
|
|
1485
1879
|
for (const [k, v] of Object.entries(schema)) {
|
|
@@ -1490,7 +1884,7 @@ function sanitizeSchemaForAjv(schema) {
|
|
|
1490
1884
|
return out;
|
|
1491
1885
|
}
|
|
1492
1886
|
function rewriteComponentRefs(schema) {
|
|
1493
|
-
if (schema
|
|
1887
|
+
if (schema === null || typeof schema !== "object") return schema;
|
|
1494
1888
|
if (Array.isArray(schema)) return schema.map(rewriteComponentRefs);
|
|
1495
1889
|
if (typeof schema.$ref === "string") {
|
|
1496
1890
|
const ref = schema.$ref;
|
|
@@ -1567,16 +1961,13 @@ async function emitPrecompiledValidators(opts) {
|
|
|
1567
1961
|
`;
|
|
1568
1962
|
cjs += ` body: ${v.body ? `exports[${JSON.stringify(v.body)}]` : "undefined"},
|
|
1569
1963
|
`;
|
|
1570
|
-
cjs +=
|
|
1571
|
-
`;
|
|
1964
|
+
cjs += " response: {\n";
|
|
1572
1965
|
for (const [key, id] of Object.entries(v.response)) {
|
|
1573
1966
|
cjs += ` ${JSON.stringify(key)}: exports[${JSON.stringify(id)}],
|
|
1574
1967
|
`;
|
|
1575
1968
|
}
|
|
1576
|
-
cjs +=
|
|
1577
|
-
|
|
1578
|
-
cjs += ` },
|
|
1579
|
-
`;
|
|
1969
|
+
cjs += " }\n";
|
|
1970
|
+
cjs += " },\n";
|
|
1580
1971
|
}
|
|
1581
1972
|
cjs += "};\n";
|
|
1582
1973
|
fs.writeFileSync(cjsPath, cjs, "utf8");
|
|
@@ -1611,7 +2002,6 @@ export function validateResponse(operationId, status, contentType, data) {
|
|
|
1611
2002
|
// src/compiler/cache/isStale.ts
|
|
1612
2003
|
import fs2 from "fs";
|
|
1613
2004
|
import path2 from "path";
|
|
1614
|
-
import "typescript";
|
|
1615
2005
|
function readJson(p) {
|
|
1616
2006
|
try {
|
|
1617
2007
|
return JSON.parse(fs2.readFileSync(p, "utf8"));
|
|
@@ -1668,7 +2058,7 @@ function findLockfile(startDir) {
|
|
|
1668
2058
|
for (const n of names) {
|
|
1669
2059
|
const p = path2.join(dir, n);
|
|
1670
2060
|
const mt = statMtimeMs(p);
|
|
1671
|
-
if (mt
|
|
2061
|
+
if (mt !== null) return { path: p, mtimeMs: mt };
|
|
1672
2062
|
}
|
|
1673
2063
|
const parent = path2.dirname(dir);
|
|
1674
2064
|
if (parent === dir) break;
|
|
@@ -1693,22 +2083,22 @@ async function isStale(params) {
|
|
|
1693
2083
|
const chain = collectTsconfigChain(tsconfigAbs);
|
|
1694
2084
|
for (const cfg of chain) {
|
|
1695
2085
|
const mt = statMtimeMs(cfg);
|
|
1696
|
-
if (mt
|
|
2086
|
+
if (mt === null) return { stale: true, reason: "config-missing", detail: cfg };
|
|
1697
2087
|
const cachedMt = cache.project.configFiles[cfg];
|
|
1698
|
-
if (cachedMt
|
|
2088
|
+
if (cachedMt === null || Math.abs(cachedMt - mt) > 1e-4) {
|
|
1699
2089
|
return { stale: true, reason: "config-updated", detail: cfg };
|
|
1700
2090
|
}
|
|
1701
2091
|
}
|
|
1702
2092
|
if (cache.project.lockfile?.path) {
|
|
1703
2093
|
const mt = statMtimeMs(cache.project.lockfile.path);
|
|
1704
|
-
if (mt
|
|
2094
|
+
if (mt === null) return { stale: true, reason: "lockfile-missing", detail: cache.project.lockfile.path };
|
|
1705
2095
|
if (Math.abs(cache.project.lockfile.mtimeMs - mt) > 1e-4) {
|
|
1706
2096
|
return { stale: true, reason: "lockfile-updated", detail: cache.project.lockfile.path };
|
|
1707
2097
|
}
|
|
1708
2098
|
}
|
|
1709
2099
|
for (const [file, cachedMt] of Object.entries(cache.inputs)) {
|
|
1710
2100
|
const mt = statMtimeMs(file);
|
|
1711
|
-
if (mt
|
|
2101
|
+
if (mt === null) return { stale: true, reason: "input-missing", detail: file };
|
|
1712
2102
|
if (Math.abs(cachedMt - mt) > 1e-4) return { stale: true, reason: "input-updated", detail: file };
|
|
1713
2103
|
}
|
|
1714
2104
|
return { stale: false, reason: "up-to-date" };
|
|
@@ -1717,7 +2107,7 @@ async function isStale(params) {
|
|
|
1717
2107
|
// src/compiler/cache/writeCache.ts
|
|
1718
2108
|
import fs3 from "fs";
|
|
1719
2109
|
import path3 from "path";
|
|
1720
|
-
import
|
|
2110
|
+
import ts11 from "typescript";
|
|
1721
2111
|
function statMtimeMs2(p) {
|
|
1722
2112
|
return fs3.statSync(p).mtimeMs;
|
|
1723
2113
|
}
|
|
@@ -1750,7 +2140,7 @@ function writeCache(params) {
|
|
|
1750
2140
|
generator: {
|
|
1751
2141
|
name: "adorn-api",
|
|
1752
2142
|
version: params.adornVersion,
|
|
1753
|
-
typescript:
|
|
2143
|
+
typescript: ts11.version
|
|
1754
2144
|
},
|
|
1755
2145
|
project: {
|
|
1756
2146
|
tsconfigPath: params.tsconfigAbs,
|
|
@@ -1763,7 +2153,7 @@ function writeCache(params) {
|
|
|
1763
2153
|
}
|
|
1764
2154
|
|
|
1765
2155
|
// src/cli.ts
|
|
1766
|
-
import
|
|
2156
|
+
import ts12 from "typescript";
|
|
1767
2157
|
import process from "process";
|
|
1768
2158
|
var ADORN_VERSION = "0.1.0";
|
|
1769
2159
|
function log(msg) {
|
|
@@ -1811,7 +2201,7 @@ async function buildCommand(args) {
|
|
|
1811
2201
|
outDir: outputDir,
|
|
1812
2202
|
project: projectPath,
|
|
1813
2203
|
adornVersion: ADORN_VERSION,
|
|
1814
|
-
typescriptVersion:
|
|
2204
|
+
typescriptVersion: ts12.version
|
|
1815
2205
|
});
|
|
1816
2206
|
if (!stale.stale) {
|
|
1817
2207
|
log("adorn-api: artifacts up-to-date");
|