UNPKG

ts-transformer-fastest-validator

Version:

TypeScript transformer converting types to fastest-validator schema.

525 lines (524 loc) 20.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ts = require("typescript"); const predefined = { ICurrency: 'currency', IDate: 'date', IEmail: 'email', IForbidden: 'forbidden', IMac: 'mac', IUrl: 'url', IUUID: 'uuid', IObjectID: 'objectID', // https://github.com/icebob/fastest-validator#objectid }; /** * Transform logic */ function transformer(program) { return (context) => (file) => visitEach(file, program, context); } exports.default = transformer; ; function visitEach(node, program, context) { return ts.visitEachChild(visitNode(node, program, context), (childNode) => visitEach(childNode, program, context), context); } function visitNode(node, program, context) { var _a; // Skip if node is not function call expression if (!ts.isCallExpression(node)) { return node; } const typeChecker = program.getTypeChecker(); const signature = typeChecker.getResolvedSignature(node); const declaration = signature === null || signature === void 0 ? void 0 : signature.declaration; if (declaration && ts.isFunctionDeclaration(declaration)) { const name = (_a = declaration.name) === null || _a === void 0 ? void 0 : _a.getText(); // If function name is schema and has first type argument process transformation if (name === 'convertToSchema' && node.typeArguments && node.typeArguments[0]) { const type = typeChecker.getTypeFromTypeNode(node.typeArguments[0]); return convert(type, typeChecker, node, context.factory, new Set()); } } return node; } /** * Convert evaluates type and uses proper function based on the type */ function convert(type, typeChecker, node, factory, history) { var _a, _b, _c; let result; const flags = type.flags; const name = (_a = type.symbol) === null || _a === void 0 ? void 0 : _a.name; if (flags === ts.TypeFlags.Never || flags === ts.TypeFlags.Undefined || flags === ts.TypeFlags.Null) { // Convert to never like null, undefined result = convertNever(type, typeChecker, node, factory, history); } else if (flags & ts.TypeFlags.Literal) { // Convert literals like true/false/1/20/'example' result = convertLiteral(type, typeChecker, node, factory, history); } else if (name && predefined.hasOwnProperty(name)) { // Convert predefined result = convertPredefined(type, typeChecker, node, factory, history); } else if (name && name === 'Buffer') { // Convert buffer result = convertBuffer(type, typeChecker, node, factory, history); } else if (flags & ts.TypeFlags.EnumLike) { // Convert Enum result = convertEnum(type, typeChecker, node, factory, history); } else if (flags & ts.TypeFlags.StringLike || flags & ts.TypeFlags.NumberLike || flags & ts.TypeFlags.BooleanLike) { // Convert primitive types like number/string/boolean result = convertPrimitive(type, typeChecker, node, factory, history); } else if (flags === ts.TypeFlags.Any || flags & ts.TypeFlags.VoidLike) { // Convert Any result = convertAny(type, typeChecker, node, factory, history); } else if (flags === ts.TypeFlags.Object && typeChecker.isArrayType(type)) { // Convert array result = convertArray(type, typeChecker, node, factory, history); } else if (flags === ts.TypeFlags.Object) { // Convert objects like interface/type result = convertObject(type, typeChecker, node, factory, history); } else if (flags === ts.TypeFlags.Union) { // Convert union like Type1 | Type2 result = convertUnion(type, typeChecker, node, factory, history); } else if (flags === ts.TypeFlags.Intersection) { // Convert objects like interface/type result = convertIntersection(type, typeChecker, node, factory, history); } else { throw Error('Unknown type'); } // Apply annotations const annotations = [ ...(((_b = type.symbol) === null || _b === void 0 ? void 0 : _b.getJsDocTags()) || []), ...(((_c = type.aliasSymbol) === null || _c === void 0 ? void 0 : _c.getJsDocTags()) || []) ]; if (annotations.length) { return applyJSDoc(annotations, result, typeChecker, factory); } return result; } /** * Literal types are converted to https://github.com/icebob/fastest-validator#equal */ function convertLiteral(type, typeChecker, node, factory, history) { const properties = []; const literalValue = parseLiteral(type, typeChecker, node, factory); properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('equal'))); properties.push(factory.createPropertyAssignment('value', literalValue)); properties.push(factory.createPropertyAssignment('strict', factory.createTrue())); if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); } return factory.createObjectLiteralExpression(properties); } /** * Predefineds interfaces could be found in 'predefined' constant */ function convertPredefined(type, typeChecker, node, factory, history) { const name = type.symbol.name; const properties = []; properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral(predefined[name]))); if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); } return factory.createObjectLiteralExpression(properties); } /** * Instance of class Buffer */ function convertBuffer(type, typeChecker, node, factory, history) { const properties = []; properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('class'))); properties.push(factory.createPropertyAssignment('instanceOf', factory.createIdentifier('Buffer'))); if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); } return factory.createObjectLiteralExpression(properties); } /** * Primities could be converted to one of: * https://github.com/icebob/fastest-validator#boolean * https://github.com/icebob/fastest-validator#number * https://github.com/icebob/fastest-validator#string */ function convertPrimitive(type, typeChecker, node, factory, history) { const properties = []; const type_string = typeChecker.typeToString(type); properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral(type_string))); if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); } return factory.createObjectLiteralExpression(properties); } /** * Any is converted to https://github.com/icebob/fastest-validator#any */ function convertAny(type, typeChecker, node, factory, history) { const properties = []; properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('any'))); if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); } return factory.createObjectLiteralExpression(properties); } /** * Any is converted to https://github.com/icebob/fastest-validator#forbidden */ function convertNever(type, typeChecker, node, factory, history) { const properties = []; properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('forbidden'))); if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); } return factory.createObjectLiteralExpression(properties); } /** * Array is convereted to https://github.com/icebob/fastest-validator#array */ function convertArray(type, typeChecker, node, factory, history) { const properties = []; const types = type.typeArguments; // Creating annonimous history for case when multi is root if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); history.add(undefined); } if (types && types.length && !(types[0].flags & ts.TypeFlags.Any)) { // Convert regular arrays properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('array'))); properties.push(factory.createPropertyAssignment('items', convert(types[0], typeChecker, node, factory, history))); } else { // Convert in case of Array<any>, any[] properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('array'))); } return factory.createObjectLiteralExpression(properties); } /** * Object is converted to https://github.com/icebob/fastest-validator#object */ function convertObject(type, typeChecker, node, factory, history) { const size = history.size; const name = type.symbol.name; if (name && name !== '__type' && history.has(name)) { return factory.createObjectLiteralExpression([ factory.createPropertyAssignment('type', factory.createStringLiteral('any')) ]); } history.add(name); const props = typeChecker.getPropertiesOfType(type) .filter((property) => { const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, node); return !!propertyType; }) .map((property) => { const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, node); let resolvedType = convert(propertyType, typeChecker, node, factory, history); // Apply annotations const annotations = property.getJsDocTags() || []; if (annotations.length) { resolvedType = applyJSDoc(annotations, resolvedType, typeChecker, factory); } // Apply optional via questionToken if (property.declarations && property.declarations[0] && property.declarations[0].questionToken) { resolvedType = applyOptional(resolvedType, factory); } return factory.createPropertyAssignment(property.name, resolvedType); }); history.delete(name); if (size === 0) { return factory.createObjectLiteralExpression(props); } else { const properties = []; properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('object'))); if (props.length) { properties.push(factory.createPropertyAssignment('props', factory.createObjectLiteralExpression(props))); } return factory.createObjectLiteralExpression(properties); } } /** * Enum is converted to https://github.com/icebob/fastest-validator#enum */ function convertEnum(type, typeChecker, node, factory, history) { const unionType = type; const types = unionType.types || []; const properties = []; properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('enum'))); properties.push(factory.createPropertyAssignment('values', factory.createArrayLiteralExpression(types.filter((type) => !(type.flags & ts.TypeFlags.Undefined)) .map((type) => parseLiteral(type, typeChecker, node, factory))))); if (history.size === 0) { properties.push(factory.createPropertyAssignment('$$root', factory.createTrue())); } return factory.createObjectLiteralExpression(properties); } /** * Union is convereted to https://github.com/icebob/fastest-validator#multi */ function convertUnion(type, typeChecker, node, factory, history) { const unionType = type; // Ignoring undefined as a type as it represents optional type let optional = false; let nullable = false; const types = unionType.types.filter((type) => { if (type.flags & ts.TypeFlags.Undefined) { optional = true; return false; } if (type.flags & ts.TypeFlags.Null) { nullable = true; return false; } return true; }); // Optimization if all union members are literals create an enum let allLiterals = true; types.forEach((type) => { const literal = type.flags & ts.TypeFlags.Literal; allLiterals = allLiterals && !!literal; }); if (allLiterals) { let result = (types.length === 1) ? convertLiteral(types[0], typeChecker, node, factory, history) : convertEnum(type, typeChecker, node, factory, history); if (optional) { result = applyOptional(result, factory); } if (nullable) { result = applyNullable(result, factory); } return result; } // Creating annonimous history for case when multi is root history.add(undefined); // In case of optional or nullable it is possible to stay with only one element if (types.length === 1) { let result = convert(types[0], typeChecker, node, factory, history); if (optional) { result = applyOptional(result, factory); } if (nullable) { result = applyNullable(result, factory); } return result; } else { return factory.createArrayLiteralExpression(types .map((type) => { let result = convert(type, typeChecker, node, factory, history); if (optional) { result = applyOptional(result, factory); } if (nullable) { result = applyNullable(result, factory); } return result; })); } } /** * Intersection is converted to https://github.com/icebob/fastest-validator#object */ function convertIntersection(type, typeChecker, node, factory, history) { const size = history.size; const unionType = type; const types = unionType.types || []; const propsRegistry = new Map(); // First, collect all properties from all intersected types types.forEach((type) => { var _a; if (type.flags & ts.TypeFlags.StringLike || type.flags & ts.TypeFlags.NumberLike || type.flags & ts.TypeFlags.BooleanLike || type.flags & ts.TypeFlags.Literal) { throw new Error('Can\'t intersect literal or primitive!'); } const name = (_a = type.symbol) === null || _a === void 0 ? void 0 : _a.name; history.add(name); typeChecker.getPropertiesOfType(type) .filter((property) => { const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, node); return !!propertyType; }) .forEach((property) => { // Collect properties for each property if (!propsRegistry.has(property.name)) { propsRegistry.set(property.name, []); } propsRegistry.get(property.name).push(property); }); }); // Now process all collected properties const props = Array.from(propsRegistry.entries()).map(([name, properties]) => { let resolvedType; if (properties.length === 1) { // Single property - convert normally const propertyType = typeChecker.getTypeOfSymbolAtLocation(properties[0], node); resolvedType = convert(propertyType, typeChecker, node, factory, history); } else { // Multiple properties - create intersection between types of property const propType = typeChecker.getPropertyOfType(type, name); resolvedType = convert(propType.type, typeChecker, node, factory, history); } // Apply any property-level JSDoc annotations if needed const annotations = properties.reduce((result, property) => { const docs = property.getJsDocTags() || []; return result.concat(...docs); }, []); if (annotations.length) { resolvedType = applyJSDoc(annotations, resolvedType, typeChecker, factory); } // Count how many have optional tag and if all do apply optional const optionalCount = properties.reduce((result, property) => { if (property.declarations && property.declarations[0] && property.declarations[0].questionToken) { return result + 1; } return result; }, 0); if (properties.length === optionalCount) { resolvedType = applyOptional(resolvedType, factory); } history.delete(name); return factory.createPropertyAssignment(name, resolvedType); }); if (size === 0) { return factory.createObjectLiteralExpression(props); } else { const properties = []; properties.push(factory.createPropertyAssignment('type', factory.createStringLiteral('object'))); properties.push(factory.createPropertyAssignment('props', factory.createObjectLiteralExpression(props))); return factory.createObjectLiteralExpression(properties); } } /** * Adds optional property to an object or an array of objects */ function applyOptional(type, factory) { if (ts.isObjectLiteralExpression(type)) { const exists = type.properties.find(one => { const name = one.name; return (name === null || name === void 0 ? void 0 : name.escapedText) === 'optional'; }); if (exists) { return type; } return factory.updateObjectLiteralExpression(type, [ ...type.properties, factory.createPropertyAssignment('optional', factory.createTrue()) ]); } else if (ts.isArrayLiteralExpression(type)) { return factory.updateArrayLiteralExpression(type, type.elements.map((element) => { return applyOptional(element, factory); })); } return type; } /** * Adds nullable property to an object or an array of objects */ function applyNullable(type, factory) { if (ts.isObjectLiteralExpression(type)) { const exists = type.properties.find(one => { const name = one.name; return (name === null || name === void 0 ? void 0 : name.escapedText) === 'nullable'; }); if (exists) { return type; } return factory.updateObjectLiteralExpression(type, [ ...type.properties, factory.createPropertyAssignment('nullable', factory.createTrue()) ]); } else if (ts.isArrayLiteralExpression(type)) { return factory.updateArrayLiteralExpression(type, type.elements.map((element) => { return applyOptional(element, factory); })); } return type; } /** * Adds annotations as properties to an object or an array of objects */ function applyJSDoc(annotations, type, typeChecker, factory) { const properties = annotations.filter(annotation => annotation.text) .map(annotation => { const text = annotation.text; const value = Array.isArray(text) ? text[0].text : text; let literalValue; if (value === 'true') { literalValue = factory.createTrue(); } else if (value === 'false') { literalValue = factory.createFalse(); } else if (+value) { literalValue = factory.createNumericLiteral(+value); } else { literalValue = factory.createStringLiteral(value); } return factory.createPropertyAssignment(annotation.name, literalValue); }); if (ts.isObjectLiteralExpression(type)) { return factory.updateObjectLiteralExpression(type, [ ...type.properties, ...properties ]); } else if (ts.isArrayLiteralExpression(type)) { return factory.updateArrayLiteralExpression(type, type.elements.map((element) => { return applyJSDoc(annotations, element, typeChecker, factory); })); } return type; } /** * Parsing means making elements exactly like they should be, without coverting */ function parseLiteralBoolean(type, typeChecker, node, factory) { const type_string = typeChecker.typeToString(type); if (type_string === 'true') { return factory.createTrue(); } else { return factory.createFalse(); } } function parseLiteral(type, typeChecker, node, factory) { if (type.flags & ts.TypeFlags.BooleanLike) { return parseLiteralBoolean(type, typeChecker, node, factory); } const literalType = type; const value = literalType.value; switch (typeof value) { case 'string': return factory.createStringLiteral(type.value.toString()); case 'number': return factory.createNumericLiteral(+type.value); default: throw new Error('Unknow literal type ' + value); } }