prismaql
Version:
A powerful tool for managing and editing Prisma schema files using a SQL-like DSL.
217 lines • 10.8 kB
JavaScript
import treeify from "treeify";
import chalk from 'chalk';
import boxen from 'boxen';
import { PrismaQlRelationCollector } from "./field-relation-collector.js";
import pkg from '@prisma/internals';
const { getDMMF } = pkg;
import fs from 'fs';
const collector = new PrismaQlRelationCollector();
export class PrismaQlFieldRelationLogger {
relations;
setRelations(relations) {
this.relations = relations;
}
constructor(relations) {
if (relations) {
this.setRelations(relations);
}
}
buildJsonModelTrees(rootModel, relations, maxDepth, depth = 0, visitedModels = new Set()) {
if (depth > maxDepth || visitedModels.has(rootModel)) {
return { trees: [], models: new Set(), relations: new Set() };
}
visitedModels.add(rootModel);
let trees = [];
let models = new Set();
let relationsSet = new Set();
const modelRelations = relations.filter(rel => rel.modelName === rootModel);
let relationNodes = [];
for (const relation of modelRelations) {
const isSelfRelation = relation.modelName === relation.relatedModel;
const isList = (relation.type === "1:M" || relation.type === "M:N") && !relation.foreignKey;
let relationNode = {
relatedModel: relation.relatedModel,
field: relation.fieldName || relation.modelName,
type: relation.type,
alias: relation.fieldName || relation.relationName,
foreignKey: relation.foreignKey,
referenceKey: relation.referenceKey,
relationTable: relation.relationTable,
inverseField: relation.inverseField,
constraints: relation.constraints || [],
isSelfRelation,
isList,
};
models.add(rootModel);
models.add(relation.relatedModel);
relationsSet.add(`${rootModel} -> ${relation.relatedModel}`);
const subTree = this.buildJsonModelTrees(relation.relatedModel, relations, maxDepth, depth + 1, visitedModels);
if (subTree.trees.length > 0) {
relationNode.subTree = subTree.trees[0];
}
relationNodes.push(relationNode);
}
trees.push({ model: rootModel, relations: relationNodes });
return { trees, models, relations: relationsSet };
}
buildModelTrees(rootModel, relations, maxDepth, depth = 0, visitedModels = new Set()) {
if (depth > maxDepth || visitedModels.has(rootModel))
return { trees: [], models: new Set(), relations: new Set() };
visitedModels.add(rootModel);
let trees = [];
let models = new Set();
let relationsSet = new Set();
const modelRelations = relations.filter(rel => rel.modelName === rootModel);
let table = {};
for (const relation of modelRelations) {
const relationType = relation.type;
const name = relation.fieldName || relation.modelName;
const relationAlias = `(as ${relation.fieldName || relation.relationName})`;
// Determine if the relation is self-referencing
const isSelfRelation = relation.modelName === relation.relatedModel;
const selfRelationIcon = isSelfRelation ? chalk.yellow("🔁") : ""; // Add self-relation icon
let keyInfo = chalk.gray("[-]");
if (relation.foreignKey) {
const direction = relation.relationDirection === "backward" ? "←" : "→";
keyInfo = `[FK: ${chalk.blue(relation.foreignKey)} ${direction} ${chalk.green(relation.referenceKey || "id")}]`;
}
else if (relation.relationTable) {
keyInfo = `[M:N via ${chalk.yellow(relation.relationTable)}]`;
}
if (relation.relationTable && relation.relationTable !== relation.modelName) {
if (!table[relation.relationTable]) {
table[relation.relationTable] = {}; // Adding join table
}
// Add relation inside join table
table[relation.relationTable][`→ ${chalk.yellow(relation.modelName)}:${chalk.cyan(relation.fieldName)} [FK: ${chalk.blue(relation.foreignKey || "?")} → ${chalk.green(relation.referenceKey || "?")}]`] = {};
table[relation.relationTable][`→ ${chalk.yellow(relation.relatedModel)}:${chalk.cyan(relation.inverseField)} [FK: ${chalk.blue(relation.foreignKey || "?")} → ${chalk.green(relation.referenceKey || "?")}]`] = {};
}
const constraints = relation?.constraints?.length
? `Constraints: ${chalk.magenta(relation.constraints.join(", "))}`
: "";
const isList = (relationType === "1:M" || relationType === "M:N") && !relation?.foreignKey;
let relationLabel = `→ ${chalk.yellow(relation.relatedModel + (isList ? '[]' : ''))}:${chalk.cyan(name)} ${relationAlias} ${chalk.red(relationType)} ${keyInfo} ${constraints} ${selfRelationIcon}`;
if (!table[relationLabel]) {
table[relationLabel] = {};
}
// Add to statistics
models.add(rootModel);
models.add(relation.relatedModel);
relationsSet.add(`${rootModel} -> ${relation.relatedModel}`);
}
trees.push({ [chalk.bold(rootModel)]: table });
for (const relation of modelRelations) {
const subTree = this.buildModelTrees(relation.relatedModel, relations, maxDepth, depth + 1, visitedModels);
trees = trees.concat(subTree.trees);
subTree.models.forEach(m => models.add(m));
subTree.relations.forEach(r => relationsSet.add(r));
}
return { trees, models, relations: relationsSet };
}
getRelationStatistics(modelName, maxDepth = 1) {
if (!this.relations?.length) {
throw new Error('No relations found. Please run relation-collector first and use the setRelations method to set the relations.');
}
let relatedModels = new Set(); // Unique models
let relationCount = 0; // Number of relations
// Recursive function to traverse relations
const exploreRelations = (currentModel, depth) => {
if (depth > maxDepth || relatedModels.has(currentModel))
return;
relatedModels.add(currentModel);
// Filter relations for the current model
for (const rel of this.relations.filter(r => r.modelName === currentModel)) {
relationCount++;
exploreRelations(rel.relatedModel, depth + 1);
}
};
// Start traversal from `modelName`
exploreRelations(modelName, 1);
return {
uniqueModels: relatedModels.size, // Number of unique models
totalRelations: relationCount, // Total number of relations
maxDepth // Depth passed from outside
};
}
collectRelationStatistics(models, relations, rootModel, maxDepth) {
const directRelations = rootModel ? [...relations].filter(r => r.startsWith(rootModel)) : [...relations];
return {
uniqueModels: models.size,
totalRelations: relations.size,
directRelations: directRelations.length,
maxDepth
};
}
async parseSchemaAndSetRelations(schema) {
const dmmf = await getDMMF({ datamodel: schema });
const models = dmmf.datamodel.models;
this.setRelations(await collector.setModels(models));
return this.relations;
}
async provideRelationsFromBuilder(builder) {
const schema = builder.print({ sort: true });
return this.parseSchemaAndSetRelations(schema);
}
async provideRelationsFromSchema(schema) {
return this.parseSchemaAndSetRelations(schema);
}
async privideRelationByPrismaPath(prismaPath) {
const prismaSchemaContent = fs.readFileSync(prismaPath, 'utf-8');
return this.parseSchemaAndSetRelations(prismaSchemaContent);
}
generateRelationTreeLog(rootModel, maxDepth = 1, relations) {
if (relations?.length) {
this.setRelations(relations);
}
if (!this.relations?.length) {
throw new Error('No relations found.');
}
const { models, relations: rels, trees } = this.buildModelTrees(rootModel, this.relations, maxDepth);
// Collect statistics
const stats = this.collectRelationStatistics(models, rels, rootModel, maxDepth);
let output = `${chalk.green.bold('📊 Relation Tree Statistics')}\n`;
output += `${chalk.yellow('Model:')} ${chalk.bold(rootModel)}\n`;
output += `${chalk.cyan('Max Depth:')} ${chalk.bold(maxDepth)}\n`;
output += `${chalk.blue('Related Models:')} ${chalk.bold(stats.uniqueModels)}\n`;
output += `${chalk.magenta('Total Relations:')} ${chalk.bold(stats.totalRelations)}\n`;
output += `${chalk.redBright('Direct Relations:')} ${chalk.bold(stats.directRelations)}\n`;
// direct relations
let treeOutput = '';
for (const tree of trees) {
treeOutput += treeify.asTree(tree, true, true) + '\n';
}
const results = [...rels.values()].filter(el => {
return el.startsWith(rootModel) || el.endsWith(rootModel);
}).map(r => chalk.gray(r)).join('\n');
const relsList = `${chalk.white.bold('🔗 Direct Relations')}\n${results}`;
// Output statistics + tree, without extra spaces
return boxen(output.trim() + '\n' + treeOutput.trim() + `\n\n${relsList}`, {
padding: 1,
borderColor: 'green',
borderStyle: 'round'
});
}
}
export const getRelationStatistics = (relations, modelName, maxDepth = 1) => {
let relatedModels = new Set(); // Unique models
let relationCount = 0; // Number of relations
// Recursive function to traverse relations
const exploreRelations = (currentModel, depth) => {
if (depth > maxDepth || relatedModels.has(currentModel))
return;
relatedModels.add(currentModel);
// Filter relations for the current model
for (const rel of relations.filter(r => r.modelName === currentModel)) {
relationCount++;
exploreRelations(rel.relatedModel, depth + 1);
}
};
// Start traversal from `modelName`
exploreRelations(modelName, 1);
return {
uniqueModels: relatedModels.size, // Number of unique models
totalRelations: relationCount, // Total number of relations
maxDepth // Depth passed from outside
};
};
//# sourceMappingURL=field-relation-logger.js.map