Files
mcp_web_dev_server/dist/tools/laravel/generateEloquentRelation.js
T
2026-07-31 13:12:54 -04:00

116 lines
5.0 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { z } from 'zod';
import { camelCase, pascalCase } from '../../lib/strings.js';
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
import { requireLaravel } from './requireLaravel.js';
const RELATION_TYPES = [
'belongsTo',
'hasOne',
'hasMany',
'belongsToMany',
'hasManyThrough',
'morphTo',
'morphMany',
'morphOne',
];
function findModelFile(root, model) {
const candidates = [
path.join(root, 'app', 'Models', `${model}.php`),
path.join(root, 'app', `${model}.php`),
];
return candidates.find((file) => fs.existsSync(file)) ?? null;
}
function relationMethodSource(methodName, relation, relatedModel) {
const related = `\\App\\Models\\${relatedModel}::class`;
return `
public function ${methodName}()
{
return $this->${relation}(${related});
}
`;
}
function insertMethodBeforeClosingBrace(source, method) {
const trimmed = method.trimEnd() + '\n';
if (source.includes(`function ${trimmed.match(/function\s+(\w+)/)?.[1] ?? ''}(`)) {
return source;
}
const lastBrace = source.lastIndexOf('}');
if (lastBrace === -1) {
throw new Error('Could not find closing class brace in model file.');
}
return `${source.slice(0, lastBrace)}${trimmed}${source.slice(lastBrace)}`;
}
export function registerGenerateEloquentRelationTool(server) {
server.registerTool('generate_eloquent_relation', {
title: 'Generate Eloquent Relation',
description: 'Add an Eloquent relationship method to a model (belongsTo, hasMany, etc.), optionally also adding the inverse relation on the related model.',
inputSchema: {
model: z.string().min(1).describe('Model class name, e.g. "Post".'),
relation: z.enum(RELATION_TYPES).describe('Eloquent relation type.'),
related: z.string().min(1).describe('Related model class name, e.g. "User".'),
method: z.string().optional().describe('Method name on the source model. Defaults from relation type.'),
inverse: z
.boolean()
.optional()
.default(false)
.describe('If true, also add a sensible inverse method on the related model.'),
},
}, async ({ model, relation, related, method, inverse }) => {
try {
const laravel = requireLaravel();
const modelName = pascalCase(model);
const relatedName = pascalCase(related);
const modelFile = findModelFile(laravel.root, modelName);
if (!modelFile) {
return errorResult(`Model file not found for "${modelName}" under app/Models or app/.`);
}
const defaultMethod = relation === 'belongsTo' || relation === 'hasOne' || relation === 'morphTo' || relation === 'morphOne'
? camelCase(relatedName)
: `${camelCase(relatedName)}s`;
const methodName = method?.trim() || defaultMethod;
const before = fs.readFileSync(modelFile, 'utf8');
const next = insertMethodBeforeClosingBrace(before, relationMethodSource(methodName, relation, relatedName));
fs.writeFileSync(modelFile, next);
const updated = [modelFile];
let inverseMethod = null;
let inverseFile = null;
if (inverse) {
const inverseMap = {
belongsTo: 'hasMany',
hasMany: 'belongsTo',
hasOne: 'belongsTo',
belongsToMany: 'belongsToMany',
morphMany: 'morphTo',
morphOne: 'morphTo',
};
const inverseRelation = inverseMap[relation];
if (inverseRelation) {
inverseFile = findModelFile(laravel.root, relatedName);
if (!inverseFile) {
return errorResult(`Added ${methodName}() on ${modelName}, but related model "${relatedName}" was not found for the inverse.`);
}
inverseMethod =
inverseRelation === 'belongsTo' || inverseRelation === 'hasOne' || inverseRelation === 'morphTo'
? camelCase(modelName)
: `${camelCase(modelName)}s`;
const relatedSource = fs.readFileSync(inverseFile, 'utf8');
fs.writeFileSync(inverseFile, insertMethodBeforeClosingBrace(relatedSource, relationMethodSource(inverseMethod, inverseRelation, modelName)));
updated.push(inverseFile);
}
}
return jsonResult({
model: modelName,
related: relatedName,
relation,
method: methodName,
inverseMethod,
files: updated,
});
}
catch (error) {
return errorResult(toErrorMessage(error));
}
});
}
//# sourceMappingURL=generateEloquentRelation.js.map