@nestjs/graphql
Version:
Nest - modern, fast, powerful node.js web framework (@graphql)
245 lines (244 loc) • 12.1 kB
JavaScript
import { posix } from 'path';
import * as ts from 'typescript';
import { ArgsType, Field, HideField, InputType, InterfaceType, ObjectType, } from '../../decorators/index.js';
import { METADATA_FACTORY_NAME } from '../plugin-constants.js';
import { pluginDebugLogger } from '../plugin-debug-logger.js';
import { createImportEquals, findNullableTypeFromUnion, getDecoratorName, getDecorators, getJSDocDescription, getJsDocDeprecation, getModifiers, hasDecorators, hasModifiers, isNull, isUndefined, safelyMergeObjects, serializePrimitiveObjectToAst, updateDecoratorArguments, } from '../utils/ast-utils.js';
import { convertPath, getTypeReferenceAsString, } from '../utils/plugin-utils.js';
import { typeReferenceToIdentifier } from '../utils/type-reference-to-identifier.util.js';
const CLASS_DECORATORS = [
ObjectType.name,
InterfaceType.name,
InputType.name,
ArgsType.name,
];
export class ModelClassVisitor {
constructor() {
this._typeImports = {};
this._collectedMetadata = {};
}
get typeImports() {
return this._typeImports;
}
get collectedMetadata() {
const metadataWithImports = [];
Object.keys(this._collectedMetadata).forEach((filePath) => {
const metadata = this._collectedMetadata[filePath];
const path = filePath.replace(/\.[jt]s$/, '');
const importExpr = ts.factory.createCallExpression(ts.factory.createToken(ts.SyntaxKind.ImportKeyword), undefined, [ts.factory.createStringLiteral(path)]);
metadataWithImports.push([importExpr, metadata]);
});
return metadataWithImports;
}
visit(sourceFile, ctx, program, pluginOptions) {
this.importsToAdd = new Set();
const typeChecker = program.getTypeChecker();
const factory = ctx.factory;
const visitNode = (node) => {
const decorators = getDecorators(node);
if (ts.isClassDeclaration(node) &&
hasDecorators(decorators, CLASS_DECORATORS)) {
const isExported = node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
if (pluginOptions.readonly && !isExported) {
if (pluginOptions.debug) {
pluginDebugLogger.debug(`Skipping class "${node.name.getText()}" because it's not exported.`);
}
return;
}
const [members, amendedMetadata] = this.amendFieldsDecorators(factory, node.members, pluginOptions, sourceFile.fileName, typeChecker);
const metadata = this.collectMetadataFromClassMembers(factory, members, pluginOptions, sourceFile.fileName, typeChecker);
if (!pluginOptions.readonly) {
return this.updateClassDeclaration(factory, node, members, metadata, pluginOptions);
}
else {
const filePath = this.normalizeImportPath(pluginOptions.pathToSource, sourceFile.fileName);
if (!this._collectedMetadata[filePath]) {
this._collectedMetadata[filePath] = {};
}
const attributeKey = node.name.getText();
this._collectedMetadata[filePath][attributeKey] = safelyMergeObjects(factory, metadata, amendedMetadata);
return;
}
}
else if (ts.isSourceFile(node) && !pluginOptions.readonly) {
const visitedNode = ts.visitEachChild(node, visitNode, ctx);
const importStatements = this.createEagerImports(factory);
const existingStatements = Array.from(visitedNode.statements);
return factory.updateSourceFile(visitedNode, [
...importStatements,
...existingStatements,
]);
}
if (pluginOptions.readonly) {
ts.forEachChild(node, visitNode);
}
else {
return ts.visitEachChild(node, visitNode, ctx);
}
};
return ts.visitNode(sourceFile, visitNode);
}
addDescriptionToClassDecorators(f, node) {
const description = getJSDocDescription(node);
const decorators = getDecorators(node);
if (!description) {
return decorators;
}
// get one of allowed decorators from list
return decorators.map((decorator) => {
if (!CLASS_DECORATORS.includes(getDecoratorName(decorator))) {
return decorator;
}
const decoratorExpression = decorator.expression;
const objectLiteralExpression = serializePrimitiveObjectToAst(f, {
description,
});
let newArgumentsArray = [];
if (decoratorExpression.arguments.length === 0) {
newArgumentsArray = [objectLiteralExpression];
}
else {
// Options always a last parameter:
// @ObjectType('name', {description: ''});
// @ObjectType({description: ''});
newArgumentsArray = decoratorExpression.arguments.map((argument, index) => {
if (index + 1 != decoratorExpression.arguments.length) {
return argument;
}
// merge existing props with new props
return safelyMergeObjects(f, objectLiteralExpression, argument);
});
}
return f.updateDecorator(decorator, f.updateCallExpression(decoratorExpression, decoratorExpression.expression, decoratorExpression.typeArguments, newArgumentsArray));
});
}
amendFieldsDecorators(f, members, pluginOptions, hostFilename, // sourceFile.fileName,
typeChecker) {
const propertyAssignments = [];
const updatedClassElements = members.map((member) => {
const decorators = getDecorators(member);
if ((ts.isPropertyDeclaration(member) || ts.isGetAccessor(member)) &&
hasDecorators(decorators, [Field.name])) {
try {
return updateDecoratorArguments(f, member, Field.name, (decoratorArguments) => {
const options = this.getOptionsFromFieldDecoratorOrUndefined(decoratorArguments);
const { type, ...metadata } = this.createFieldMetadata(f, member, typeChecker, hostFilename, pluginOptions, this.getTypeFromFieldDecoratorOrUndefined(decoratorArguments));
const serializedMetadata = serializePrimitiveObjectToAst(f, metadata);
propertyAssignments.push(f.createPropertyAssignment(f.createIdentifier(member.name.getText()), serializedMetadata));
return [
type,
options
? safelyMergeObjects(f, serializedMetadata, options)
: serializedMetadata,
];
});
}
catch (e) {
// omit error
}
}
return member;
});
return [
updatedClassElements,
f.createObjectLiteralExpression(propertyAssignments),
];
}
collectMetadataFromClassMembers(f, members, pluginOptions, hostFilename, // sourceFile.fileName,
typeChecker) {
const properties = [];
members.forEach((member) => {
const decorators = getDecorators(member);
const modifiers = getModifiers(member);
if ((ts.isPropertyDeclaration(member) || ts.isGetAccessor(member)) &&
!hasModifiers(modifiers, [
ts.SyntaxKind.StaticKeyword,
ts.SyntaxKind.PrivateKeyword,
]) &&
!hasDecorators(decorators, [HideField.name, Field.name])) {
try {
const metadata = this.createFieldMetadata(f, member, typeChecker, hostFilename, pluginOptions);
properties.push(f.createPropertyAssignment(f.createIdentifier(member.name.getText()), serializePrimitiveObjectToAst(f, metadata)));
}
catch (e) {
// omit error
}
}
});
return f.createObjectLiteralExpression(properties);
}
updateClassDeclaration(f, node, members, propsMetadata, pluginOptions) {
const method = f.createMethodDeclaration([f.createModifier(ts.SyntaxKind.StaticKeyword)], undefined, f.createIdentifier(METADATA_FACTORY_NAME), undefined, undefined, [], undefined, f.createBlock([f.createReturnStatement(propsMetadata)], true));
const decorators = pluginOptions.introspectComments
? this.addDescriptionToClassDecorators(f, node)
: getDecorators(node);
return f.updateClassDeclaration(node, [...decorators, ...getModifiers(node)], node.name, node.typeParameters, node.heritageClauses, [...members, method]);
}
getOptionsFromFieldDecoratorOrUndefined(decoratorArguments) {
if (decoratorArguments.length > 1) {
return decoratorArguments[1];
}
if (decoratorArguments.length === 1 &&
!ts.isArrowFunction(decoratorArguments[0])) {
return decoratorArguments[0];
}
}
getTypeFromFieldDecoratorOrUndefined(decoratorArguments) {
if (decoratorArguments.length > 0 &&
ts.isArrowFunction(decoratorArguments[0])) {
return decoratorArguments[0];
}
}
createFieldMetadata(f, node, typeChecker, hostFilename = '', pluginOptions = {}, typeArrowFunction) {
const type = typeChecker.getTypeAtLocation(node);
const isNullable = !!node.questionToken || isNull(type) || isUndefined(type);
if (!typeArrowFunction) {
typeArrowFunction =
typeArrowFunction ||
f.createArrowFunction(undefined, undefined, [], undefined, undefined, this.getTypeUsingTypeChecker(f, node.type, typeChecker, hostFilename, pluginOptions));
}
const description = pluginOptions.introspectComments
? getJSDocDescription(node)
: undefined;
const deprecationReason = pluginOptions.introspectComments
? getJsDocDeprecation(node)
: undefined;
return {
nullable: isNullable || undefined,
type: typeArrowFunction,
description,
deprecationReason,
};
}
getTypeUsingTypeChecker(f, node, typeChecker, hostFilename, options) {
if (node && ts.isUnionTypeNode(node)) {
const nullableType = findNullableTypeFromUnion(node, typeChecker);
const remainingTypes = node.types.filter((item) => item !== nullableType);
if (remainingTypes.length === 1) {
return this.getTypeUsingTypeChecker(f, remainingTypes[0], typeChecker, hostFilename, options);
}
}
const type = typeChecker.getTypeAtLocation(node);
if (!type) {
return undefined;
}
const typeReferenceDescriptor = getTypeReferenceAsString(type, typeChecker);
if (!typeReferenceDescriptor.typeName) {
return undefined;
}
return typeReferenceToIdentifier(typeReferenceDescriptor, hostFilename, options, f, type, this._typeImports, this.importsToAdd);
}
createEagerImports(f) {
if (!this.importsToAdd.size) {
return [];
}
return Array.from(this.importsToAdd).map((path, index) => {
return createImportEquals(f, 'eager_import_' + index, path);
});
}
normalizeImportPath(pathToSource, path) {
let relativePath = posix.relative(convertPath(pathToSource), convertPath(path));
relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;
return relativePath;
}
}