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
JavaScript
"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