UNPKG

ts-transformer-fastest-validator

Version:

TypeScript transformer converting types to fastest-validator schema.

657 lines (518 loc) 25.5 kB
import * as ts from 'typescript'; import { isArrayBufferView } from 'util/types'; const predefined: { [name: string]: string } = { ICurrency: 'currency', // https://github.com/icebob/fastest-validator#currency IDate: 'date', // https://github.com/icebob/fastest-validator#date IEmail: 'email', // https://github.com/icebob/fastest-validator#email IForbidden: 'forbidden',// https://github.com/icebob/fastest-validator#forbidden IMac: 'mac', // https://github.com/icebob/fastest-validator#mac IUrl: 'url', // https://github.com/icebob/fastest-validator#url IUUID: 'uuid', // https://github.com/icebob/fastest-validator#uuid IObjectID: 'objectID', // https://github.com/icebob/fastest-validator#objectid }; /** * Transform logic */ export default function transformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> { return (context: ts.TransformationContext) => (file: ts.SourceFile) => visitEach(file, program, context); }; function visitEach(node: ts.SourceFile, program: ts.Program, context: ts.TransformationContext): ts.SourceFile; function visitEach(node: ts.Node, program: ts.Program, context: ts.TransformationContext): ts.Node; function visitEach(node: ts.Node, program: ts.Program, context: ts.TransformationContext): ts.Node { return ts.visitEachChild(visitNode(node, program, context), (childNode) => visitEach(childNode, program, context), context); } function visitNode(node: ts.Node, program: ts.Program, context: ts.TransformationContext): ts.Node { // 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?.declaration; if(declaration && ts.isFunctionDeclaration(declaration)) { const name = declaration.name?.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<string>()); } } return node; } /** * Convert evaluates type and uses proper function based on the type */ function convert(type: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.PrimaryExpression { let result: ts.ArrayLiteralExpression | ts.ObjectLiteralExpression; const flags = type.flags; const name = (type as ts.ObjectType).symbol?.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 as any).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: ts.JSDocTagInfo[] = [ ...(type.symbol?.getJsDocTags() || []), ...(type.aliasSymbol?.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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { 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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { const name = (type as ts.ObjectType).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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { 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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { 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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { 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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { 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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { const properties = []; const types = (type as ts.TypeReference).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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { const size = history.size; const name = (type as ts.ObjectType).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: ts.Symbol) => { const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, node); return !!propertyType; }) .map((property: ts.Symbol) => { const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, node); let resolvedType = convert(propertyType, typeChecker, node, factory, history); // Apply annotations const annotations: ts.JSDocTagInfo[] = property.getJsDocTags() || []; if(annotations.length) { resolvedType = applyJSDoc(annotations, resolvedType as ts.ObjectLiteralExpression, typeChecker, factory) } // Apply optional via questionToken if(property.declarations && property.declarations[0] && (property.declarations[0] as ts.ParameterDeclaration).questionToken) { resolvedType = applyOptional(resolvedType as any, factory); } return factory.createPropertyAssignment(property.name, resolvedType); }); history.delete(name); if(size === 0) { return factory.createObjectLiteralExpression(props); } else { const properties: ts.PropertyAssignment[] = []; 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: ts.EnumType, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { const unionType = type as ts.UnionOrIntersectionType; 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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ArrayLiteralExpression | ts.ObjectLiteralExpression { const unionType = type as ts.UnionOrIntersectionType; // 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: any = (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: any = 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: any = 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: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory, history: Set<string | undefined>): ts.ObjectLiteralExpression { const size = history.size; const unionType = type as ts.UnionOrIntersectionType; const types = unionType.types || []; const propsRegistry = new Map<string, ts.Symbol[]>(); // First, collect all properties from all intersected types types.forEach((type) => { 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 = (type as ts.ObjectType).symbol?.name; history.add(name); typeChecker.getPropertiesOfType(type) .filter((property: ts.Symbol) => { 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: ts.PropertyAssignment[] = Array.from(propsRegistry.entries()).map(([name, properties]) => { let resolvedType: ts.PrimaryExpression; 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: any = typeChecker.getPropertyOfType(type, name); resolvedType = convert(propType.type, typeChecker, node, factory, history); } // Apply any property-level JSDoc annotations if needed const annotations: ts.JSDocTagInfo[] = properties.reduce((result: ts.JSDocTagInfo[], property) => { const docs = property.getJsDocTags() || []; return result.concat(...docs); }, []); if(annotations.length) { resolvedType = applyJSDoc(annotations, resolvedType as ts.ObjectLiteralExpression, 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] as ts.ParameterDeclaration).questionToken) { return result + 1; } return result; }, 0); if(properties.length === optionalCount) { resolvedType = applyOptional(resolvedType as any, factory); } history.delete(name); return factory.createPropertyAssignment(name, resolvedType); }); if(size === 0) { return factory.createObjectLiteralExpression(props); } else { const properties: ts.PropertyAssignment[] = []; 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: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression, factory: ts.NodeFactory): ts.ObjectLiteralExpression | ts.ArrayLiteralExpression { if(ts.isObjectLiteralExpression(type)) { const exists = type.properties.find(one => { const name = (one.name as ts.Identifier); return 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: any) => { return applyOptional(element, factory) })); } return type; } /** * Adds nullable property to an object or an array of objects */ function applyNullable(type: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression, factory: ts.NodeFactory): ts.ObjectLiteralExpression | ts.ArrayLiteralExpression { if(ts.isObjectLiteralExpression(type)) { const exists = type.properties.find(one => { const name = (one.name as ts.Identifier); return 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: any) => { return applyOptional(element, factory) })); } return type; } /** * Adds annotations as properties to an object or an array of objects */ function applyJSDoc(annotations: ts.JSDocTagInfo[], type: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression, typeChecker: ts.TypeChecker, factory: ts.NodeFactory): ts.ObjectLiteralExpression | ts.ArrayLiteralExpression { const properties: ts.PropertyAssignment[] = annotations.filter(annotation => annotation.text) .map(annotation => { const text = (annotation.text as unknown as any); const value = Array.isArray(text) ? text[0].text : text; let literalValue: ts.LiteralExpression | ts.TrueLiteral | ts.FalseLiteral; 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: any) => { return applyJSDoc(annotations, element, typeChecker, factory) })); } return type; } /** * Parsing means making elements exactly like they should be, without coverting */ function parseLiteralBoolean(type: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory): ts.StringLiteral | ts.NumericLiteral | ts.TrueLiteral | ts.FalseLiteral { const type_string = typeChecker.typeToString(type); if(type_string === 'true') { return factory.createTrue(); } else { return factory.createFalse(); } } function parseLiteral(type: ts.Type, typeChecker: ts.TypeChecker, node: ts.Node, factory: ts.NodeFactory): ts.StringLiteral | ts.NumericLiteral | ts.TrueLiteral | ts.FalseLiteral { if(type.flags & ts.TypeFlags.BooleanLike) { return parseLiteralBoolean(type, typeChecker, node, factory); } const literalType = type as ts.LiteralType; const value = literalType.value; switch (typeof value) { case 'string': return factory.createStringLiteral((type as any).value.toString()); case 'number': return factory.createNumericLiteral(+(type as any).value); default: throw new Error('Unknow literal type ' + value) } }