UNPKG

@mockilo/mocktail-cli

Version:

**Craft your data cocktail — realistic mock data, shaken not stirred.**

432 lines (374 loc) 13.6 kB
import { getLocalizedFaker } from '../utils/localeManager'; import { SchemaField } from '../schema-parsers/baseSchemaParser'; export interface TypeGenerator { name: string; patterns: (string | RegExp)[]; generate: (field: SchemaField, context?: GenerationContext) => any; priority: number; // Higher priority = checked first description?: string; } export interface GenerationContext { modelName: string; recordIndex: number; relatedFields?: Record<string, any>; existingData?: Record<string, any[]>; schemaType?: string; } export interface FormatGenerator { format: string; baseType: string; generate: (field: SchemaField, context?: GenerationContext) => any; validate?: (value: any) => boolean; } export interface ScalarGenerator { scalarName: string; generate: (field: SchemaField, context?: GenerationContext) => any; validate?: (value: any) => boolean; } export class ExtensibleTypeSystem { private typeGenerators: Map<string, TypeGenerator> = new Map(); private formatGenerators: Map<string, FormatGenerator> = new Map(); private scalarGenerators: Map<string, ScalarGenerator> = new Map(); private contextualGenerators: Map<string, TypeGenerator> = new Map(); constructor() { this.registerDefaultGenerators(); this.registerDefaultFormats(); this.registerDefaultScalars(); this.registerContextualGenerators(); } // Register a custom type generator registerTypeGenerator(generator: TypeGenerator): void { this.typeGenerators.set(generator.name, generator); } // Register a format-based generator (JSON Schema, OpenAPI) registerFormatGenerator(generator: FormatGenerator): void { this.formatGenerators.set(generator.format, generator); } // Register a scalar generator (GraphQL, custom types) registerScalarGenerator(generator: ScalarGenerator): void { this.scalarGenerators.set(generator.scalarName, generator); } // Generate value for a field using the most appropriate generator generateValue(field: SchemaField, context?: GenerationContext): any { // 1. Check for custom scalars first (highest priority) if (this.scalarGenerators.has(field.type)) { const generator = this.scalarGenerators.get(field.type)!; return generator.generate(field, context); } // 2. Check for format-based generation (JSON Schema formats) if (field.format && this.formatGenerators.has(field.format)) { const generator = this.formatGenerators.get(field.format)!; return generator.generate(field, context); } // 3. Check contextual generators (field name + type combinations) const contextKey = `${field.name}:${field.type}`; if (this.contextualGenerators.has(contextKey)) { const generator = this.contextualGenerators.get(contextKey)!; return generator.generate(field, context); } // 4. Check field name patterns (smart field detection) for (const [_, generator] of this.typeGenerators) { for (const pattern of generator.patterns) { if (typeof pattern === 'string') { if (field.name.toLowerCase().includes(pattern.toLowerCase())) { return generator.generate(field, context); } } else if (pattern instanceof RegExp) { if (pattern.test(field.name) || pattern.test(field.type)) { return generator.generate(field, context); } } } } // 5. Fallback to basic type generation return this.generateBasicType(field, context); } private generateBasicType(field: SchemaField, _context?: GenerationContext): any { const baseType = field.type.replace(/\[\]$/, "").toLowerCase(); switch (baseType) { case 'string': case 'text': return getLocalizedFaker().lorem.words({ min: 1, max: 3 }); case 'int': case 'integer': case 'number': return getLocalizedFaker().number.int({ min: 1, max: 1000 }); case 'float': case 'double': case 'decimal': return getLocalizedFaker().number.float({ min: 0, max: 1000, fractionDigits: 2 }); case 'boolean': case 'bool': return getLocalizedFaker().datatype.boolean(); case 'date': case 'datetime': case 'timestamp': return getLocalizedFaker().date.past(); case 'uuid': case 'guid': return getLocalizedFaker().string.uuid(); case 'object': return { key: getLocalizedFaker().lorem.word(), value: getLocalizedFaker().lorem.sentence() }; case 'array': return []; case 'null': return null; default: // If we don't recognize the type, generate a basic string return getLocalizedFaker().lorem.words({ min: 1, max: 3 }); } } private registerDefaultGenerators(): void { // Email generators with context awareness this.registerTypeGenerator({ name: 'email', patterns: ['email', 'mail', /.*email.*/i], priority: 100, generate: (_field, context) => { // Try to use related name fields for realistic emails const firstName = context?.relatedFields?.['firstName'] || context?.relatedFields?.['name']; const lastName = context?.relatedFields?.['lastName']; if (firstName && lastName) { return getLocalizedFaker().internet.email({ firstName, lastName }); } else if (firstName) { return getLocalizedFaker().internet.email({ firstName }); } return getLocalizedFaker().internet.email(); } }); // Name generators this.registerTypeGenerator({ name: 'name', patterns: ['name', 'fullname', 'username', 'displayname'], priority: 90, generate: () => getLocalizedFaker().person.fullName() }); this.registerTypeGenerator({ name: 'firstName', patterns: ['firstname', 'first_name', 'fname'], priority: 90, generate: () => getLocalizedFaker().person.firstName() }); this.registerTypeGenerator({ name: 'lastName', patterns: ['lastname', 'last_name', 'lname', 'surname'], priority: 90, generate: () => getLocalizedFaker().person.lastName() }); // URL/Website generators this.registerTypeGenerator({ name: 'url', patterns: ['url', 'website', 'homepage', 'link', /.*url.*/i], priority: 85, generate: () => getLocalizedFaker().internet.url() }); // Phone generators this.registerTypeGenerator({ name: 'phone', patterns: ['phone', 'telephone', 'mobile', /.*phone.*/i], priority: 85, generate: () => getLocalizedFaker().phone.number() }); // Address generators this.registerTypeGenerator({ name: 'address', patterns: ['address', 'street', 'location'], priority: 80, generate: () => getLocalizedFaker().location.streetAddress() }); this.registerTypeGenerator({ name: 'city', patterns: ['city', 'town'], priority: 80, generate: () => getLocalizedFaker().location.city() }); this.registerTypeGenerator({ name: 'country', patterns: ['country', 'nation'], priority: 80, generate: () => getLocalizedFaker().location.country() }); // Content generators this.registerTypeGenerator({ name: 'title', patterns: ['title', 'heading', 'subject'], priority: 75, generate: () => getLocalizedFaker().lorem.sentence({ min: 3, max: 8 }) }); this.registerTypeGenerator({ name: 'description', patterns: ['description', 'desc', 'summary', 'bio'], priority: 75, generate: () => getLocalizedFaker().lorem.paragraph({ min: 1, max: 3 }) }); this.registerTypeGenerator({ name: 'content', patterns: ['content', 'body', 'text', 'message'], priority: 75, generate: () => getLocalizedFaker().lorem.paragraphs({ min: 1, max: 4 }) }); // ID generators this.registerTypeGenerator({ name: 'id', patterns: ['id', '_id', /.*id$/i], priority: 95, generate: () => getLocalizedFaker().string.uuid() }); } private registerDefaultFormats(): void { // JSON Schema / OpenAPI formats this.registerFormatGenerator({ format: 'email', baseType: 'string', generate: (_field, context) => { const firstName = context?.relatedFields?.['firstName'] || context?.relatedFields?.['name']; const lastName = context?.relatedFields?.['lastName']; if (firstName && lastName) { return getLocalizedFaker().internet.email({ firstName, lastName }); } return getLocalizedFaker().internet.email(); } }); this.registerFormatGenerator({ format: 'uri', baseType: 'string', generate: () => getLocalizedFaker().internet.url() }); this.registerFormatGenerator({ format: 'url', baseType: 'string', generate: () => getLocalizedFaker().internet.url() }); this.registerFormatGenerator({ format: 'date', baseType: 'string', generate: () => getLocalizedFaker().date.past().toISOString().split('T')[0] }); this.registerFormatGenerator({ format: 'date-time', baseType: 'string', generate: () => getLocalizedFaker().date.past().toISOString() }); this.registerFormatGenerator({ format: 'time', baseType: 'string', generate: () => getLocalizedFaker().date.recent().toTimeString().split(' ')[0] }); this.registerFormatGenerator({ format: 'uuid', baseType: 'string', generate: () => getLocalizedFaker().string.uuid() }); this.registerFormatGenerator({ format: 'ipv4', baseType: 'string', generate: () => getLocalizedFaker().internet.ip() }); this.registerFormatGenerator({ format: 'ipv6', baseType: 'string', generate: () => getLocalizedFaker().internet.ipv6() }); this.registerFormatGenerator({ format: 'hostname', baseType: 'string', generate: () => getLocalizedFaker().internet.domainName() }); this.registerFormatGenerator({ format: 'password', baseType: 'string', generate: () => getLocalizedFaker().internet.password({ length: 12 }) }); } private registerDefaultScalars(): void { // GraphQL custom scalars this.registerScalarGenerator({ scalarName: 'EmailAddress', generate: (_field, context) => { const firstName = context?.relatedFields?.['firstName'] || context?.relatedFields?.['name']; const lastName = context?.relatedFields?.['lastName']; if (firstName && lastName) { return getLocalizedFaker().internet.email({ firstName, lastName }); } return getLocalizedFaker().internet.email(); } }); this.registerScalarGenerator({ scalarName: 'URL', generate: () => getLocalizedFaker().internet.url() }); this.registerScalarGenerator({ scalarName: 'JSON', generate: () => ({ key: getLocalizedFaker().lorem.word(), value: getLocalizedFaker().lorem.sentence(), timestamp: getLocalizedFaker().date.recent().toISOString() }) }); this.registerScalarGenerator({ scalarName: 'DateTime', generate: () => getLocalizedFaker().date.past().toISOString() }); this.registerScalarGenerator({ scalarName: 'Date', generate: () => getLocalizedFaker().date.past().toISOString().split('T')[0] }); this.registerScalarGenerator({ scalarName: 'Time', generate: () => getLocalizedFaker().date.recent().toTimeString().split(' ')[0] }); this.registerScalarGenerator({ scalarName: 'UUID', generate: () => getLocalizedFaker().string.uuid() }); this.registerScalarGenerator({ scalarName: 'PhoneNumber', generate: () => getLocalizedFaker().phone.number() }); this.registerScalarGenerator({ scalarName: 'PostalCode', generate: () => getLocalizedFaker().location.zipCode() }); this.registerScalarGenerator({ scalarName: 'CountryCode', generate: () => getLocalizedFaker().location.countryCode() }); } private registerContextualGenerators(): void { // Context-aware generators that consider field name + type + related data this.registerTypeGenerator({ name: 'userEmail', patterns: [], // No pattern matching, only explicit context priority: 110, generate: (field, context) => { if (field.name.toLowerCase().includes('email') && context?.modelName.toLowerCase().includes('user')) { const firstName = context?.relatedFields?.['firstName'] || context?.relatedFields?.['name']; const lastName = context?.relatedFields?.['lastName']; if (firstName && lastName) { return getLocalizedFaker().internet.email({ firstName, lastName }); } else if (firstName) { return getLocalizedFaker().internet.email({ firstName }); } } return getLocalizedFaker().internet.email(); } }); } // Get all registered generators for debugging getRegisteredGenerators(): { types: string[]; formats: string[]; scalars: string[]; } { return { types: Array.from(this.typeGenerators.keys()), formats: Array.from(this.formatGenerators.keys()), scalars: Array.from(this.scalarGenerators.keys()) }; } } // Singleton instance export const extensibleTypeSystem = new ExtensibleTypeSystem();