UNPKG

mocktail-cli

Version:

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

470 lines (407 loc) 14.2 kB
import { Model } from '../types'; export interface DependencyNode { name: string; model: Model; dependencies: Set<string>; dependents: Set<string>; visited: boolean; inStack: boolean; cycleId?: string; } export interface Cycle { id: string; nodes: string[]; type: 'self-reference' | 'simple-cycle' | 'complex-cycle'; strength: 'weak' | 'strong'; // weak = optional relations, strong = required relations } export interface CycleResolutionStrategy { name: string; description: string; resolve: (cycle: Cycle, nodes: Map<string, DependencyNode>) => ResolutionPlan; } export interface ResolutionPlan { strategy: string; breakPoints: Array<{ from: string; to: string; field: string; action: 'defer' | 'partial' | 'lazy' | 'break'; }>; generationOrder: string[]; deferredRelations: Array<{ model: string; field: string; targetModel: string; }>; } export class CircularDependencyResolver { private strategies: Map<string, CycleResolutionStrategy> = new Map(); private preferredStrategy: string = 'smart-break'; constructor(options: { maxDepth?: number; preferredStrategy?: string } = {}) { this.preferredStrategy = options.preferredStrategy || 'smart-break'; this.registerDefaultStrategies(); } // Main method to resolve circular dependencies resolveDependencies(models: Model[]): { generationOrder: Model[]; resolutionPlan: ResolutionPlan | null; cycles: Cycle[]; } { // 1. Build dependency graph const nodes = this.buildDependencyGraph(models); // 2. Detect cycles const cycles = this.detectCycles(nodes); // 3. If no cycles, return simple topological order if (cycles.length === 0) { return { generationOrder: this.topologicalSort(nodes), resolutionPlan: null, cycles: [] }; } // 4. Resolve cycles using preferred strategy const strategy = this.strategies.get(this.preferredStrategy); if (!strategy) { throw new Error(`Unknown resolution strategy: ${this.preferredStrategy}`); } // 5. Apply resolution strategy to the most complex cycle first const primaryCycle = this.selectPrimaryCycle(cycles); const resolutionPlan = strategy.resolve(primaryCycle, nodes); // 6. Apply resolution plan and get final order const finalOrder = this.applyResolutionPlan(resolutionPlan, nodes); return { generationOrder: finalOrder, resolutionPlan, cycles }; } private buildDependencyGraph(models: Model[]): Map<string, DependencyNode> { const nodes = new Map<string, DependencyNode>(); // Initialize nodes for (const model of models) { nodes.set(model.name, { name: model.name, model, dependencies: new Set(), dependents: new Set(), visited: false, inStack: false }); } // Build dependencies for (const model of models) { const node = nodes.get(model.name)!; for (const field of model.fields) { // Check if this field references another model if (field.isRelation && nodes.has(field.type)) { // This model depends on the referenced model node.dependencies.add(field.type); // The referenced model has this model as a dependent const targetNode = nodes.get(field.type)!; targetNode.dependents.add(model.name); } // Check for foreign key relationships if (field.relationFromFields && field.relationFromFields.length > 0) { // This is a foreign key field, so this model depends on the target if (nodes.has(field.type)) { node.dependencies.add(field.type); const targetNode = nodes.get(field.type)!; targetNode.dependents.add(model.name); } } } } return nodes; } private detectCycles(nodes: Map<string, DependencyNode>): Cycle[] { const cycles: Cycle[] = []; const visited = new Set<string>(); const recursionStack = new Set<string>(); for (const [nodeName, node] of nodes) { if (!visited.has(nodeName)) { this.dfsDetectCycles(node, nodes, visited, recursionStack, [], cycles); } } return cycles; } private dfsDetectCycles( node: DependencyNode, nodes: Map<string, DependencyNode>, visited: Set<string>, recursionStack: Set<string>, path: string[], cycles: Cycle[] ): void { visited.add(node.name); recursionStack.add(node.name); path.push(node.name); for (const depName of node.dependencies) { const depNode = nodes.get(depName); if (!depNode) continue; if (recursionStack.has(depName)) { // Found a cycle const cycleStart = path.indexOf(depName); const cycleNodes = path.slice(cycleStart); cycleNodes.push(depName); // Complete the cycle const cycle: Cycle = { id: `cycle-${cycles.length + 1}`, nodes: cycleNodes, type: this.determineCycleType(cycleNodes), strength: this.determineCycleStrength(cycleNodes, nodes) }; cycles.push(cycle); } else if (!visited.has(depName)) { this.dfsDetectCycles(depNode, nodes, visited, recursionStack, path, cycles); } } recursionStack.delete(node.name); path.pop(); } private determineCycleType(cycleNodes: string[]): 'self-reference' | 'simple-cycle' | 'complex-cycle' { if (cycleNodes.length === 2 && cycleNodes[0] === cycleNodes[1]) { return 'self-reference'; } else if (cycleNodes.length <= 3) { return 'simple-cycle'; } else { return 'complex-cycle'; } } private determineCycleStrength(cycleNodes: string[], nodes: Map<string, DependencyNode>): 'weak' | 'strong' { // Check if any of the relationships in the cycle are optional for (let i = 0; i < cycleNodes.length - 1; i++) { const fromNodeName = cycleNodes[i]; if (!fromNodeName) continue; const fromNode = nodes.get(fromNodeName); const toNodeName = cycleNodes[i + 1]; if (fromNode) { // Check if there's an optional relationship for (const field of fromNode.model.fields) { if (field.type === toNodeName && field.isOptional) { return 'weak'; } } } } return 'strong'; } private selectPrimaryCycle(cycles: Cycle[]): Cycle { // Select the most complex cycle to resolve first return cycles.reduce((primary, current) => { // Prioritize by complexity, then by strength if (current.type === 'complex-cycle' && primary.type !== 'complex-cycle') { return current; } if (current.strength === 'strong' && primary.strength === 'weak') { return current; } if (current.nodes.length > primary.nodes.length) { return current; } return primary; }); } private topologicalSort(nodes: Map<string, DependencyNode>): Model[] { const result: Model[] = []; const visited = new Set<string>(); const temp = new Set<string>(); const visit = (nodeName: string) => { if (temp.has(nodeName)) { // This shouldn't happen if cycles are resolved return; } if (visited.has(nodeName)) { return; } temp.add(nodeName); const node = nodes.get(nodeName); if (node) { for (const depName of node.dependencies) { visit(depName); } temp.delete(nodeName); visited.add(nodeName); result.push(node.model); } }; for (const nodeName of nodes.keys()) { if (!visited.has(nodeName)) { visit(nodeName); } } return result; } private applyResolutionPlan(plan: ResolutionPlan, nodes: Map<string, DependencyNode>): Model[] { // Apply the resolution plan by modifying the dependency graph for (const breakPoint of plan.breakPoints) { const fromNode = nodes.get(breakPoint.from); const toNode = nodes.get(breakPoint.to); if (fromNode && toNode) { switch (breakPoint.action) { case 'break': fromNode.dependencies.delete(breakPoint.to); toNode.dependents.delete(breakPoint.from); break; case 'defer': // Keep the dependency but mark it as deferred // This will be handled during generation break; } } } // Return the models in the specified generation order return plan.generationOrder .map(name => nodes.get(name)?.model) .filter((model): model is Model => model !== undefined); } private registerDefaultStrategies(): void { // Strategy 1: Smart Break - Break at optional relationships this.strategies.set('smart-break', { name: 'Smart Break', description: 'Break cycles at optional relationships, prefer weak cycles', resolve: (cycle, nodes) => { const breakPoints: ResolutionPlan['breakPoints'] = []; const deferredRelations: ResolutionPlan['deferredRelations'] = []; // Find the best place to break the cycle for (let i = 0; i < cycle.nodes.length - 1; i++) { const fromName = cycle.nodes[i]; const toName = cycle.nodes[i + 1]; if (!fromName || !toName) continue; const fromNode = nodes.get(fromName); if (fromNode) { // Look for optional relationships to break for (const field of fromNode.model.fields) { if (field.type === toName && field.isOptional) { breakPoints.push({ from: fromName, to: toName, field: field.name, action: 'defer' }); deferredRelations.push({ model: fromName, field: field.name, targetModel: toName }); break; } } } } // If no optional relationships found, break at the weakest link if (breakPoints.length === 0) { const fromName = cycle.nodes[0]; const toName = cycle.nodes[1]; if (!fromName || !toName) return { strategy: 'smart-break', breakPoints, generationOrder: cycle.nodes, deferredRelations }; const fromNode = nodes.get(fromName); if (fromNode) { const field = fromNode.model.fields.find(f => f.type === toName); if (field) { breakPoints.push({ from: fromName, to: toName, field: field.name, action: 'break' }); } } } // Generate order: dependencies first, then dependents const generationOrder = [...cycle.nodes]; return { strategy: 'smart-break', breakPoints, generationOrder, deferredRelations }; } }); // Strategy 2: Lazy Loading - Generate partial objects first this.strategies.set('lazy-loading', { name: 'Lazy Loading', description: 'Generate objects with null relations first, then populate later', resolve: (cycle, nodes) => { const breakPoints: ResolutionPlan['breakPoints'] = []; const deferredRelations: ResolutionPlan['deferredRelations'] = []; // Mark all cycle relationships as lazy for (let i = 0; i < cycle.nodes.length - 1; i++) { const fromName = cycle.nodes[i]; const toName = cycle.nodes[i + 1]; if (!fromName || !toName) continue; const fromNode = nodes.get(fromName); if (fromNode) { for (const field of fromNode.model.fields) { if (field.type === toName) { breakPoints.push({ from: fromName, to: toName, field: field.name, action: 'lazy' }); deferredRelations.push({ model: fromName, field: field.name, targetModel: toName }); } } } } return { strategy: 'lazy-loading', breakPoints, generationOrder: cycle.nodes, deferredRelations }; } }); // Strategy 3: Partial References - Generate with IDs only this.strategies.set('partial-references', { name: 'Partial References', description: 'Generate objects with ID references only, no nested objects', resolve: (cycle, nodes) => { const breakPoints: ResolutionPlan['breakPoints'] = []; for (let i = 0; i < cycle.nodes.length - 1; i++) { const fromName = cycle.nodes[i]; const toName = cycle.nodes[i + 1]; if (!fromName || !toName) continue; const fromNode = nodes.get(fromName); if (fromNode) { for (const field of fromNode.model.fields) { if (field.type === toName) { breakPoints.push({ from: fromName, to: toName, field: field.name, action: 'partial' }); } } } } return { strategy: 'partial-references', breakPoints, generationOrder: cycle.nodes, deferredRelations: [] }; } }); } // Public method to register custom strategies registerStrategy(strategy: CycleResolutionStrategy): void { this.strategies.set(strategy.name, strategy); } // Get available strategies getAvailableStrategies(): string[] { return Array.from(this.strategies.keys()); } // Set preferred strategy setPreferredStrategy(strategyName: string): void { if (!this.strategies.has(strategyName)) { throw new Error(`Unknown strategy: ${strategyName}`); } this.preferredStrategy = strategyName; } } // Export singleton instance export const circularDependencyResolver = new CircularDependencyResolver();