153 lines
7.7 KiB
JavaScript
153 lines
7.7 KiB
JavaScript
import { z } from "zod";
|
|
import { withAuth, getSessionCredentials } from "../../auth/index.js";
|
|
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
|
|
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
|
|
import { table } from "console";
|
|
import { withAuthParams } from "../helpers/authSchema.js";
|
|
import { canAccessTable } from "../helpers/accessControl.js";
|
|
|
|
export function registerCreateOrUpdateRecordTool(server) {
|
|
server.tool(
|
|
"create_or_update_record",
|
|
`Crea o actualiza registros en una tabla. Antes de usar: consulta el schema con 'get_table_schema' (sin 'cms_'); si dudas del formato lee 'read_doc({ name: "11-quick-reference" })' o '06-hooks-and-cmsapi'.
|
|
|
|
Reglas clave: tablas sin prefijo 'cms_'; PK es 'num' (nunca 'id'); foreign keys con sufijo '_num'; uploads son arrays — NO los envíes en 'fields', sube después con 'upload_record_image'; fechas en formato YYYY-MM-DD HH:mm:ss; checkboxes como 1/0 (números).
|
|
|
|
Para tablas builder (e.g. 'apartados') al crear nuevo registro: incluye num:null, builder:"[]", controlador, precontrolador, breadcrumb, enlace. NUNCA modifiques 'enlace' ni 'controlador' de un registro existente — los stripeo automáticamente en updates.`,
|
|
withAuthParams({
|
|
tableName: z.string().describe("Nombre de la tabla sin prefijo 'cms_' (e.g. 'productos', 'apartados')"),
|
|
recordId: z.any().optional().describe("'num' del registro a actualizar. Omitir para crear nuevo. NO se usa cuando 'fields' es array."),
|
|
fields: z.any().describe("Objeto único o array de objetos para inserción batch. Ejemplo: { nombre: 'Producto 1' } o [{ nombre: 'A' }, { nombre: 'B' }]. Antes consulta el schema y, si dudas, lee 'read_doc({ name: \"11-quick-reference\" })'."),
|
|
tableSchema: z.any().describe("Schema de la tabla para validar tipos antes de enviar (opcional)."),
|
|
}),
|
|
{ readOnlyHint: false, destructiveHint: false },
|
|
withAuth(async ({ tableName, recordId, fields }, extra) => {
|
|
try {
|
|
// Validate required parameters
|
|
const validationError = validateRequired({ tableName, fields }, ['tableName', 'fields'], 'create_or_update_record');
|
|
if (validationError) return validationError;
|
|
|
|
// Check table access
|
|
const accessCheck = canAccessTable(tableName);
|
|
if (!accessCheck.allowed) {
|
|
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: accessCheck.error }) }], isError: true };
|
|
}
|
|
|
|
// if fields is string, try to parse as JSON
|
|
if (typeof fields === 'string') {
|
|
try {
|
|
fields = JSON.parse(fields);
|
|
} catch (e) {
|
|
return {
|
|
content: [{ type: "text", text: "Error: 'fields' parameter is a string but not valid JSON." }],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
// Determine if fields is array or single object
|
|
const isArray = Array.isArray(fields);
|
|
const recordsArray = isArray ? fields : [fields];
|
|
|
|
// Check if trying to update with array (not supported)
|
|
if (isArray && recordId) {
|
|
return {
|
|
content: [{ type: "text", text: "Error: Cannot use recordId when fields is an array. Use fields as array for batch insert only." }],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
// Protect critical fields during updates — these should never be changed by AI
|
|
const PROTECTED_UPDATE_FIELDS = ['enlace', 'controlador', 'precontrolador'];
|
|
if (recordId) {
|
|
// On update: strip protected fields silently
|
|
recordsArray.forEach(record => {
|
|
PROTECTED_UPDATE_FIELDS.forEach(f => {
|
|
if (f in record) delete record[f];
|
|
});
|
|
});
|
|
}
|
|
|
|
// Process enlace field for new records only
|
|
let processedRecords = recordsArray;
|
|
if (!recordId) {
|
|
processedRecords = recordsArray.map(record => {
|
|
let enlaceValue = record.enlace;
|
|
|
|
if (!enlaceValue) {
|
|
// Generate random enlace if not provided to ensure uniqueness
|
|
enlaceValue = '/' + Math.random().toString(36).substring(2, 10) + '/';
|
|
} else {
|
|
// Ensure format /.../
|
|
enlaceValue = String(enlaceValue);
|
|
if (!enlaceValue.startsWith('/')) enlaceValue = '/' + enlaceValue;
|
|
if (!enlaceValue.endsWith('/')) enlaceValue = enlaceValue + '/';
|
|
}
|
|
|
|
return { ...record, enlace: enlaceValue };
|
|
});
|
|
}
|
|
|
|
// Prepare payload for CMS API
|
|
const credentials = await getSessionCredentials(extra.sessionId);
|
|
const recordPayload = {
|
|
tableName: tableName,
|
|
records: processedRecords,
|
|
functions: [],
|
|
options: {}
|
|
};
|
|
|
|
// Determine action: insert for new records, update for existing
|
|
const isNewRecord = !recordId;
|
|
let response;
|
|
|
|
if (isNewRecord) {
|
|
// Insert new record(s)
|
|
response = await AcaiHttpClient.postCmsApi(
|
|
credentials,
|
|
'insert',
|
|
recordPayload,
|
|
credentials.token,
|
|
credentials.tokenHash
|
|
);
|
|
} else {
|
|
// Update existing record (only single record, not array)
|
|
response = await AcaiHttpClient.postCmsApi(
|
|
credentials,
|
|
'update',
|
|
{
|
|
...recordPayload,
|
|
where: `num = ${recordId}`
|
|
},
|
|
credentials.token,
|
|
credentials.tokenHash
|
|
);
|
|
}
|
|
|
|
// Check for API errors
|
|
const apiError = handleApiResponse(response.data, 'create_or_update_record');
|
|
if (apiError) return apiError;
|
|
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: JSON.stringify({
|
|
success: true,
|
|
message: isNewRecord
|
|
? `${isArray ? recordsArray.length : 1} record(s) created successfully`
|
|
: `Record ${recordId} updated successfully`,
|
|
tableName: tableName,
|
|
recordIds: response.data?.data || (recordId || 'new'),
|
|
recordsCount: isArray ? recordsArray.length : 1,
|
|
createdIds: response.data?.data,
|
|
suggestion: isNewRecord && !isArray ? `You can verify the record by fetching: ${credentials.web_url}${processedRecords[0].enlace}` : undefined
|
|
}, null, 2)
|
|
}],
|
|
};
|
|
} catch (error) {
|
|
return handleToolError(error, 'create_or_update_record', { tableName, recordId, isArray: Array.isArray(fields) });
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|