116 lines
5.0 KiB
JavaScript
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
|