UNPKG

@boostercloud/metadata-booster

Version:

Emits detailed metadata of your types. You can then get it in runtime to deal with schema-aware operation, like defining GraphQL schemas, ORM operations, etc.

158 lines (154 loc) 6.55 kB
"use strict"; /* eslint-disable @typescript-eslint/ban-ts-comment */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getClassInfo = getClassInfo; const ts_morph_1 = require("ts-morph"); function getClassInfo(classNode, checker) { if (!classNode.name) return; const node = (0, ts_morph_1.createWrappedNode)(classNode, { typeChecker: checker }).asKindOrThrow(ts_morph_1.SyntaxKind.ClassDeclaration); return { name: node.getNameOrThrow(), fields: getInstanceProperties(node).map((p) => ({ name: p.getName(), typeInfo: getTypeInfo(p.getType(), p) })), methods: node.getMethods().map((m) => ({ name: m.getName(), typeInfo: getTypeInfo(m.getReturnType(), m) })), }; } function getInstanceProperties(classDeclaration) { if (classDeclaration == undefined) { return []; } // Ensure to get the properties of the base classes too return [...getInstanceProperties(classDeclaration.getBaseClass()), ...classDeclaration.getInstanceProperties()]; } function hasQuestionTokenNode(node) { // @ts-ignore if (node && typeof node['hasQuestionToken'] === 'function') { // @ts-ignore return node === null || node === void 0 ? void 0 : node.hasQuestionToken(); } return false; } function getTypeInfo(tp, nd) { return go(tp, 0, nd); function go(type, depth, node) { var _a, _b, _c, _d; const { name, isNullable } = getTypeInfoNameSafe(type, node); const isGetAccessor = ts_morph_1.Node.isGetAccessorDeclaration(node); /* This metadata is used for DTOs, since some of the types introduced in newer versions of packages are recursive, without a depth limit this will go into an infinite loop. Eight levels should be enough for any DTO. */ if (8 < depth) { return { name, typeName: null, typeGroup: 'Other', isNullable, parameters: [], isGetAccessor, }; } const typeGroupTuples = [ [(t) => t.isString(), 'String'], [(t) => t.isNumber(), 'Number'], [(t) => t.isBoolean(), 'Boolean'], [(t) => t.isEnum(), 'Enum'], [(t) => t.isUnion(), 'Union'], [(t) => t.isIntersection(), 'Intersection'], [(t) => t.isClass(), 'Class'], [(t) => t.isInterface(), 'Interface'], [(t) => t.getAliasSymbol() != null, 'Type'], [isReadonlyArray, 'ReadonlyArray'], [(t) => t.isArray(), 'Array'], [(t) => t.getCallSignatures().length > 0, 'Function'], [(t) => t.isObject(), 'Object'], ]; type = type.getNonNullableType(); const typeInfo = { name, typeName: '', typeGroup: ((_a = typeGroupTuples.find(([fn]) => fn(type))) === null || _a === void 0 ? void 0 : _a[1]) || 'Other', isNullable, parameters: [], isGetAccessor, }; switch (typeInfo.typeGroup) { case 'Enum': typeInfo.parameters = type.getUnionTypes().map((t) => go(t, depth + 1)); break; case 'Union': typeInfo.parameters = type.getUnionTypes().map((t) => go(t, depth + 1, node)); break; case 'Intersection': typeInfo.parameters = type.getIntersectionTypes().map((t) => go(t, depth + 1, node)); break; default: typeInfo.parameters = type.getTypeArguments().map((a) => go(a, depth + 1, node)); } // typeName is used for referencing the type in the metadata switch (typeInfo.typeGroup) { case 'String': case 'Number': case 'Boolean': typeInfo.typeName = typeInfo.typeGroup; break; case 'Union': case 'Intersection': typeInfo.typeName = null; break; case 'Enum': case 'Class': case 'ReadonlyArray': case 'Array': // getSymbol() is used for complex types, in which cases getText() returns too much information (e.g. Map<User> instead of just Map) typeInfo.typeName = ((_b = type.getSymbol()) === null || _b === void 0 ? void 0 : _b.getName()) || ''; break; case 'Object': typeInfo.typeName = ((_c = type.getSymbol()) === null || _c === void 0 ? void 0 : _c.getName()) || ''; if (typeInfo.typeName === '__type') { // This happens for literal objects like `{ a: string, b: { c: string } }` typeInfo.typeName = 'Object'; } break; case 'Interface': case 'Type': case 'Function': case 'Other': if (type.isEnumLiteral()) { typeInfo.name = ((_d = type.getSymbol()) === null || _d === void 0 ? void 0 : _d.getName()) || ''; // e.g. "Small" } typeInfo.typeName = null; break; } if (typeInfo.typeName === '') { typeInfo.typeName = typeInfo.name; } if (typeInfo.typeName === '') throw new Error(` Could not extract typeName for type ${JSON.stringify(typeInfo)} This is probably a bug in the metadata extractor. More information ---------------- typeInfo: ${JSON.stringify(typeInfo)} type: ${JSON.stringify(type.getText())} node: ${JSON.stringify(node === null || node === void 0 ? void 0 : node.getText())} depth: ${depth} `); return typeInfo; } function getTypeInfoNameSafe(type, node) { const isNullable = type.isNullable() || hasQuestionTokenNode(node); // node is passed for better name printing: https://github.com/dsherret/ts-morph/issues/907 const name = isNullable ? // Since the update of packages of May, 4th 2023, this is adding "undefined" and/or "null" to nullables. type.getText(node).replace(' | undefined', '').replace(' | null', '') : type.getText(node); return { name, isNullable }; } function isReadonlyArray(t) { var _a; return t.isObject() && (((_a = t.getSymbol()) === null || _a === void 0 ? void 0 : _a.getName()) || '') === 'ReadonlyArray'; } }