prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
175 lines • 8.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CircularDependencyDetector = void 0;
exports.detectCircularDependencies = detectCircularDependencies;
/**
* Detects circular dependencies in Prisma model relationships and determines
* which relations should be excluded to break the cycles.
*/
class CircularDependencyDetector {
constructor(models) {
this.relationGraph = new Map();
this.models = models;
this.buildRelationGraph();
}
/**
* Build a graph of all model relationships
*/
buildRelationGraph() {
for (const model of this.models) {
const relations = [];
for (const field of model.fields) {
// Only process relation fields (kind === 'object')
if (field.kind === 'object') {
relations.push({
fromModel: model.name,
toModel: field.type,
fieldName: field.name,
relationName: field.relationName,
isOptional: !field.isRequired,
isList: field.isList,
});
}
}
this.relationGraph.set(model.name, relations);
}
}
/**
* Detect problematic circular dependencies (not just any cycle).
* Only considers cycles that would cause TypeScript compilation issues.
*/
detectProblematicCycles() {
const problematicCycles = [];
// Find direct bidirectional relationships (A -> B and B -> A)
for (const model of this.models) {
const modelRelations = this.relationGraph.get(model.name) || [];
for (const relation of modelRelations) {
const targetModel = relation.toModel;
// Skip self-references for now (handle separately)
if (targetModel === model.name)
continue;
// Check if target model has a relation back to this model
const targetRelations = this.relationGraph.get(targetModel) || [];
const backReference = targetRelations.find((r) => r.toModel === model.name);
if (backReference) {
// Found bidirectional relationship - this is potentially problematic
// Only add if we haven't already added the reverse
const cycleExists = problematicCycles.some((cycle) => cycle.length === 3 &&
cycle[0] === targetModel &&
cycle[1] === model.name &&
cycle[2] === targetModel);
if (!cycleExists) {
problematicCycles.push([model.name, targetModel, model.name]);
}
}
}
}
// Handle self-referencing models
for (const model of this.models) {
const modelRelations = this.relationGraph.get(model.name) || [];
const selfReferences = modelRelations.filter((r) => r.toModel === model.name);
if (selfReferences.length > 1) {
// Multiple self-references create circular dependencies
problematicCycles.push([model.name, model.name]);
}
}
return problematicCycles;
}
/**
* Determine which relations to exclude to break cycles.
* Strategy: For bidirectional relationships, prefer to exclude optional relations
* over required ones, and exclude "back-references" over "forward-references".
*/
determineExclusions(cycles) {
var _a, _b;
const exclusions = new Map();
for (const cycle of cycles) {
// Handle self-referencing cycles (model -> model)
if (cycle.length === 2 && cycle[0] === cycle[1]) {
const modelName = cycle[0];
const relations = this.relationGraph.get(modelName) || [];
const selfReferences = relations.filter((r) => r.toModel === modelName);
if (selfReferences.length > 1) {
// For multiple self-references, exclude all but one
// Keep the first one, exclude the rest
for (let i = 1; i < selfReferences.length; i++) {
if (!exclusions.has(modelName)) {
exclusions.set(modelName, new Set());
}
(_a = exclusions.get(modelName)) === null || _a === void 0 ? void 0 : _a.add(selfReferences[i].fieldName);
}
}
continue;
}
// Handle bidirectional relationships (A -> B -> A)
if (cycle.length === 3 && cycle[0] === cycle[2]) {
const modelA = cycle[0];
const modelB = cycle[1];
const relationsA = this.relationGraph.get(modelA) || [];
const relationsB = this.relationGraph.get(modelB) || [];
const relationAtoB = relationsA.find((r) => r.toModel === modelB);
const relationBtoA = relationsB.find((r) => r.toModel === modelA);
if (relationAtoB && relationBtoA) {
// Decide which relation to exclude based on priority:
// 1. Prefer to keep required relations over optional ones
// 2. Prefer to keep non-list relations over list relations
// 3. If equal, exclude the "back-reference" (second relation found)
let excludeRelation = null;
if (relationAtoB.isOptional && !relationBtoA.isOptional) {
// A->B is optional, B->A is required: exclude A->B
excludeRelation = { model: modelA, field: relationAtoB.fieldName };
}
else if (!relationAtoB.isOptional && relationBtoA.isOptional) {
// A->B is required, B->A is optional: exclude B->A
excludeRelation = { model: modelB, field: relationBtoA.fieldName };
}
else if (relationAtoB.isList && !relationBtoA.isList) {
// A->B is array, B->A is single: exclude A->B (keep the FK side)
excludeRelation = { model: modelA, field: relationAtoB.fieldName };
}
else if (!relationAtoB.isList && relationBtoA.isList) {
// A->B is single, B->A is array: exclude B->A (keep the FK side)
excludeRelation = { model: modelB, field: relationBtoA.fieldName };
}
else {
// If equal priority, exclude the alphabetically later model's relation
// This provides consistent behavior
if (modelA.localeCompare(modelB) > 0) {
excludeRelation = { model: modelA, field: relationAtoB.fieldName };
}
else {
excludeRelation = { model: modelB, field: relationBtoA.fieldName };
}
}
if (excludeRelation) {
if (!exclusions.has(excludeRelation.model)) {
exclusions.set(excludeRelation.model, new Set());
}
(_b = exclusions.get(excludeRelation.model)) === null || _b === void 0 ? void 0 : _b.add(excludeRelation.field);
}
}
}
}
return exclusions;
}
/**
* Detect circular dependencies and return which relations should be excluded
*/
detect() {
const cycles = this.detectProblematicCycles();
const excludedRelations = this.determineExclusions(cycles);
return {
excludedRelations,
cycles,
};
}
}
exports.CircularDependencyDetector = CircularDependencyDetector;
/**
* Utility function to detect circular dependencies in Prisma models
*/
function detectCircularDependencies(models) {
const detector = new CircularDependencyDetector(models);
return detector.detect();
}
//# sourceMappingURL=circular-dependency-detector.js.map