UNPKG

typescript-runtime-schemas

Version:

A TypeScript schema generation tool that extracts Zod schemas from TypeScript source files with runtime validation support. Generate validation schemas directly from your existing TypeScript types with support for computed types and constraint-based valid

399 lines (384 loc) 18.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const schema_extractor_1 = require("./schema-extractor"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); // Create test files for testing const testDir = path.join(__dirname, "../test-schema-files"); beforeAll(async () => { // Create test directory await fs.promises.mkdir(testDir, { recursive: true }); // Create test files with types that extend SupportsRuntimeValidation await fs.promises.writeFile(path.join(testDir, "user-types.ts"), ` import { SupportsRuntimeValidation, Min, Max, MinLength, MaxLength, Email } from './constraint-types'; export interface BaseUser { id: number; name: string; email: string; age?: number; } // This type extends SupportsRuntimeValidation - should be extracted export type ValidatedUser = BaseUser & SupportsRuntimeValidation & { id: number & Min<1>; name: string & MinLength<2> & MaxLength<50>; email: string & Email; age?: number & Min<13> & Max<120>; }; // This type does NOT extend SupportsRuntimeValidation - should be skipped export type UnvalidatedUser = BaseUser & { id: number; name: string; }; // This type uses Pick and extends SupportsRuntimeValidation export type UserProfile = Pick<ValidatedUser, 'name' | 'email'> & SupportsRuntimeValidation; // This type uses computed types export type PartialValidatedUser = Partial<ValidatedUser> & SupportsRuntimeValidation; `); await fs.promises.writeFile(path.join(testDir, "product-types.ts"), ` import { SupportsRuntimeValidation, Min, Max, MinLength, MaxLength } from './constraint-types'; export interface BaseProduct { id: number; title: string; price: number; description?: string; tags: string[]; } // Product with validation constraints export type ValidatedProduct = BaseProduct & SupportsRuntimeValidation & { id: number & Min<1>; title: string & MinLength<1> & MaxLength<100>; price: number & Min<0>; description?: string & MaxLength<500>; tags: string[] & MinLength<1> & MaxLength<10>; }; // Product without validation - should be skipped export type SimpleProduct = BaseProduct; `); await fs.promises.writeFile(path.join(testDir, "constraint-types.ts"), ` export type SupportsRuntimeValidation = {}; export type Constraint<K extends any> = {}; export type Min<N extends number> = Constraint<N>; export type Max<N extends number> = Constraint<N>; export type MinLength<N extends number> = Constraint<N>; export type MaxLength<N extends number> = Constraint<N>; export type Email = Constraint<string>; export type Regex<P extends string> = Constraint<P>; `); // Create a subdirectory with more types const subDir = path.join(testDir, "nested"); await fs.promises.mkdir(subDir, { recursive: true }); await fs.promises.writeFile(path.join(subDir, "order-types.ts"), ` import { SupportsRuntimeValidation, Min, MinLength } from '../constraint-types'; export type ValidatedOrder = { orderId: string & MinLength<5>; items: Array<{ productId: number & Min<1>; quantity: number & Min<1>; }> & MinLength<1>; totalAmount: number & Min<0>; } & SupportsRuntimeValidation; `); }); afterAll(async () => { // Clean up test files await fs.promises.rm(testDir, { recursive: true, force: true }); }); describe("SchemaExtractor Integration Tests", () => { describe("Source Code Extraction", () => { test("should extract schemas from source code with SupportsRuntimeValidation", async () => { const sourceCode = ` type SupportsRuntimeValidation = {}; type Constraint<K extends any> = {}; type Min<N extends number> = Constraint<N>; type Max<N extends number> = Constraint<N>; type Email = Constraint<string>; type ValidatedInput = { id: number & Min<1>; email: string & Email; age?: number & Max<120>; } & SupportsRuntimeValidation; type UnvalidatedInput = { id: number; name: string; }; `; const schemas = await (0, schema_extractor_1.extractSchemasFromSource)(sourceCode); expect(schemas).toHaveLength(1); expect(schemas[0].typeName).toBe("ValidatedInput"); expect(schemas[0].schema).toHaveProperty("id"); expect(schemas[0].schema).toHaveProperty("email"); expect(schemas[0].schema).toHaveProperty("age"); // Check constraints expect(schemas[0].schema.id.constraints).toHaveProperty("min", 1); expect(schemas[0].schema.email.constraints).toHaveProperty("email", true); expect(schemas[0].schema.age.constraints).toHaveProperty("max", 120); }); test("should handle computed types like Pick and Partial", async () => { const sourceCode = ` type SupportsRuntimeValidation = {}; type Constraint<K extends any> = {}; type Min<N extends number> = Constraint<N>; type Email = Constraint<string>; type BaseUser = { id: number & Min<1>; name: string; email: string & Email; age: number; }; type UserProfile = Pick<BaseUser, 'name' | 'email'> & SupportsRuntimeValidation; type PartialUser = Partial<BaseUser> & SupportsRuntimeValidation; `; const schemas = await (0, schema_extractor_1.extractSchemasFromSource)(sourceCode); expect(schemas).toHaveLength(2); // Check Pick type const userProfile = schemas.find((s) => s.typeName === "UserProfile"); expect(userProfile).toBeDefined(); expect(userProfile.schema).toHaveProperty("name"); expect(userProfile.schema).toHaveProperty("email"); expect(userProfile.schema).not.toHaveProperty("id"); expect(userProfile.schema).not.toHaveProperty("age"); // Check Partial type const partialUser = schemas.find((s) => s.typeName === "PartialUser"); expect(partialUser).toBeDefined(); expect(partialUser.schema.id.required).toBe(false); expect(partialUser.schema.name.required).toBe(false); }); }); describe("File-based Extraction", () => { test("should extract schemas from a single file", async () => { const filePath = path.join(testDir, "user-types.ts"); const schemas = await (0, schema_extractor_1.extractSchemasFromFile)(filePath); expect(schemas.length).toBeGreaterThan(0); const validatedUser = schemas.find((s) => s.typeName === "ValidatedUser"); expect(validatedUser).toBeDefined(); expect(validatedUser.schema).toHaveProperty("id"); expect(validatedUser.schema).toHaveProperty("name"); expect(validatedUser.schema).toHaveProperty("email"); // Should not include UnvalidatedUser (doesn't extend SupportsRuntimeValidation) const unvalidatedUser = schemas.find((s) => s.typeName === "UnvalidatedUser"); expect(unvalidatedUser).toBeUndefined(); }); test("should include source info when requested", async () => { const filePath = path.join(testDir, "user-types.ts"); const schemas = await (0, schema_extractor_1.extractSchemasFromFile)(filePath, { includeSourceInfo: true, }); expect(schemas.length).toBeGreaterThan(0); const validatedUser = schemas.find((s) => s.typeName === "ValidatedUser"); expect(validatedUser).toBeDefined(); expect(validatedUser.sourceInfo).toBeDefined(); expect(validatedUser.sourceInfo.filePath).toContain("user-types.ts"); expect(validatedUser.sourceInfo.line).toBeGreaterThan(0); }); }); describe("Directory-based Extraction", () => { test("should extract schemas from all files in directory", async () => { const schemas = await (0, schema_extractor_1.extractSchemasFromDirectory)(testDir); expect(schemas.length).toBeGreaterThan(0); // Should find types from multiple files const typeNames = schemas.map((s) => s.typeName); expect(typeNames).toContain("ValidatedUser"); expect(typeNames).toContain("ValidatedProduct"); expect(typeNames).toContain("UserProfile"); // Should not contain unvalidated types expect(typeNames).not.toContain("UnvalidatedUser"); expect(typeNames).not.toContain("SimpleProduct"); }); test("should handle recursive directory discovery", async () => { const schemas = await (0, schema_extractor_1.extractSchemasFromDirectory)(testDir, { recursive: true, }); const typeNames = schemas.map((s) => s.typeName); expect(typeNames).toContain("ValidatedOrder"); // From nested directory }); test("should handle non-recursive directory discovery", async () => { const schemas = await (0, schema_extractor_1.extractSchemasFromDirectory)(testDir, { recursive: false, }); const typeNames = schemas.map((s) => s.typeName); expect(typeNames).not.toContain("ValidatedOrder"); // Should not find nested types }); }); describe("Glob-based Extraction", () => { test("should extract schemas using glob patterns", async () => { const schemas = await (0, schema_extractor_1.extractSchemasFromGlob)(testDir, "**/*-types.ts", [ "**/constraint-types.ts", ]); const typeNames = schemas.map((s) => s.typeName); expect(typeNames).toContain("ValidatedUser"); expect(typeNames).toContain("ValidatedProduct"); // Should not contain constraint types (excluded) expect(typeNames).not.toContain("SupportsRuntimeValidation"); }); }); describe("Metadata and Statistics", () => { test("should provide extraction metadata", async () => { const filePath = path.join(testDir, "user-types.ts"); const result = await (0, schema_extractor_1.extractWithMetadata)(filePath); expect(result.totalTypesFound).toBeGreaterThan(0); expect(result.typesWithSupportsRuntimeValidation).toBeGreaterThan(0); expect(result.typesWithSupportsRuntimeValidation).toBeLessThanOrEqual(result.totalTypesFound); expect(result.processingTime).toBeGreaterThan(0); expect(result.schemas).toHaveLength(result.typesWithSupportsRuntimeValidation); }); test("should get validation types without extracting schemas", async () => { const filePath = path.join(testDir, "user-types.ts"); const validationTypes = await schema_extractor_1.SchemaExtractor.getValidationTypes(filePath); expect(validationTypes).toContain("ValidatedUser"); expect(validationTypes).toContain("UserProfile"); expect(validationTypes).not.toContain("UnvalidatedUser"); }); }); describe("Single Type Extraction", () => { test("should extract schema for a specific type", async () => { const filePath = path.join(testDir, "user-types.ts"); const schema = await schema_extractor_1.SchemaExtractor.extractSingleSchema(filePath, "ValidatedUser"); expect(schema).toBeDefined(); expect(schema.typeName).toBe("ValidatedUser"); expect(schema.schema).toHaveProperty("id"); expect(schema.schema).toHaveProperty("name"); }); test("should return null for non-existent type", async () => { const filePath = path.join(testDir, "user-types.ts"); const schema = await schema_extractor_1.SchemaExtractor.extractSingleSchema(filePath, "NonExistentType"); expect(schema).toBeNull(); }); test("should return null for type that doesn't extend SupportsRuntimeValidation", async () => { const filePath = path.join(testDir, "user-types.ts"); const schema = await schema_extractor_1.SchemaExtractor.extractSingleSchema(filePath, "UnvalidatedUser"); expect(schema).toBeNull(); }); }); describe("Complex Type Scenarios", () => { test("should handle nested objects with constraints", async () => { const sourceCode = ` type SupportsRuntimeValidation = {}; type Constraint<K extends any> = {}; type Min<N extends number> = Constraint<N>; type MinLength<N extends number> = Constraint<N>; type Email = Constraint<string>; type ValidatedCompany = { name: string & MinLength<1>; employees: Array<{ id: number & Min<1>; email: string & Email; }> & MinLength<1>; settings: { maxUsers: number & Min<1>; theme: string; }; } & SupportsRuntimeValidation; `; const schemas = await (0, schema_extractor_1.extractSchemasFromSource)(sourceCode); expect(schemas).toHaveLength(1); const schema = schemas[0].schema; expect(schema).toHaveProperty("name"); expect(schema).toHaveProperty("employees"); expect(schema).toHaveProperty("settings"); // Check array constraints expect(schema.employees.type).toBe("array"); expect(schema.employees.constraints).toHaveProperty("minLength", 1); // Check nested object expect(schema.settings.type).toBe("object"); expect(schema.settings.properties).toHaveProperty("maxUsers"); expect(schema.settings.properties).toHaveProperty("theme"); }); test("should handle intersection types with multiple constraints", async () => { const sourceCode = ` type SupportsRuntimeValidation = {}; type Constraint<K extends any> = {}; type Min<N extends number> = Constraint<N>; type Max<N extends number> = Constraint<N>; type MinLength<N extends number> = Constraint<N>; type MaxLength<N extends number> = Constraint<N>; type ValidatedString = { value: string & MinLength<5> & MaxLength<50>; count: number & Min<0> & Max<100>; } & SupportsRuntimeValidation; `; const schemas = await (0, schema_extractor_1.extractSchemasFromSource)(sourceCode); expect(schemas).toHaveLength(1); const schema = schemas[0].schema; // Check multiple constraints on string expect(schema.value.constraints).toHaveProperty("minLength", 5); expect(schema.value.constraints).toHaveProperty("maxLength", 50); // Check multiple constraints on number expect(schema.count.constraints).toHaveProperty("min", 0); expect(schema.count.constraints).toHaveProperty("max", 100); }); }); describe("Error Handling", () => { test("should handle files with syntax errors gracefully", async () => { const tempFile = path.join(testDir, "invalid-syntax.ts"); await fs.promises.writeFile(tempFile, ` type SupportsRuntimeValidation = {}; type InvalidType = { // Missing closing brace id: number; `); // Should not throw, but may return empty results const schemas = await (0, schema_extractor_1.extractSchemasFromFile)(tempFile); expect(Array.isArray(schemas)).toBe(true); await fs.promises.unlink(tempFile); }); test("should handle non-existent files", async () => { await expect((0, schema_extractor_1.extractSchemasFromFile)("non-existent-file.ts")).rejects.toThrow(); }); test("should handle empty directories", async () => { const emptyDir = path.join(testDir, "empty"); await fs.promises.mkdir(emptyDir, { recursive: true }); await expect((0, schema_extractor_1.extractSchemasFromDirectory)(emptyDir)).rejects.toThrow(); await fs.promises.rmdir(emptyDir); }); }); describe("Performance and Efficiency", () => { test("should process multiple files efficiently", async () => { const startTime = Date.now(); const result = await (0, schema_extractor_1.extractWithMetadata)(testDir, { recursive: true }); const endTime = Date.now(); expect(result.processingTime).toBeLessThan(endTime - startTime + 100); // Allow some margin expect(result.totalTypesFound).toBeGreaterThan(0); expect(result.typesWithSupportsRuntimeValidation).toBeGreaterThan(0); }); test("should only resolve types that extend SupportsRuntimeValidation", async () => { const result = await (0, schema_extractor_1.extractWithMetadata)(testDir, { recursive: true }); // Should find more total types than validation types expect(result.totalTypesFound).toBeGreaterThan(result.typesWithSupportsRuntimeValidation); // All returned schemas should be from validation types expect(result.schemas).toHaveLength(result.typesWithSupportsRuntimeValidation); }); }); }); //# sourceMappingURL=schema-extractor.test.js.map