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) }); } }) ); }