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