Skip to content

Commit

Permalink
feat: support v10
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Jun 16, 2023
1 parent 0e9f187 commit 5db5edf
Show file tree
Hide file tree
Showing 25 changed files with 1,000 additions and 213 deletions.
70 changes: 36 additions & 34 deletions e2e/validate-schema.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,48 +49,50 @@ describe('Validate OpenAPI schema', () => {
});

it('should produce a valid OpenAPI 3.0 schema', async () => {
SwaggerModule.loadPluginMetadata({
metadata: {
'@nestjs/swagger': {
models: [
[
require('./src/cats/classes/cat.class'),
{
Cat: {
tags: {
description: 'Tags of the cat',
example: ['tag1', 'tag2']
}
await SwaggerModule.loadPluginMetadata(async () => ({
'@nestjs/swagger': {
models: [
[
import('./src/cats/classes/cat.class'),
{
Cat: {
tags: {
description: 'Tags of the cat',
example: ['tag1', 'tag2']
}
}
],
[
require('./src/cats/dto/create-cat.dto'),
{
CreateCatDto: {
name: {
description: 'Name of the cat'
}
}
],
[
import('./src/cats/dto/create-cat.dto'),
{
CreateCatDto: {
name: {
description: 'Name of the cat'
}
}
]
],
controllers: [
[
require('./src/cats/cats.controller'),
{
CatsController: {
findAllBulk: {
type: [require('./src/cats/classes/cat.class').Cat],
summary: 'Find all cats in bulk'
}
}
]
],
controllers: [
[
import('./src/cats/cats.controller'),
{
CatsController: {
findAllBulk: {
type: [
await import('./src/cats/classes/cat.class').then(
(f) => f.Cat
)
],
summary: 'Find all cats in bulk'
}
}
]
}
]
}
]
}
});
}));
const document = SwaggerModule.createDocument(app, options);

const doc = JSON.stringify(document, null, 2);
Expand Down
102 changes: 74 additions & 28 deletions lib/plugin/utils/plugin-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,56 +33,71 @@ export function getDecoratorOrUndefinedByNames(

export function getTypeReferenceAsString(
type: ts.Type,
typeChecker: ts.TypeChecker
): string {
typeChecker: ts.TypeChecker,
arrayDepth = 0
): {
typeName: string;
isArray?: boolean;
arrayDepth?: number;
} {
if (isArray(type)) {
const arrayType = getTypeArguments(type)[0];
const elementType = getTypeReferenceAsString(arrayType, typeChecker);
if (!elementType) {
return undefined;
const { typeName, arrayDepth: depth } = getTypeReferenceAsString(
arrayType,
typeChecker,
arrayDepth + 1
);
if (!typeName) {
return { typeName: undefined };
}
return `[${elementType}]`;
return {
typeName: `${typeName}`,
isArray: true,
arrayDepth: depth
};
}
if (isBoolean(type)) {
return Boolean.name;
return { typeName: Boolean.name, arrayDepth };
}
if (isNumber(type)) {
return Number.name;
return { typeName: Number.name, arrayDepth };
}
if (isBigInt(type)) {
return BigInt.name;
return { typeName: BigInt.name, arrayDepth };
}
if (isString(type) || isStringLiteral(type)) {
return String.name;
return { typeName: String.name, arrayDepth };
}
if (isPromiseOrObservable(getText(type, typeChecker))) {
const typeArguments = getTypeArguments(type);
const elementType = getTypeReferenceAsString(
head(typeArguments),
typeChecker
typeChecker,
arrayDepth
);
if (!elementType) {
return undefined;
}
return elementType;
}
if (type.isClass()) {
return getText(type, typeChecker);
return { typeName: getText(type, typeChecker), arrayDepth };
}
try {
const text = getText(type, typeChecker);
if (text === Date.name) {
return text;
return { typeName: text, arrayDepth };
}
if (isOptionalBoolean(text)) {
return Boolean.name;
return { typeName: Boolean.name, arrayDepth };
}
if (
isAutoGeneratedTypeUnion(type) ||
isAutoGeneratedEnumUnion(type, typeChecker)
) {
const types = (type as ts.UnionOrIntersectionType).types;
return getTypeReferenceAsString(types[types.length - 1], typeChecker);
return getTypeReferenceAsString(
types[types.length - 1],
typeChecker,
arrayDepth
);
}
if (
text === 'any' ||
Expand All @@ -91,17 +106,17 @@ export function getTypeReferenceAsString(
isInterface(type) ||
(type.isUnionOrIntersection() && !isEnum(type))
) {
return 'Object';
return { typeName: 'Object', arrayDepth };
}
if (isEnum(type)) {
return undefined;
return { typeName: undefined, arrayDepth };
}
if (type.aliasSymbol) {
return 'Object';
return { typeName: 'Object', arrayDepth };
}
return undefined;
return { typeName: undefined };
} catch {
return undefined;
return { typeName: undefined };
}
}

Expand All @@ -124,11 +139,11 @@ export function replaceImportPath(
options: PluginOptions
) {
if (!typeReference.includes('import')) {
return typeReference;
return { typeReference, importPath: null };
}
let importPath = /\(\"([^)]).+(\")/.exec(typeReference)[0];
if (!importPath) {
return undefined;
return { typeReference: undefined, importPath: null };
}
importPath = convertPath(importPath);
importPath = importPath.slice(2, importPath.length - 1);
Expand All @@ -138,11 +153,16 @@ export function replaceImportPath(
throw {};
}
require.resolve(importPath);
return typeReference.replace('import', 'require');
typeReference = typeReference.replace('import', 'require');
return {
typeReference,
importPath: null
};
} catch (_error) {
const from = options?.readonly
? options.pathToSource
: posix.dirname(fileName);

let relativePath = posix.relative(from, importPath);
relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;

Expand All @@ -169,16 +189,42 @@ export function replaceImportPath(
}

typeReference = typeReference.replace(importPath, relativePath);
return typeReference.replace('import', 'require');

return {
typeReference: options.readonly
? convertToAsyncImport(typeReference)
: typeReference.replace('import', 'require'),
importPath: relativePath
};
}
}

function convertToAsyncImport(typeReference: string) {
const regexp = /import\(.+\).([^\]]+)(\])?/;
const match = regexp.exec(typeReference);

if (match?.length >= 2) {
const importPos = typeReference.indexOf(match[0]);
typeReference = typeReference.replace(
match[1],
`then((f) => f.${match[1]})`
);
return insertAt(typeReference, importPos, 'await ');
}

return typeReference;
}

export function insertAt(string: string, index: number, substring: string) {
return string.slice(0, index) + substring + string.slice(index);
}

export function isDynamicallyAdded(identifier: ts.Node) {
return identifier && !identifier.parent && identifier.pos === -1;
}

/**
* when "strict" mode enabled, TypeScript transform the enum type to a union composed of
* When "strict" mode enabled, TypeScript transform the enum type to a union composed of
* the enum values and the undefined type. Hence, we have to lookup all the union types to get the original type
* @param type
* @param typeChecker
Expand Down
52 changes: 45 additions & 7 deletions lib/plugin/visitors/controller-class.visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
string,
Record<string, ClassMetadata>
> = {};
private readonly _typeImports: Record<string, string> = {};

get typeImports() {
return this._typeImports;
}

get collectedMetadata(): Array<
[ts.CallExpression, Record<string, ClassMetadata>]
Expand Down Expand Up @@ -340,18 +345,21 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
if (!type) {
return undefined;
}
let typeReference = getTypeReferenceAsString(type, typeChecker);
if (!typeReference) {
const { typeName, isArray } = getTypeReferenceAsString(type, typeChecker);
if (!typeName) {
return undefined;
}
if (typeReference.includes('node_modules')) {
if (typeName.includes('node_modules')) {
return undefined;
}
typeReference = replaceImportPath(typeReference, hostFilename, options);
return factory.createPropertyAssignment(
'type',
factory.createIdentifier(typeReference)
const identifier = this.typeReferenceStringToIdentifier(
typeName,
isArray,
hostFilename,
options,
factory
);
return factory.createPropertyAssignment('type', identifier);
}

createStatusPropertyAssignment(
Expand Down Expand Up @@ -395,4 +403,34 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;
return relativePath;
}

private typeReferenceStringToIdentifier(
_typeReference: string,
isArray: boolean,
hostFilename: string,
options: PluginOptions,
factory: ts.NodeFactory
) {
const { typeReference, importPath } = replaceImportPath(
_typeReference,
hostFilename,
options
);

let identifier: ts.Identifier;
if (options.readonly && typeReference?.includes('import')) {
if (!this._typeImports[importPath]) {
this._typeImports[importPath] = typeReference;
}

identifier = factory.createIdentifier(
isArray ? `[t["${importPath}"]]` : `t["${importPath}"]`
);
} else {
identifier = factory.createIdentifier(
isArray ? `[${typeReference}]` : typeReference
);
}
return identifier;
}
}
Loading

0 comments on commit 5db5edf

Please sign in to comment.