gufi-cli 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp.js +1257 -135
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -122,6 +122,204 @@ async function developerRequest(path, options = {}, retry = true) {
|
|
|
122
122
|
return res.json();
|
|
123
123
|
}
|
|
124
124
|
// ════════════════════════════════════════════════════════════════════════════
|
|
125
|
+
// File Field Normalization
|
|
126
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
127
|
+
/**
|
|
128
|
+
* MIME types by extension
|
|
129
|
+
*/
|
|
130
|
+
const MIME_TYPES = {
|
|
131
|
+
// Images
|
|
132
|
+
png: "image/png",
|
|
133
|
+
jpg: "image/jpeg",
|
|
134
|
+
jpeg: "image/jpeg",
|
|
135
|
+
gif: "image/gif",
|
|
136
|
+
webp: "image/webp",
|
|
137
|
+
svg: "image/svg+xml",
|
|
138
|
+
// Documents
|
|
139
|
+
pdf: "application/pdf",
|
|
140
|
+
doc: "application/msword",
|
|
141
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
142
|
+
xls: "application/vnd.ms-excel",
|
|
143
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
144
|
+
// Other
|
|
145
|
+
json: "application/json",
|
|
146
|
+
xml: "application/xml",
|
|
147
|
+
zip: "application/zip",
|
|
148
|
+
txt: "text/plain",
|
|
149
|
+
csv: "text/csv",
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Get MIME type from filename extension
|
|
153
|
+
*/
|
|
154
|
+
function getMimeFromFilename(filename) {
|
|
155
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
156
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Generate unique file ID
|
|
160
|
+
*/
|
|
161
|
+
function generateFileId() {
|
|
162
|
+
return `file_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Normalize a single file object - ensures all required fields are present
|
|
166
|
+
* Input: { url, name } (minimum required)
|
|
167
|
+
* Output: { id, url, name, mime, uploaded_at }
|
|
168
|
+
*/
|
|
169
|
+
function normalizeFileObject(file) {
|
|
170
|
+
// Validate required fields
|
|
171
|
+
if (!file.url || typeof file.url !== "string") {
|
|
172
|
+
throw new Error("File field error: 'url' is required and must be a string");
|
|
173
|
+
}
|
|
174
|
+
if (!file.name || typeof file.name !== "string") {
|
|
175
|
+
throw new Error("File field error: 'name' is required and must be a string");
|
|
176
|
+
}
|
|
177
|
+
// Validate url format (must be company_X/... path or full URL)
|
|
178
|
+
const isValidPath = file.url.startsWith("company_") || file.url.startsWith("http");
|
|
179
|
+
if (!isValidPath) {
|
|
180
|
+
throw new Error(`File field error: 'url' must start with 'company_X/' or be a full URL. Got: ${file.url}`);
|
|
181
|
+
}
|
|
182
|
+
// Return normalized object with auto-generated fields
|
|
183
|
+
return {
|
|
184
|
+
id: file.id || generateFileId(),
|
|
185
|
+
url: file.url,
|
|
186
|
+
name: file.name,
|
|
187
|
+
mime: file.mime || getMimeFromFilename(file.name),
|
|
188
|
+
uploaded_at: file.uploaded_at || new Date().toISOString(),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Normalize file fields in data object
|
|
193
|
+
* Detects file fields (arrays with objects containing 'url') and normalizes them
|
|
194
|
+
*/
|
|
195
|
+
function normalizeFileFields(data) {
|
|
196
|
+
if (!data || typeof data !== "object")
|
|
197
|
+
return data;
|
|
198
|
+
const normalized = { ...data };
|
|
199
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
200
|
+
// Check if it's a file field (array with objects containing 'url')
|
|
201
|
+
if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
202
|
+
// Check if first item has 'url' property (indicates file field)
|
|
203
|
+
if ("url" in value[0]) {
|
|
204
|
+
normalized[key] = value.map(normalizeFileObject);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return normalized;
|
|
209
|
+
}
|
|
210
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
211
|
+
// Relation Field Validation
|
|
212
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
213
|
+
/**
|
|
214
|
+
* Extract entity ID from table name (e.g., "m617_t20413" → "20413")
|
|
215
|
+
*/
|
|
216
|
+
function extractEntityIdFromTable(tableName) {
|
|
217
|
+
const match = tableName.match(/m\d+_t(\d+)/);
|
|
218
|
+
return match ? match[1] : null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Cache for entity schemas to avoid repeated API calls
|
|
222
|
+
*/
|
|
223
|
+
const entitySchemaCache = new Map();
|
|
224
|
+
const CACHE_TTL = 60000; // 1 minute
|
|
225
|
+
/**
|
|
226
|
+
* Get entity schema with caching
|
|
227
|
+
*/
|
|
228
|
+
async function getEntitySchema(entityId, companyId) {
|
|
229
|
+
const cacheKey = `${companyId}:${entityId}`;
|
|
230
|
+
const cached = entitySchemaCache.get(cacheKey);
|
|
231
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
232
|
+
return cached.fields;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const data = await apiRequest(`/api/schema/entities/${entityId}`, {
|
|
236
|
+
headers: { "X-Company-ID": companyId },
|
|
237
|
+
}, companyId);
|
|
238
|
+
const fields = data.fields || [];
|
|
239
|
+
entitySchemaCache.set(cacheKey, { fields, timestamp: Date.now() });
|
|
240
|
+
return fields;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Resolve a reference path (e.g., "ecommerce.clientes") to a table name
|
|
248
|
+
*/
|
|
249
|
+
async function resolveRefToTable(ref, companyId) {
|
|
250
|
+
try {
|
|
251
|
+
const schemaData = await apiRequest("/api/company/schema", {
|
|
252
|
+
headers: { "X-Company-ID": companyId },
|
|
253
|
+
}, companyId);
|
|
254
|
+
const modules = schemaData.modules || schemaData.data?.modules || [];
|
|
255
|
+
const [moduleName, entityName] = ref.split(".");
|
|
256
|
+
for (const mod of modules) {
|
|
257
|
+
if (mod.name !== moduleName)
|
|
258
|
+
continue;
|
|
259
|
+
for (const sub of mod.submodules || []) {
|
|
260
|
+
for (const ent of sub.entities || []) {
|
|
261
|
+
if (ent.name === entityName) {
|
|
262
|
+
return ent.ui?.tableName || `m${ent.ui?.moduleId}_t${ent.ui?.entityId}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Ignore errors, validation is best-effort
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Validate relation fields in data - checks if referenced IDs exist
|
|
275
|
+
* Returns { valid: true } or { valid: false, errors: [...] }
|
|
276
|
+
*/
|
|
277
|
+
async function validateRelationFields(tableName, companyId, data) {
|
|
278
|
+
const errors = [];
|
|
279
|
+
// Extract entity ID from table name
|
|
280
|
+
const entityId = extractEntityIdFromTable(tableName);
|
|
281
|
+
if (!entityId) {
|
|
282
|
+
return { valid: true, errors: [] }; // Can't validate without entity ID
|
|
283
|
+
}
|
|
284
|
+
// Get entity schema to find relation fields
|
|
285
|
+
const fields = await getEntitySchema(entityId, companyId);
|
|
286
|
+
if (!fields.length) {
|
|
287
|
+
return { valid: true, errors: [] }; // Can't validate without schema
|
|
288
|
+
}
|
|
289
|
+
// Find relation fields
|
|
290
|
+
const relationFields = fields.filter((f) => f.type === "relation" && f.settings?.ref);
|
|
291
|
+
for (const field of relationFields) {
|
|
292
|
+
const fieldName = field.name;
|
|
293
|
+
const value = data[fieldName];
|
|
294
|
+
// Skip if no value provided
|
|
295
|
+
if (value === undefined || value === null || value === "")
|
|
296
|
+
continue;
|
|
297
|
+
// Get the ID to validate
|
|
298
|
+
const refId = typeof value === "object" ? value.id : value;
|
|
299
|
+
if (!refId || typeof refId !== "number")
|
|
300
|
+
continue;
|
|
301
|
+
// Resolve the reference to a table name
|
|
302
|
+
const refTable = await resolveRefToTable(field.settings.ref, companyId);
|
|
303
|
+
if (!refTable)
|
|
304
|
+
continue;
|
|
305
|
+
// Check if the ID exists in the referenced table
|
|
306
|
+
try {
|
|
307
|
+
const result = await apiRequest(`/api/tables/${refTable}/getOne`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
body: JSON.stringify({ id: refId }),
|
|
310
|
+
}, companyId);
|
|
311
|
+
if (!result || !result.data) {
|
|
312
|
+
errors.push(`Relation '${fieldName}': ID ${refId} does not exist in ${field.settings.ref}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
// If we get an error, the ID probably doesn't exist
|
|
317
|
+
errors.push(`Relation '${fieldName}': ID ${refId} not found in ${field.settings.ref} (${e.message || 'not found'})`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { valid: errors.length === 0, errors };
|
|
321
|
+
}
|
|
322
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
125
323
|
// Tool Definitions - Descriptions loaded from docs/mcp/tool-descriptions.json
|
|
126
324
|
// ════════════════════════════════════════════════════════════════════════════
|
|
127
325
|
// Load tool descriptions from centralized documentation
|
|
@@ -172,8 +370,35 @@ const TOOLS = [
|
|
|
172
370
|
type: "object",
|
|
173
371
|
properties: {
|
|
174
372
|
company_id: { type: "string", description: "Company ID (optional if user has only one company)" },
|
|
175
|
-
|
|
373
|
+
format: { type: "string", description: "Output format: 'flat' (text, default) or 'json' (structured)" },
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "gufi_schema_modify",
|
|
379
|
+
description: getDesc("gufi_schema_modify"),
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: "object",
|
|
382
|
+
properties: {
|
|
383
|
+
company_id: { type: "string", description: "Company ID (optional if user has only one company)" },
|
|
384
|
+
preview: { type: "boolean", description: "If true, show what would be done without executing (dry run)" },
|
|
385
|
+
operations: {
|
|
386
|
+
type: "array",
|
|
387
|
+
description: "Array of operations to execute",
|
|
388
|
+
items: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
op: { type: "string", description: "Operation: add_field, update_field, remove_field, add_entity, update_entity, remove_entity" },
|
|
392
|
+
entity: { type: "string", description: "Entity reference: id, name, or 'module.entity'" },
|
|
393
|
+
module: { type: "string", description: "Module reference (for add_entity)" },
|
|
394
|
+
field: { type: "object", description: "Field definition (for add_field)" },
|
|
395
|
+
field_name: { type: "string", description: "Field name (for update_field, remove_field)" },
|
|
396
|
+
changes: { type: "object", description: "Changes to apply (for update_*)" },
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
176
400
|
},
|
|
401
|
+
required: ["operations"],
|
|
177
402
|
},
|
|
178
403
|
},
|
|
179
404
|
{
|
|
@@ -226,33 +451,47 @@ const TOOLS = [
|
|
|
226
451
|
inputSchema: {
|
|
227
452
|
type: "object",
|
|
228
453
|
properties: {
|
|
229
|
-
module_id: { type: "string", description: "Module ID
|
|
454
|
+
module_id: { type: "string", description: "Module ID" },
|
|
455
|
+
company_id: { type: "string", description: "Company ID (required - module IDs are local per company)" },
|
|
230
456
|
},
|
|
231
|
-
required: ["module_id"],
|
|
457
|
+
required: ["module_id", "company_id"],
|
|
232
458
|
},
|
|
233
459
|
},
|
|
234
460
|
{
|
|
235
|
-
name: "
|
|
236
|
-
description: getDesc("
|
|
461
|
+
name: "gufi_entity",
|
|
462
|
+
description: getDesc("gufi_entity"),
|
|
237
463
|
inputSchema: {
|
|
238
464
|
type: "object",
|
|
239
465
|
properties: {
|
|
240
|
-
|
|
241
|
-
|
|
466
|
+
entity_id: { type: "string", description: "Entity ID" },
|
|
467
|
+
company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
|
|
242
468
|
},
|
|
243
|
-
required: ["
|
|
469
|
+
required: ["entity_id", "company_id"],
|
|
244
470
|
},
|
|
245
471
|
},
|
|
246
472
|
{
|
|
247
|
-
name: "
|
|
248
|
-
description: getDesc("
|
|
473
|
+
name: "gufi_field",
|
|
474
|
+
description: getDesc("gufi_field"),
|
|
249
475
|
inputSchema: {
|
|
250
476
|
type: "object",
|
|
251
477
|
properties: {
|
|
252
|
-
|
|
253
|
-
|
|
478
|
+
entity: { type: "string", description: "Entity reference: id, name, or 'module.entity'" },
|
|
479
|
+
field: { type: "string", description: "Field name" },
|
|
480
|
+
company_id: { type: "string", description: "Company ID (required)" },
|
|
254
481
|
},
|
|
255
|
-
required: ["
|
|
482
|
+
required: ["entity", "field", "company_id"],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "gufi_relations",
|
|
487
|
+
description: getDesc("gufi_relations"),
|
|
488
|
+
inputSchema: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: {
|
|
491
|
+
module_id: { type: "string", description: "Module ID (optional - if omitted, shows all relations in company)" },
|
|
492
|
+
company_id: { type: "string", description: "Company ID (required)" },
|
|
493
|
+
},
|
|
494
|
+
required: ["company_id"],
|
|
256
495
|
},
|
|
257
496
|
},
|
|
258
497
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -276,8 +515,9 @@ const TOOLS = [
|
|
|
276
515
|
type: "object",
|
|
277
516
|
properties: {
|
|
278
517
|
automation_id: { type: "string", description: "Automation script ID" },
|
|
518
|
+
company_id: { type: "string", description: "Company ID (required - automation IDs are local per company)" },
|
|
279
519
|
},
|
|
280
|
-
required: ["automation_id"],
|
|
520
|
+
required: ["automation_id", "company_id"],
|
|
281
521
|
},
|
|
282
522
|
},
|
|
283
523
|
{
|
|
@@ -301,8 +541,9 @@ const TOOLS = [
|
|
|
301
541
|
type: "object",
|
|
302
542
|
properties: {
|
|
303
543
|
entity_id: { type: "string", description: "Entity ID (from module schema)" },
|
|
544
|
+
company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
|
|
304
545
|
},
|
|
305
|
-
required: ["entity_id"],
|
|
546
|
+
required: ["entity_id", "company_id"],
|
|
306
547
|
},
|
|
307
548
|
},
|
|
308
549
|
{
|
|
@@ -312,12 +553,13 @@ const TOOLS = [
|
|
|
312
553
|
type: "object",
|
|
313
554
|
properties: {
|
|
314
555
|
entity_id: { type: "string", description: "Entity ID" },
|
|
556
|
+
company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
|
|
315
557
|
automations: {
|
|
316
558
|
type: "object",
|
|
317
559
|
description: "Automation configuration with on_create, on_update, on_delete, on_click arrays",
|
|
318
560
|
},
|
|
319
561
|
},
|
|
320
|
-
required: ["entity_id", "automations"],
|
|
562
|
+
required: ["entity_id", "company_id", "automations"],
|
|
321
563
|
},
|
|
322
564
|
},
|
|
323
565
|
{
|
|
@@ -336,21 +578,41 @@ const TOOLS = [
|
|
|
336
578
|
// ─────────────────────────────────────────────────────────────────────────
|
|
337
579
|
// Data (CRUD)
|
|
338
580
|
// ─────────────────────────────────────────────────────────────────────────
|
|
581
|
+
{
|
|
582
|
+
name: "gufi_data",
|
|
583
|
+
description: getDesc("gufi_data"),
|
|
584
|
+
inputSchema: {
|
|
585
|
+
type: "object",
|
|
586
|
+
properties: {
|
|
587
|
+
action: { type: "string", description: "Action: list, get, create, update, delete" },
|
|
588
|
+
table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
|
|
589
|
+
company_id: { type: "string", description: "Company ID (required)" },
|
|
590
|
+
id: { type: "number", description: "Row ID (for get, update, delete)" },
|
|
591
|
+
data: { type: "object", description: "Row data (for create, update)" },
|
|
592
|
+
limit: { type: "number", description: "Number of rows for list (default 20)" },
|
|
593
|
+
offset: { type: "number", description: "Offset for pagination (default 0)" },
|
|
594
|
+
sort: { type: "string", description: "Field to sort by (default: id)" },
|
|
595
|
+
order: { type: "string", description: "ASC or DESC (default: DESC)" },
|
|
596
|
+
filter: { type: "string", description: "Filter expression (field=value)" },
|
|
597
|
+
},
|
|
598
|
+
required: ["action", "table", "company_id"],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
339
601
|
{
|
|
340
602
|
name: "gufi_rows",
|
|
341
603
|
description: getDesc("gufi_rows"),
|
|
342
604
|
inputSchema: {
|
|
343
605
|
type: "object",
|
|
344
606
|
properties: {
|
|
345
|
-
table: { type: "string", description: "Table name (physical ID like m360_t16192
|
|
346
|
-
company_id: { type: "string", description: "Company ID (required
|
|
607
|
+
table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
|
|
608
|
+
company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
|
|
347
609
|
limit: { type: "number", description: "Number of rows (default 20)" },
|
|
348
610
|
offset: { type: "number", description: "Offset for pagination (default 0)" },
|
|
349
611
|
sort: { type: "string", description: "Field to sort by (default: id)" },
|
|
350
612
|
order: { type: "string", description: "ASC or DESC (default: DESC)" },
|
|
351
613
|
filter: { type: "string", description: "Filter expression (field=value)" },
|
|
352
614
|
},
|
|
353
|
-
required: ["table"],
|
|
615
|
+
required: ["table", "company_id"],
|
|
354
616
|
},
|
|
355
617
|
},
|
|
356
618
|
{
|
|
@@ -360,10 +622,10 @@ const TOOLS = [
|
|
|
360
622
|
type: "object",
|
|
361
623
|
properties: {
|
|
362
624
|
table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
|
|
363
|
-
company_id: { type: "string", description: "Company ID (required
|
|
625
|
+
company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
|
|
364
626
|
id: { type: "number", description: "Row ID" },
|
|
365
627
|
},
|
|
366
|
-
required: ["table", "id"],
|
|
628
|
+
required: ["table", "company_id", "id"],
|
|
367
629
|
},
|
|
368
630
|
},
|
|
369
631
|
{
|
|
@@ -373,10 +635,10 @@ const TOOLS = [
|
|
|
373
635
|
type: "object",
|
|
374
636
|
properties: {
|
|
375
637
|
table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
|
|
376
|
-
company_id: { type: "string", description: "Company ID (required
|
|
638
|
+
company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
|
|
377
639
|
data: { type: "object", description: "Row data as key-value pairs" },
|
|
378
640
|
},
|
|
379
|
-
required: ["table", "data"],
|
|
641
|
+
required: ["table", "company_id", "data"],
|
|
380
642
|
},
|
|
381
643
|
},
|
|
382
644
|
{
|
|
@@ -386,11 +648,11 @@ const TOOLS = [
|
|
|
386
648
|
type: "object",
|
|
387
649
|
properties: {
|
|
388
650
|
table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
|
|
389
|
-
company_id: { type: "string", description: "Company ID (required
|
|
651
|
+
company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
|
|
390
652
|
id: { type: "number", description: "Row ID" },
|
|
391
653
|
data: { type: "object", description: "Fields to update" },
|
|
392
654
|
},
|
|
393
|
-
required: ["table", "id", "data"],
|
|
655
|
+
required: ["table", "company_id", "id", "data"],
|
|
394
656
|
},
|
|
395
657
|
},
|
|
396
658
|
{
|
|
@@ -400,10 +662,10 @@ const TOOLS = [
|
|
|
400
662
|
type: "object",
|
|
401
663
|
properties: {
|
|
402
664
|
table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
|
|
403
|
-
company_id: { type: "string", description: "Company ID (required
|
|
665
|
+
company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
|
|
404
666
|
id: { type: "number", description: "Row ID" },
|
|
405
667
|
},
|
|
406
|
-
required: ["table", "id"],
|
|
668
|
+
required: ["table", "company_id", "id"],
|
|
407
669
|
},
|
|
408
670
|
},
|
|
409
671
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -427,8 +689,9 @@ const TOOLS = [
|
|
|
427
689
|
properties: {
|
|
428
690
|
key: { type: "string", description: "Variable name (uppercase recommended)" },
|
|
429
691
|
value: { type: "string", description: "Variable value" },
|
|
692
|
+
company_id: { type: "string", description: "Company ID (required)" },
|
|
430
693
|
},
|
|
431
|
-
required: ["key", "value"],
|
|
694
|
+
required: ["key", "value", "company_id"],
|
|
432
695
|
},
|
|
433
696
|
},
|
|
434
697
|
{
|
|
@@ -438,8 +701,9 @@ const TOOLS = [
|
|
|
438
701
|
type: "object",
|
|
439
702
|
properties: {
|
|
440
703
|
key: { type: "string", description: "Variable name to delete" },
|
|
704
|
+
company_id: { type: "string", description: "Company ID (required)" },
|
|
441
705
|
},
|
|
442
|
-
required: ["key"],
|
|
706
|
+
required: ["key", "company_id"],
|
|
443
707
|
},
|
|
444
708
|
},
|
|
445
709
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -503,6 +767,20 @@ const TOOLS = [
|
|
|
503
767
|
required: ["view_id"],
|
|
504
768
|
},
|
|
505
769
|
},
|
|
770
|
+
{
|
|
771
|
+
name: "gufi_view_create",
|
|
772
|
+
description: getDesc("gufi_view_create"),
|
|
773
|
+
inputSchema: {
|
|
774
|
+
type: "object",
|
|
775
|
+
properties: {
|
|
776
|
+
name: { type: "string", description: "View name (e.g., 'Sales Dashboard')" },
|
|
777
|
+
template: { type: "string", description: "Template: 'dashboard', 'table', 'form', or 'blank'" },
|
|
778
|
+
package_id: { type: "string", description: "Package ID to add the view to (optional)" },
|
|
779
|
+
description: { type: "string", description: "View description (optional)" },
|
|
780
|
+
},
|
|
781
|
+
required: ["name", "template"],
|
|
782
|
+
},
|
|
783
|
+
},
|
|
506
784
|
// ─────────────────────────────────────────────────────────────────────────
|
|
507
785
|
// Packages (Marketplace Distribution)
|
|
508
786
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -691,6 +969,22 @@ const TOOLS = [
|
|
|
691
969
|
required: ["package_id"],
|
|
692
970
|
},
|
|
693
971
|
},
|
|
972
|
+
{
|
|
973
|
+
name: "gufi_link_view",
|
|
974
|
+
description: getDesc("gufi_link_view"),
|
|
975
|
+
inputSchema: {
|
|
976
|
+
type: "object",
|
|
977
|
+
properties: {
|
|
978
|
+
package_id: { type: "string", description: "Package ID" },
|
|
979
|
+
module_id: { type: "string", description: "Package module ID (from gufi_package)" },
|
|
980
|
+
submodule_name: { type: "string", description: "Submodule name to add the view to" },
|
|
981
|
+
view_id: { type: "number", description: "Marketplace view ID to link" },
|
|
982
|
+
label: { type: "string", description: "Display name for the view in the menu" },
|
|
983
|
+
icon: { type: "string", description: "Lucide icon name (e.g., 'BarChart3', 'Package')" },
|
|
984
|
+
},
|
|
985
|
+
required: ["package_id", "module_id", "submodule_name", "view_id", "label"],
|
|
986
|
+
},
|
|
987
|
+
},
|
|
694
988
|
];
|
|
695
989
|
// ════════════════════════════════════════════════════════════════════════════
|
|
696
990
|
// Tool Handlers
|
|
@@ -966,54 +1260,95 @@ const toolHandlers = {
|
|
|
966
1260
|
}
|
|
967
1261
|
},
|
|
968
1262
|
async gufi_schema(params) {
|
|
969
|
-
// Use /api/
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
for (const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1263
|
+
// Use the new /api/schema/readable endpoint for FLAT format (default)
|
|
1264
|
+
// This returns human-readable text optimized for Claude comprehension
|
|
1265
|
+
if (params.format === "json") {
|
|
1266
|
+
// JSON format for programmatic use
|
|
1267
|
+
const data = await apiRequest("/api/company/schema", {
|
|
1268
|
+
headers: params.company_id ? { "X-Company-ID": params.company_id } : {},
|
|
1269
|
+
}, params.company_id);
|
|
1270
|
+
const modulesRaw = data.modules || data.data?.modules || [];
|
|
1271
|
+
const modules = [];
|
|
1272
|
+
for (const mod of modulesRaw) {
|
|
1273
|
+
let moduleId;
|
|
1274
|
+
const entities = [];
|
|
1275
|
+
for (const sub of mod.submodules || []) {
|
|
1276
|
+
for (const ent of sub.entities || []) {
|
|
1277
|
+
if (ent.ui?.moduleId)
|
|
1278
|
+
moduleId = ent.ui.moduleId;
|
|
1279
|
+
const entityId = ent.ui?.entityId || ent.pk_id || ent.id;
|
|
1280
|
+
entities.push({
|
|
1281
|
+
id: entityId,
|
|
1282
|
+
name: ent.name,
|
|
1283
|
+
label: ent.label,
|
|
1284
|
+
table: moduleId && entityId ? `m${moduleId}_t${entityId}` : null,
|
|
1285
|
+
fields: (ent.fields || []).map((f) => ({
|
|
1286
|
+
name: f.name,
|
|
1287
|
+
type: f.type,
|
|
1288
|
+
label: f.label,
|
|
1289
|
+
required: f.required,
|
|
1290
|
+
})),
|
|
1291
|
+
});
|
|
985
1292
|
}
|
|
986
1293
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
for (const ent of sub.entities || []) {
|
|
993
|
-
const entityId = ent.ui?.entityId || ent.pk_id || ent.id;
|
|
994
|
-
entities.push({
|
|
995
|
-
id: entityId,
|
|
996
|
-
name: ent.name,
|
|
997
|
-
label: ent.label,
|
|
998
|
-
table: moduleId && entityId ? `m${moduleId}_t${entityId}` : null,
|
|
999
|
-
fields: (ent.fields || []).map((f) => ({
|
|
1000
|
-
name: f.name,
|
|
1001
|
-
type: f.type,
|
|
1002
|
-
label: f.label,
|
|
1003
|
-
})),
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
if (params.module_id && String(moduleId) !== params.module_id) {
|
|
1008
|
-
continue;
|
|
1294
|
+
modules.push({
|
|
1295
|
+
id: moduleId,
|
|
1296
|
+
name: mod.name || mod.displayName,
|
|
1297
|
+
entities,
|
|
1298
|
+
});
|
|
1009
1299
|
}
|
|
1010
|
-
modules
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1300
|
+
return { format: "json", modules };
|
|
1301
|
+
}
|
|
1302
|
+
// Default: FLAT text format optimized for Claude comprehension
|
|
1303
|
+
const url = `${getApiUrl()}/api/schema/readable`;
|
|
1304
|
+
let token = getToken();
|
|
1305
|
+
if (!token) {
|
|
1306
|
+
token = await autoLogin();
|
|
1307
|
+
if (!token)
|
|
1308
|
+
throw new Error("Not logged in. Run: gufi login");
|
|
1309
|
+
}
|
|
1310
|
+
const headers = {
|
|
1311
|
+
Authorization: `Bearer ${token}`,
|
|
1312
|
+
"X-Client": "mcp",
|
|
1313
|
+
};
|
|
1314
|
+
if (params.company_id) {
|
|
1315
|
+
headers["X-Company-ID"] = params.company_id;
|
|
1316
|
+
}
|
|
1317
|
+
const response = await fetch(url, { headers });
|
|
1318
|
+
if (!response.ok) {
|
|
1319
|
+
const text = await response.text();
|
|
1320
|
+
throw new Error(`API Error ${response.status}: ${text}`);
|
|
1015
1321
|
}
|
|
1016
|
-
|
|
1322
|
+
// Return as plain text for maximum readability
|
|
1323
|
+
const schema = await response.text();
|
|
1324
|
+
return { format: "flat", schema };
|
|
1325
|
+
},
|
|
1326
|
+
async gufi_schema_modify(params) {
|
|
1327
|
+
// Use the new /api/schema/operations endpoint for semantic operations
|
|
1328
|
+
const result = await apiRequest("/api/schema/operations", {
|
|
1329
|
+
method: "POST",
|
|
1330
|
+
body: JSON.stringify({
|
|
1331
|
+
operations: params.operations,
|
|
1332
|
+
preview: params.preview || false,
|
|
1333
|
+
}),
|
|
1334
|
+
}, params.company_id);
|
|
1335
|
+
if (params.preview) {
|
|
1336
|
+
return {
|
|
1337
|
+
preview: true,
|
|
1338
|
+
operations_count: params.operations.length,
|
|
1339
|
+
would_execute: result.preview || result.results?.map((r) => ({
|
|
1340
|
+
op: r.op,
|
|
1341
|
+
description: r.message,
|
|
1342
|
+
sql: r.sql || null,
|
|
1343
|
+
})),
|
|
1344
|
+
hint: "Set preview: false to execute these operations",
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
success: result.success,
|
|
1349
|
+
results: result.results,
|
|
1350
|
+
summary: result.results?.map((r) => r.message).join("; "),
|
|
1351
|
+
};
|
|
1017
1352
|
},
|
|
1018
1353
|
async gufi_docs(params) {
|
|
1019
1354
|
// Map topics to file names
|
|
@@ -1144,49 +1479,201 @@ const toolHandlers = {
|
|
|
1144
1479
|
};
|
|
1145
1480
|
},
|
|
1146
1481
|
async gufi_module(params) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1482
|
+
// 💜 Use relational endpoint /api/schema/modules/:id
|
|
1483
|
+
const data = await apiRequest(`/api/schema/modules/${params.module_id}`, {
|
|
1484
|
+
headers: { "X-Company-ID": params.company_id },
|
|
1485
|
+
}, params.company_id);
|
|
1486
|
+
const { module, submodules, entities } = data;
|
|
1487
|
+
if (!module) {
|
|
1488
|
+
throw new Error(`Module ${params.module_id} not found in company ${params.company_id}`);
|
|
1150
1489
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1490
|
+
// Build submodules with entities (lightweight, no field definitions)
|
|
1491
|
+
const submodulesWithEntities = submodules.map((sub) => {
|
|
1492
|
+
const subEntities = entities.filter((e) => e.submodule_id === sub.id);
|
|
1493
|
+
return {
|
|
1494
|
+
id: sub.id,
|
|
1495
|
+
name: sub.name,
|
|
1496
|
+
label: sub.label,
|
|
1497
|
+
position: sub.position,
|
|
1498
|
+
entities: subEntities.map((e) => ({
|
|
1499
|
+
id: e.id,
|
|
1500
|
+
name: e.name,
|
|
1501
|
+
label: e.label,
|
|
1502
|
+
kind: e.kind,
|
|
1503
|
+
tableName: `m${module.id}_t${e.id}`,
|
|
1504
|
+
fieldCount: parseInt(e.field_count) || 0,
|
|
1505
|
+
// Include view_spec if present
|
|
1506
|
+
...(e.view_spec && { view_spec: e.view_spec }),
|
|
1507
|
+
})),
|
|
1508
|
+
};
|
|
1509
|
+
});
|
|
1510
|
+
// Extract relations from entities for quick reference
|
|
1511
|
+
const relations = [];
|
|
1512
|
+
// Note: Full field details require gufi_entity call
|
|
1160
1513
|
return {
|
|
1161
|
-
module_id:
|
|
1162
|
-
company_id:
|
|
1163
|
-
|
|
1514
|
+
module_id: params.module_id,
|
|
1515
|
+
company_id: params.company_id,
|
|
1516
|
+
module: {
|
|
1517
|
+
id: module.id,
|
|
1164
1518
|
name: module.name,
|
|
1165
|
-
|
|
1519
|
+
display_name: module.display_name,
|
|
1166
1520
|
icon: module.icon,
|
|
1167
|
-
|
|
1521
|
+
position: module.position,
|
|
1168
1522
|
},
|
|
1523
|
+
submodules: submodulesWithEntities,
|
|
1524
|
+
entityCount: entities.length,
|
|
1525
|
+
hint: "Use gufi_entity({ entity_id, company_id }) to see fields and relations for a specific entity",
|
|
1169
1526
|
};
|
|
1170
1527
|
},
|
|
1171
|
-
async
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1528
|
+
async gufi_entity(params) {
|
|
1529
|
+
// 💜 Fetch entity with all fields from relational endpoint
|
|
1530
|
+
const data = await apiRequest(`/api/schema/entities/${params.entity_id}`, {
|
|
1531
|
+
headers: { "X-Company-ID": params.company_id },
|
|
1532
|
+
}, params.company_id);
|
|
1533
|
+
const { entity, fields } = data;
|
|
1534
|
+
if (!entity) {
|
|
1535
|
+
throw new Error(`Entity ${params.entity_id} not found in company ${params.company_id}`);
|
|
1175
1536
|
}
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1537
|
+
const tableName = `m${entity.module_id}_t${entity.id}`;
|
|
1538
|
+
// Extract relations for quick reference
|
|
1539
|
+
const relations = fields
|
|
1540
|
+
.filter((f) => f.type === "relation" && f.settings?.ref)
|
|
1541
|
+
.map((f) => ({
|
|
1542
|
+
field: f.name,
|
|
1543
|
+
ref: f.settings.ref,
|
|
1544
|
+
display: f.settings.display || "id",
|
|
1545
|
+
}));
|
|
1546
|
+
// Format fields for readability
|
|
1547
|
+
const formattedFields = fields.map((f) => {
|
|
1548
|
+
const constraints = [];
|
|
1549
|
+
if (f.required)
|
|
1550
|
+
constraints.push("required");
|
|
1551
|
+
if (f.settings?.unique)
|
|
1552
|
+
constraints.push("unique");
|
|
1553
|
+
if (f.settings?.ref)
|
|
1554
|
+
constraints.push(`→ ${f.settings.ref}`);
|
|
1555
|
+
if (f.settings?.options?.length > 0) {
|
|
1556
|
+
const opts = f.settings.options.slice(0, 5).map((o) => o.value || o);
|
|
1557
|
+
constraints.push(`[${opts.join("|")}${f.settings.options.length > 5 ? "..." : ""}]`);
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
name: f.name,
|
|
1561
|
+
type: f.type,
|
|
1562
|
+
label: f.label,
|
|
1563
|
+
...(constraints.length > 0 && { constraints: constraints.join(", ") }),
|
|
1564
|
+
...(f.settings && Object.keys(f.settings).length > 0 && { settings: f.settings }),
|
|
1565
|
+
};
|
|
1180
1566
|
});
|
|
1181
|
-
return {
|
|
1567
|
+
return {
|
|
1568
|
+
entity_id: params.entity_id,
|
|
1569
|
+
company_id: params.company_id,
|
|
1570
|
+
entity: {
|
|
1571
|
+
id: entity.id,
|
|
1572
|
+
name: entity.name,
|
|
1573
|
+
label: entity.label,
|
|
1574
|
+
kind: entity.kind,
|
|
1575
|
+
tableName,
|
|
1576
|
+
moduleName: entity.module_name,
|
|
1577
|
+
submoduleName: entity.submodule_name,
|
|
1578
|
+
...(entity.view_spec && { view_spec: entity.view_spec }),
|
|
1579
|
+
},
|
|
1580
|
+
fields: formattedFields,
|
|
1581
|
+
relations,
|
|
1582
|
+
fieldCount: fields.length,
|
|
1583
|
+
hint: "Use gufi_schema_modify to add/update/remove fields",
|
|
1584
|
+
};
|
|
1182
1585
|
},
|
|
1183
|
-
async
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1586
|
+
async gufi_field(params) {
|
|
1587
|
+
// 💜 Get a single field's full details
|
|
1588
|
+
// First resolve entity to ID if needed
|
|
1589
|
+
const schemaData = await apiRequest("/api/company/schema", {
|
|
1187
1590
|
headers: { "X-Company-ID": params.company_id },
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1591
|
+
}, params.company_id);
|
|
1592
|
+
const modules = schemaData.modules || schemaData.data?.modules || [];
|
|
1593
|
+
let targetEntity = null;
|
|
1594
|
+
let targetField = null;
|
|
1595
|
+
// Search for entity by id, name, or module.entity path
|
|
1596
|
+
const entityRef = params.entity;
|
|
1597
|
+
const [modulePart, entityPart] = entityRef.includes(".") ? entityRef.split(".") : [null, entityRef];
|
|
1598
|
+
for (const mod of modules) {
|
|
1599
|
+
for (const sub of mod.submodules || []) {
|
|
1600
|
+
for (const ent of sub.entities || []) {
|
|
1601
|
+
const matchesId = String(ent.ui?.entityId || ent.id) === entityRef;
|
|
1602
|
+
const matchesName = ent.name === (entityPart || entityRef);
|
|
1603
|
+
const matchesPath = modulePart && mod.name === modulePart && ent.name === entityPart;
|
|
1604
|
+
if (matchesId || matchesName || matchesPath) {
|
|
1605
|
+
targetEntity = { ...ent, moduleName: mod.name, submoduleName: sub.name };
|
|
1606
|
+
targetField = (ent.fields || []).find((f) => f.name === params.field);
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (targetEntity)
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
if (targetEntity)
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
if (!targetEntity) {
|
|
1617
|
+
throw new Error(`Entity "${params.entity}" not found in company ${params.company_id}`);
|
|
1618
|
+
}
|
|
1619
|
+
if (!targetField) {
|
|
1620
|
+
const availableFields = (targetEntity.fields || []).map((f) => f.name).slice(0, 10);
|
|
1621
|
+
throw new Error(`Field "${params.field}" not found in entity "${targetEntity.name}". Available: ${availableFields.join(", ")}${availableFields.length >= 10 ? "..." : ""}`);
|
|
1622
|
+
}
|
|
1623
|
+
return {
|
|
1624
|
+
entity: targetEntity.name,
|
|
1625
|
+
field: {
|
|
1626
|
+
name: targetField.name,
|
|
1627
|
+
type: targetField.type,
|
|
1628
|
+
label: targetField.label,
|
|
1629
|
+
required: targetField.required || false,
|
|
1630
|
+
settings: targetField.settings || {},
|
|
1631
|
+
},
|
|
1632
|
+
modify_hint: `gufi_schema_modify({ operations: [{ op: "update_field", entity: "${targetEntity.name}", field_name: "${params.field}", changes: { ... } }] })`,
|
|
1633
|
+
};
|
|
1634
|
+
},
|
|
1635
|
+
async gufi_relations(params) {
|
|
1636
|
+
// 💜 Get all relations in a module (or all modules if not specified)
|
|
1637
|
+
const schemaData = await apiRequest("/api/company/schema", {
|
|
1638
|
+
headers: { "X-Company-ID": params.company_id },
|
|
1639
|
+
}, params.company_id);
|
|
1640
|
+
const modules = schemaData.modules || schemaData.data?.modules || [];
|
|
1641
|
+
const relations = [];
|
|
1642
|
+
for (const mod of modules) {
|
|
1643
|
+
// Skip if module_id specified and doesn't match
|
|
1644
|
+
const moduleId = mod.submodules?.[0]?.entities?.[0]?.ui?.moduleId;
|
|
1645
|
+
if (params.module_id && String(moduleId) !== params.module_id)
|
|
1646
|
+
continue;
|
|
1647
|
+
for (const sub of mod.submodules || []) {
|
|
1648
|
+
for (const ent of sub.entities || []) {
|
|
1649
|
+
const entityPath = `${mod.name}.${ent.name}`;
|
|
1650
|
+
for (const field of ent.fields || []) {
|
|
1651
|
+
if (field.type === "relation" && field.settings?.ref) {
|
|
1652
|
+
relations.push({
|
|
1653
|
+
from: entityPath,
|
|
1654
|
+
field: field.name,
|
|
1655
|
+
to: field.settings.ref,
|
|
1656
|
+
display: field.settings.display || "id",
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// Group by source entity for readability
|
|
1664
|
+
const grouped = {};
|
|
1665
|
+
for (const rel of relations) {
|
|
1666
|
+
if (!grouped[rel.from])
|
|
1667
|
+
grouped[rel.from] = [];
|
|
1668
|
+
grouped[rel.from].push({ field: rel.field, to: rel.to, display: rel.display });
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
company_id: params.company_id,
|
|
1672
|
+
...(params.module_id && { module_id: params.module_id }),
|
|
1673
|
+
total_relations: relations.length,
|
|
1674
|
+
relations_by_entity: grouped,
|
|
1675
|
+
flat_list: relations,
|
|
1676
|
+
};
|
|
1190
1677
|
},
|
|
1191
1678
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1192
1679
|
// Automations
|
|
@@ -1205,16 +1692,21 @@ const toolHandlers = {
|
|
|
1205
1692
|
};
|
|
1206
1693
|
},
|
|
1207
1694
|
async gufi_automation(params) {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1695
|
+
// 💜 Fetch automation directly from specified company (IDs are local per company)
|
|
1696
|
+
const data = await apiRequest("/api/automation-scripts", {
|
|
1697
|
+
headers: { "X-Company-ID": params.company_id },
|
|
1698
|
+
});
|
|
1699
|
+
const automations = Array.isArray(data) ? data : data.data || [];
|
|
1700
|
+
const found = automations.find((a) => String(a.id) === params.automation_id);
|
|
1701
|
+
if (!found) {
|
|
1702
|
+
throw new Error(`Automation ${params.automation_id} not found in company ${params.company_id}`);
|
|
1211
1703
|
}
|
|
1212
1704
|
return {
|
|
1213
|
-
id:
|
|
1214
|
-
name:
|
|
1215
|
-
description:
|
|
1216
|
-
company_id:
|
|
1217
|
-
code:
|
|
1705
|
+
id: found.id,
|
|
1706
|
+
name: found.name,
|
|
1707
|
+
description: found.description,
|
|
1708
|
+
company_id: params.company_id,
|
|
1709
|
+
code: found.code || "",
|
|
1218
1710
|
};
|
|
1219
1711
|
},
|
|
1220
1712
|
async gufi_automation_create(params) {
|
|
@@ -1230,16 +1722,24 @@ const toolHandlers = {
|
|
|
1230
1722
|
return { success: true, automation: response.data || response };
|
|
1231
1723
|
},
|
|
1232
1724
|
async gufi_entity_automations(params) {
|
|
1233
|
-
const response = await apiRequest(`/api/entities/${params.entity_id}/automations
|
|
1725
|
+
const response = await apiRequest(`/api/entities/${params.entity_id}/automations`, {
|
|
1726
|
+
headers: { "X-Company-ID": params.company_id },
|
|
1727
|
+
}, params.company_id);
|
|
1234
1728
|
const data = response.data || response;
|
|
1235
|
-
return {
|
|
1729
|
+
return {
|
|
1730
|
+
entity_id: params.entity_id,
|
|
1731
|
+
company_id: params.company_id,
|
|
1732
|
+
automations: data.automations || data || [],
|
|
1733
|
+
availableScripts: data.availableScripts || [],
|
|
1734
|
+
};
|
|
1236
1735
|
},
|
|
1237
1736
|
async gufi_entity_automations_update(params) {
|
|
1238
1737
|
const response = await apiRequest(`/api/entities/${params.entity_id}/automations`, {
|
|
1239
1738
|
method: "PUT",
|
|
1240
1739
|
body: JSON.stringify(params.automations),
|
|
1241
|
-
|
|
1242
|
-
|
|
1740
|
+
headers: { "X-Company-ID": params.company_id },
|
|
1741
|
+
}, params.company_id);
|
|
1742
|
+
return { success: true, entity_id: params.entity_id, company_id: params.company_id };
|
|
1243
1743
|
},
|
|
1244
1744
|
async gufi_automations_executions(params) {
|
|
1245
1745
|
let endpoint = `/api/automation-executions?limit=${params.limit || 20}`;
|
|
@@ -1254,8 +1754,82 @@ const toolHandlers = {
|
|
|
1254
1754
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1255
1755
|
// Data (CRUD)
|
|
1256
1756
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1757
|
+
async gufi_data(params) {
|
|
1758
|
+
// 💜 Unified data tool - all CRUD operations in one
|
|
1759
|
+
const { action, table, company_id } = params;
|
|
1760
|
+
switch (action) {
|
|
1761
|
+
case "list": {
|
|
1762
|
+
const limit = params.limit || 20;
|
|
1763
|
+
const offset = params.offset || 0;
|
|
1764
|
+
const body = {
|
|
1765
|
+
pagination: { current: Math.floor(offset / limit) + 1, pageSize: limit },
|
|
1766
|
+
sorters: [{ field: params.sort || "id", order: params.order || "desc" }],
|
|
1767
|
+
filters: [],
|
|
1768
|
+
};
|
|
1769
|
+
if (params.filter) {
|
|
1770
|
+
const [field, value] = params.filter.split("=");
|
|
1771
|
+
if (field && value)
|
|
1772
|
+
body.filters.push({ field, operator: "eq", value });
|
|
1773
|
+
}
|
|
1774
|
+
const result = await apiRequest(`/api/tables/${table}/list`, {
|
|
1775
|
+
method: "POST",
|
|
1776
|
+
body: JSON.stringify(body),
|
|
1777
|
+
}, company_id);
|
|
1778
|
+
return { action: "list", table, total: result.total, rows: result.data || [] };
|
|
1779
|
+
}
|
|
1780
|
+
case "get": {
|
|
1781
|
+
if (!params.id)
|
|
1782
|
+
throw new Error("id is required for 'get' action");
|
|
1783
|
+
const result = await apiRequest(`/api/tables/${table}/getOne`, {
|
|
1784
|
+
method: "POST",
|
|
1785
|
+
body: JSON.stringify({ id: params.id }),
|
|
1786
|
+
}, company_id);
|
|
1787
|
+
return { action: "get", table, id: params.id, row: result.data || result };
|
|
1788
|
+
}
|
|
1789
|
+
case "create": {
|
|
1790
|
+
if (!params.data)
|
|
1791
|
+
throw new Error("data is required for 'create' action");
|
|
1792
|
+
// 💜 Validate relation fields before creating
|
|
1793
|
+
const createValidation = await validateRelationFields(table, company_id, params.data);
|
|
1794
|
+
if (!createValidation.valid) {
|
|
1795
|
+
throw new Error(`Relation validation failed:\n${createValidation.errors.join("\n")}`);
|
|
1796
|
+
}
|
|
1797
|
+
const result = await apiRequest(`/api/tables/${table}`, {
|
|
1798
|
+
method: "POST",
|
|
1799
|
+
body: JSON.stringify(params.data),
|
|
1800
|
+
}, company_id);
|
|
1801
|
+
return { action: "create", table, success: true, id: result.id || result.data?.id, row: result.data || result };
|
|
1802
|
+
}
|
|
1803
|
+
case "update": {
|
|
1804
|
+
if (!params.id)
|
|
1805
|
+
throw new Error("id is required for 'update' action");
|
|
1806
|
+
if (!params.data)
|
|
1807
|
+
throw new Error("data is required for 'update' action");
|
|
1808
|
+
// 💜 Validate relation fields before updating
|
|
1809
|
+
const updateValidation = await validateRelationFields(table, company_id, params.data);
|
|
1810
|
+
if (!updateValidation.valid) {
|
|
1811
|
+
throw new Error(`Relation validation failed:\n${updateValidation.errors.join("\n")}`);
|
|
1812
|
+
}
|
|
1813
|
+
const result = await apiRequest(`/api/tables/${table}/${params.id}`, {
|
|
1814
|
+
method: "PUT",
|
|
1815
|
+
body: JSON.stringify(params.data),
|
|
1816
|
+
}, company_id);
|
|
1817
|
+
return { action: "update", table, success: true, id: params.id, row: result.data || result };
|
|
1818
|
+
}
|
|
1819
|
+
case "delete": {
|
|
1820
|
+
if (!params.id)
|
|
1821
|
+
throw new Error("id is required for 'delete' action");
|
|
1822
|
+
await apiRequest(`/api/tables/${table}/${params.id}`, {
|
|
1823
|
+
method: "DELETE",
|
|
1824
|
+
}, company_id);
|
|
1825
|
+
return { action: "delete", table, success: true, id: params.id };
|
|
1826
|
+
}
|
|
1827
|
+
default:
|
|
1828
|
+
throw new Error(`Unknown action: ${action}. Use: list, get, create, update, delete`);
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1257
1831
|
async gufi_rows(params) {
|
|
1258
|
-
|
|
1832
|
+
// 💜 company_id is now required - IDs are local per company
|
|
1259
1833
|
const limit = params.limit || 20;
|
|
1260
1834
|
const offset = params.offset || 0;
|
|
1261
1835
|
const body = {
|
|
@@ -1272,51 +1846,64 @@ const toolHandlers = {
|
|
|
1272
1846
|
const result = await apiRequest(`/api/tables/${params.table}/list`, {
|
|
1273
1847
|
method: "POST",
|
|
1274
1848
|
body: JSON.stringify(body),
|
|
1275
|
-
},
|
|
1849
|
+
}, params.company_id);
|
|
1276
1850
|
return {
|
|
1277
1851
|
table: params.table,
|
|
1852
|
+
company_id: params.company_id,
|
|
1278
1853
|
total: result.total,
|
|
1279
1854
|
rows: result.data || [],
|
|
1280
1855
|
};
|
|
1281
1856
|
},
|
|
1282
1857
|
async gufi_row(params) {
|
|
1283
|
-
const companyId = params.company_id || await detectCompanyFromTable(params.table);
|
|
1284
1858
|
const result = await apiRequest(`/api/tables/${params.table}/getOne`, {
|
|
1285
1859
|
method: "POST",
|
|
1286
1860
|
body: JSON.stringify({ id: params.id }),
|
|
1287
|
-
},
|
|
1861
|
+
}, params.company_id);
|
|
1288
1862
|
return {
|
|
1289
1863
|
table: params.table,
|
|
1864
|
+
company_id: params.company_id,
|
|
1290
1865
|
id: params.id,
|
|
1291
1866
|
row: result.data || result,
|
|
1292
1867
|
};
|
|
1293
1868
|
},
|
|
1294
1869
|
async gufi_row_create(params) {
|
|
1295
|
-
|
|
1870
|
+
// Normalize file fields (auto-generate id, mime, uploaded_at)
|
|
1871
|
+
const normalizedData = normalizeFileFields(params.data);
|
|
1872
|
+
// 💜 Validate relation fields before creating
|
|
1873
|
+
const validation = await validateRelationFields(params.table, params.company_id, normalizedData);
|
|
1874
|
+
if (!validation.valid) {
|
|
1875
|
+
throw new Error(`Relation validation failed:\n${validation.errors.join("\n")}`);
|
|
1876
|
+
}
|
|
1296
1877
|
const result = await apiRequest(`/api/tables/${params.table}`, {
|
|
1297
1878
|
method: "POST",
|
|
1298
|
-
body: JSON.stringify(
|
|
1299
|
-
},
|
|
1879
|
+
body: JSON.stringify(normalizedData),
|
|
1880
|
+
}, params.company_id);
|
|
1300
1881
|
return {
|
|
1301
1882
|
success: true,
|
|
1883
|
+
company_id: params.company_id,
|
|
1302
1884
|
id: result.id || result.data?.id,
|
|
1303
1885
|
row: result.data || result,
|
|
1304
1886
|
};
|
|
1305
1887
|
},
|
|
1306
1888
|
async gufi_row_update(params) {
|
|
1307
|
-
|
|
1889
|
+
// Normalize file fields (auto-generate id, mime, uploaded_at)
|
|
1890
|
+
const normalizedData = normalizeFileFields(params.data);
|
|
1891
|
+
// 💜 Validate relation fields before updating
|
|
1892
|
+
const validation = await validateRelationFields(params.table, params.company_id, normalizedData);
|
|
1893
|
+
if (!validation.valid) {
|
|
1894
|
+
throw new Error(`Relation validation failed:\n${validation.errors.join("\n")}`);
|
|
1895
|
+
}
|
|
1308
1896
|
const result = await apiRequest(`/api/tables/${params.table}/${params.id}`, {
|
|
1309
1897
|
method: "PUT",
|
|
1310
|
-
body: JSON.stringify(
|
|
1311
|
-
},
|
|
1312
|
-
return { success: true, row: result.data || result };
|
|
1898
|
+
body: JSON.stringify(normalizedData),
|
|
1899
|
+
}, params.company_id);
|
|
1900
|
+
return { success: true, company_id: params.company_id, row: result.data || result };
|
|
1313
1901
|
},
|
|
1314
1902
|
async gufi_row_delete(params) {
|
|
1315
|
-
const companyId = params.company_id || await detectCompanyFromTable(params.table);
|
|
1316
1903
|
await apiRequest(`/api/tables/${params.table}/${params.id}`, {
|
|
1317
1904
|
method: "DELETE",
|
|
1318
|
-
},
|
|
1319
|
-
return { success: true };
|
|
1905
|
+
}, params.company_id);
|
|
1906
|
+
return { success: true, company_id: params.company_id };
|
|
1320
1907
|
},
|
|
1321
1908
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1322
1909
|
// Environment Variables
|
|
@@ -1329,14 +1916,14 @@ const toolHandlers = {
|
|
|
1329
1916
|
await apiRequest("/api/cli/env", {
|
|
1330
1917
|
method: "POST",
|
|
1331
1918
|
body: JSON.stringify({ key: params.key, value: params.value }),
|
|
1332
|
-
});
|
|
1333
|
-
return { success: true };
|
|
1919
|
+
}, params.company_id);
|
|
1920
|
+
return { success: true, company_id: params.company_id };
|
|
1334
1921
|
},
|
|
1335
1922
|
async gufi_env_delete(params) {
|
|
1336
1923
|
await apiRequest(`/api/cli/env/${encodeURIComponent(params.key)}`, {
|
|
1337
1924
|
method: "DELETE",
|
|
1338
|
-
});
|
|
1339
|
-
return { success: true };
|
|
1925
|
+
}, params.company_id);
|
|
1926
|
+
return { success: true, company_id: params.company_id };
|
|
1340
1927
|
},
|
|
1341
1928
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1342
1929
|
// Views
|
|
@@ -1392,6 +1979,516 @@ const toolHandlers = {
|
|
|
1392
1979
|
: `❌ ${response.errors?.length || 0} errors, ${response.warnings?.length || 0} warnings`,
|
|
1393
1980
|
};
|
|
1394
1981
|
},
|
|
1982
|
+
async gufi_view_create(params) {
|
|
1983
|
+
// Generate safe function name
|
|
1984
|
+
const safeName = params.name.replace(/[^a-zA-Z0-9]/g, "");
|
|
1985
|
+
// Template generators
|
|
1986
|
+
const getBlankTemplate = (name, fnName) => ({
|
|
1987
|
+
index: `// ${name}
|
|
1988
|
+
import React from 'react';
|
|
1989
|
+
import { Package } from 'lucide-react';
|
|
1990
|
+
import { type FeatureProps } from '@/sdk';
|
|
1991
|
+
|
|
1992
|
+
export { featureConfig } from './core/dataProvider';
|
|
1993
|
+
|
|
1994
|
+
export default function ${fnName}View({ id, gufi }: FeatureProps) {
|
|
1995
|
+
const viewSpec = gufi?.context?.viewSpec || {};
|
|
1996
|
+
|
|
1997
|
+
return (
|
|
1998
|
+
<div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
|
|
1999
|
+
<header className="mb-6">
|
|
2000
|
+
<h1 className="text-xl font-bold flex items-center gap-2">
|
|
2001
|
+
<Package className="w-5 h-5 text-violet-600" />
|
|
2002
|
+
${name}
|
|
2003
|
+
</h1>
|
|
2004
|
+
</header>
|
|
2005
|
+
|
|
2006
|
+
<div className="bg-white rounded-xl border shadow-sm p-6">
|
|
2007
|
+
<p className="text-gray-600">Start building your view here!</p>
|
|
2008
|
+
<p className="text-sm text-gray-400 mt-2">
|
|
2009
|
+
viewSpec.mainTable: {viewSpec.mainTable || 'Not configured'}
|
|
2010
|
+
</p>
|
|
2011
|
+
</div>
|
|
2012
|
+
</div>
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
`,
|
|
2016
|
+
dataProvider: `import type { FeatureConfig } from '@/sdk';
|
|
2017
|
+
|
|
2018
|
+
export const featureConfig: FeatureConfig = {
|
|
2019
|
+
dataSources: {
|
|
2020
|
+
mainTable: {
|
|
2021
|
+
type: 'table',
|
|
2022
|
+
module: 'default',
|
|
2023
|
+
entity: 'items',
|
|
2024
|
+
label: 'Main Table'
|
|
2025
|
+
}
|
|
2026
|
+
},
|
|
2027
|
+
inputs: {},
|
|
2028
|
+
seedData: {
|
|
2029
|
+
description: { es: 'Vista', en: 'View' },
|
|
2030
|
+
data: {},
|
|
2031
|
+
order: []
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
`,
|
|
2035
|
+
});
|
|
2036
|
+
const getDashboardTemplate = (name, fnName) => ({
|
|
2037
|
+
index: `// ${name}
|
|
2038
|
+
import React, { useState, useEffect } from 'react';
|
|
2039
|
+
import { Package, TrendingUp, Users, DollarSign } from 'lucide-react';
|
|
2040
|
+
import { cn, toastError, type FeatureProps } from '@/sdk';
|
|
2041
|
+
import { Button, Card, CardContent, CardHeader, CardTitle } from '@/sdk';
|
|
2042
|
+
|
|
2043
|
+
export { featureConfig } from './core/dataProvider';
|
|
2044
|
+
|
|
2045
|
+
export default function ${fnName}View({ id, gufi }: FeatureProps) {
|
|
2046
|
+
const dataProvider = gufi?.dataProvider;
|
|
2047
|
+
const viewSpec = gufi?.context?.viewSpec || {};
|
|
2048
|
+
const isPreview = gufi?.context?.isPreview;
|
|
2049
|
+
const seedData = gufi?.seedData;
|
|
2050
|
+
|
|
2051
|
+
const [data, setData] = useState<any[]>([]);
|
|
2052
|
+
const [loading, setLoading] = useState(true);
|
|
2053
|
+
const [stats, setStats] = useState({ total: 0, active: 0, revenue: 0 });
|
|
2054
|
+
|
|
2055
|
+
useEffect(() => {
|
|
2056
|
+
const load = async () => {
|
|
2057
|
+
setLoading(true);
|
|
2058
|
+
|
|
2059
|
+
if (isPreview && seedData?.data) {
|
|
2060
|
+
const items = seedData.data.items || [];
|
|
2061
|
+
setData(items);
|
|
2062
|
+
setStats({
|
|
2063
|
+
total: items.length,
|
|
2064
|
+
active: items.filter((i: any) => i.status === 'active').length,
|
|
2065
|
+
revenue: items.reduce((acc: number, i: any) => acc + (i.amount || 0), 0)
|
|
2066
|
+
});
|
|
2067
|
+
setLoading(false);
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (!dataProvider || !viewSpec.mainTable) {
|
|
2072
|
+
setLoading(false);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
try {
|
|
2077
|
+
const res = await dataProvider.getList({
|
|
2078
|
+
resource: viewSpec.mainTable,
|
|
2079
|
+
pagination: { current: 1, pageSize: 100 },
|
|
2080
|
+
});
|
|
2081
|
+
setData(res.data || []);
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
toastError('Error loading data');
|
|
2084
|
+
}
|
|
2085
|
+
setLoading(false);
|
|
2086
|
+
};
|
|
2087
|
+
load();
|
|
2088
|
+
}, [dataProvider, viewSpec.mainTable, seedData, isPreview]);
|
|
2089
|
+
|
|
2090
|
+
if (loading) {
|
|
2091
|
+
return (
|
|
2092
|
+
<div className="p-6 space-y-4">
|
|
2093
|
+
{[1,2,3].map(i => (
|
|
2094
|
+
<div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" />
|
|
2095
|
+
))}
|
|
2096
|
+
</div>
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
return (
|
|
2101
|
+
<div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
|
|
2102
|
+
<header className="mb-6">
|
|
2103
|
+
<h1 className="text-2xl font-bold flex items-center gap-2">
|
|
2104
|
+
<Package className="w-6 h-6 text-violet-600" />
|
|
2105
|
+
${name}
|
|
2106
|
+
</h1>
|
|
2107
|
+
</header>
|
|
2108
|
+
|
|
2109
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
2110
|
+
<Card>
|
|
2111
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
2112
|
+
<CardTitle className="text-sm font-medium text-gray-500">Total</CardTitle>
|
|
2113
|
+
<Users className="w-4 h-4 text-violet-500" />
|
|
2114
|
+
</CardHeader>
|
|
2115
|
+
<CardContent>
|
|
2116
|
+
<div className="text-2xl font-bold">{stats.total}</div>
|
|
2117
|
+
</CardContent>
|
|
2118
|
+
</Card>
|
|
2119
|
+
<Card>
|
|
2120
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
2121
|
+
<CardTitle className="text-sm font-medium text-gray-500">Active</CardTitle>
|
|
2122
|
+
<TrendingUp className="w-4 h-4 text-green-500" />
|
|
2123
|
+
</CardHeader>
|
|
2124
|
+
<CardContent>
|
|
2125
|
+
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
|
|
2126
|
+
</CardContent>
|
|
2127
|
+
</Card>
|
|
2128
|
+
<Card>
|
|
2129
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
2130
|
+
<CardTitle className="text-sm font-medium text-gray-500">Revenue</CardTitle>
|
|
2131
|
+
<DollarSign className="w-4 h-4 text-violet-500" />
|
|
2132
|
+
</CardHeader>
|
|
2133
|
+
<CardContent>
|
|
2134
|
+
<div className="text-2xl font-bold">\${stats.revenue.toLocaleString()}</div>
|
|
2135
|
+
</CardContent>
|
|
2136
|
+
</Card>
|
|
2137
|
+
</div>
|
|
2138
|
+
|
|
2139
|
+
<Card className="flex-1">
|
|
2140
|
+
<CardHeader>
|
|
2141
|
+
<CardTitle>Records</CardTitle>
|
|
2142
|
+
</CardHeader>
|
|
2143
|
+
<CardContent>
|
|
2144
|
+
<div className="space-y-2">
|
|
2145
|
+
{data.slice(0, 10).map((item, i) => (
|
|
2146
|
+
<div key={item.id || i} className="p-3 bg-gray-50 rounded-lg flex justify-between">
|
|
2147
|
+
<span>{item.name || item.nombre || 'Item ' + (i + 1)}</span>
|
|
2148
|
+
<span className="text-gray-500">{item.status || '-'}</span>
|
|
2149
|
+
</div>
|
|
2150
|
+
))}
|
|
2151
|
+
{data.length === 0 && (
|
|
2152
|
+
<div className="text-center text-gray-500 py-8">No data available</div>
|
|
2153
|
+
)}
|
|
2154
|
+
</div>
|
|
2155
|
+
</CardContent>
|
|
2156
|
+
</Card>
|
|
2157
|
+
</div>
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
`,
|
|
2161
|
+
dataProvider: `import type { FeatureConfig } from '@/sdk';
|
|
2162
|
+
|
|
2163
|
+
export const featureConfig: FeatureConfig = {
|
|
2164
|
+
dataSources: {
|
|
2165
|
+
mainTable: {
|
|
2166
|
+
type: 'table',
|
|
2167
|
+
module: 'default',
|
|
2168
|
+
entity: 'items',
|
|
2169
|
+
label: 'Main Data'
|
|
2170
|
+
}
|
|
2171
|
+
},
|
|
2172
|
+
inputs: {
|
|
2173
|
+
title: {
|
|
2174
|
+
type: 'text',
|
|
2175
|
+
label: 'Dashboard Title',
|
|
2176
|
+
default: '${name}'
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
seedData: {
|
|
2180
|
+
description: {
|
|
2181
|
+
es: 'Dashboard de ejemplo',
|
|
2182
|
+
en: 'Example dashboard'
|
|
2183
|
+
},
|
|
2184
|
+
data: {
|
|
2185
|
+
items: [
|
|
2186
|
+
{ id: 1, name: 'Item 1', status: 'active', amount: 1500 },
|
|
2187
|
+
{ id: 2, name: 'Item 2', status: 'active', amount: 2300 },
|
|
2188
|
+
{ id: 3, name: 'Item 3', status: 'pending', amount: 800 },
|
|
2189
|
+
]
|
|
2190
|
+
},
|
|
2191
|
+
order: ['items']
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
`,
|
|
2195
|
+
});
|
|
2196
|
+
const getTableTemplate = (name, fnName) => ({
|
|
2197
|
+
index: `// ${name}
|
|
2198
|
+
import React, { useState, useEffect } from 'react';
|
|
2199
|
+
import { TableIcon, Search } from 'lucide-react';
|
|
2200
|
+
import { cn, toastError, type FeatureProps } from '@/sdk';
|
|
2201
|
+
import { Input, Button } from '@/sdk';
|
|
2202
|
+
|
|
2203
|
+
export { featureConfig } from './core/dataProvider';
|
|
2204
|
+
|
|
2205
|
+
export default function ${fnName}View({ id, gufi }: FeatureProps) {
|
|
2206
|
+
const dataProvider = gufi?.dataProvider;
|
|
2207
|
+
const viewSpec = gufi?.context?.viewSpec || {};
|
|
2208
|
+
const isPreview = gufi?.context?.isPreview;
|
|
2209
|
+
const seedData = gufi?.seedData;
|
|
2210
|
+
|
|
2211
|
+
const [data, setData] = useState<any[]>([]);
|
|
2212
|
+
const [loading, setLoading] = useState(true);
|
|
2213
|
+
const [search, setSearch] = useState('');
|
|
2214
|
+
|
|
2215
|
+
useEffect(() => {
|
|
2216
|
+
const load = async () => {
|
|
2217
|
+
setLoading(true);
|
|
2218
|
+
if (isPreview && seedData?.data) {
|
|
2219
|
+
setData(seedData.data.items || []);
|
|
2220
|
+
setLoading(false);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
if (!dataProvider || !viewSpec.mainTable) {
|
|
2224
|
+
setLoading(false);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
try {
|
|
2228
|
+
const res = await dataProvider.getList({
|
|
2229
|
+
resource: viewSpec.mainTable,
|
|
2230
|
+
pagination: { current: 1, pageSize: 50 },
|
|
2231
|
+
});
|
|
2232
|
+
setData(res.data || []);
|
|
2233
|
+
} catch (err) {
|
|
2234
|
+
toastError('Error loading data');
|
|
2235
|
+
}
|
|
2236
|
+
setLoading(false);
|
|
2237
|
+
};
|
|
2238
|
+
load();
|
|
2239
|
+
}, [dataProvider, viewSpec.mainTable, seedData, isPreview]);
|
|
2240
|
+
|
|
2241
|
+
const filtered = data.filter(item =>
|
|
2242
|
+
!search || JSON.stringify(item).toLowerCase().includes(search.toLowerCase())
|
|
2243
|
+
);
|
|
2244
|
+
|
|
2245
|
+
if (loading) {
|
|
2246
|
+
return (
|
|
2247
|
+
<div className="p-6">
|
|
2248
|
+
<div className="h-10 bg-gray-100 rounded animate-pulse mb-4" />
|
|
2249
|
+
{[1,2,3,4,5].map(i => (
|
|
2250
|
+
<div key={i} className="h-12 bg-gray-50 rounded animate-pulse mb-2" />
|
|
2251
|
+
))}
|
|
2252
|
+
</div>
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
return (
|
|
2257
|
+
<div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
|
|
2258
|
+
<header className="mb-4 flex items-center justify-between">
|
|
2259
|
+
<h1 className="text-xl font-bold flex items-center gap-2">
|
|
2260
|
+
<TableIcon className="w-5 h-5 text-violet-600" />
|
|
2261
|
+
${name}
|
|
2262
|
+
</h1>
|
|
2263
|
+
<div className="relative">
|
|
2264
|
+
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
|
2265
|
+
<Input
|
|
2266
|
+
className="pl-9 w-64"
|
|
2267
|
+
placeholder="Search..."
|
|
2268
|
+
value={search}
|
|
2269
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
2270
|
+
/>
|
|
2271
|
+
</div>
|
|
2272
|
+
</header>
|
|
2273
|
+
|
|
2274
|
+
<div className="bg-white rounded-xl border shadow-sm overflow-hidden">
|
|
2275
|
+
<table className="w-full">
|
|
2276
|
+
<thead className="bg-gray-50 border-b">
|
|
2277
|
+
<tr>
|
|
2278
|
+
<th className="text-left p-3 font-medium text-gray-600">ID</th>
|
|
2279
|
+
<th className="text-left p-3 font-medium text-gray-600">Name</th>
|
|
2280
|
+
<th className="text-left p-3 font-medium text-gray-600">Status</th>
|
|
2281
|
+
</tr>
|
|
2282
|
+
</thead>
|
|
2283
|
+
<tbody>
|
|
2284
|
+
{filtered.map((item, i) => (
|
|
2285
|
+
<tr key={item.id || i} className="border-b hover:bg-gray-50">
|
|
2286
|
+
<td className="p-3">{item.id}</td>
|
|
2287
|
+
<td className="p-3">{item.name || item.nombre || '-'}</td>
|
|
2288
|
+
<td className="p-3">{item.status || '-'}</td>
|
|
2289
|
+
</tr>
|
|
2290
|
+
))}
|
|
2291
|
+
{filtered.length === 0 && (
|
|
2292
|
+
<tr>
|
|
2293
|
+
<td colSpan={3} className="p-8 text-center text-gray-500">No data found</td>
|
|
2294
|
+
</tr>
|
|
2295
|
+
)}
|
|
2296
|
+
</tbody>
|
|
2297
|
+
</table>
|
|
2298
|
+
</div>
|
|
2299
|
+
</div>
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
`,
|
|
2303
|
+
dataProvider: `import type { FeatureConfig } from '@/sdk';
|
|
2304
|
+
|
|
2305
|
+
export const featureConfig: FeatureConfig = {
|
|
2306
|
+
dataSources: {
|
|
2307
|
+
mainTable: {
|
|
2308
|
+
type: 'table',
|
|
2309
|
+
module: 'default',
|
|
2310
|
+
entity: 'items',
|
|
2311
|
+
label: 'Data Table'
|
|
2312
|
+
}
|
|
2313
|
+
},
|
|
2314
|
+
inputs: {},
|
|
2315
|
+
seedData: {
|
|
2316
|
+
description: { es: 'Tabla de datos', en: 'Data table' },
|
|
2317
|
+
data: {
|
|
2318
|
+
items: [
|
|
2319
|
+
{ id: 1, name: 'Record 1', status: 'active' },
|
|
2320
|
+
{ id: 2, name: 'Record 2', status: 'pending' },
|
|
2321
|
+
{ id: 3, name: 'Record 3', status: 'completed' },
|
|
2322
|
+
]
|
|
2323
|
+
},
|
|
2324
|
+
order: ['items']
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
`,
|
|
2328
|
+
});
|
|
2329
|
+
const getFormTemplate = (name, fnName) => ({
|
|
2330
|
+
index: `// ${name}
|
|
2331
|
+
import React, { useState } from 'react';
|
|
2332
|
+
import { FileText, Save } from 'lucide-react';
|
|
2333
|
+
import { toastSuccess, toastError, type FeatureProps } from '@/sdk';
|
|
2334
|
+
import { Button, Input, Label } from '@/sdk';
|
|
2335
|
+
|
|
2336
|
+
export { featureConfig } from './core/dataProvider';
|
|
2337
|
+
|
|
2338
|
+
export default function ${fnName}View({ id, gufi }: FeatureProps) {
|
|
2339
|
+
const dataProvider = gufi?.dataProvider;
|
|
2340
|
+
const viewSpec = gufi?.context?.viewSpec || {};
|
|
2341
|
+
const CB = gufi?.CB;
|
|
2342
|
+
|
|
2343
|
+
const [formData, setFormData] = useState({ name: '', email: '', notes: '' });
|
|
2344
|
+
const [saving, setSaving] = useState(false);
|
|
2345
|
+
|
|
2346
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
2347
|
+
e.preventDefault();
|
|
2348
|
+
if (!dataProvider || !viewSpec.mainTable || !CB) {
|
|
2349
|
+
toastError('Configuration error');
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
setSaving(true);
|
|
2353
|
+
try {
|
|
2354
|
+
await dataProvider.create({
|
|
2355
|
+
resource: viewSpec.mainTable,
|
|
2356
|
+
variables: {
|
|
2357
|
+
name: CB.text(formData.name),
|
|
2358
|
+
email: CB.text(formData.email),
|
|
2359
|
+
notes: CB.text(formData.notes),
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
toastSuccess('Saved successfully!');
|
|
2363
|
+
setFormData({ name: '', email: '', notes: '' });
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
toastError('Error saving');
|
|
2366
|
+
}
|
|
2367
|
+
setSaving(false);
|
|
2368
|
+
};
|
|
2369
|
+
|
|
2370
|
+
return (
|
|
2371
|
+
<div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
|
|
2372
|
+
<header className="mb-6">
|
|
2373
|
+
<h1 className="text-xl font-bold flex items-center gap-2">
|
|
2374
|
+
<FileText className="w-5 h-5 text-violet-600" />
|
|
2375
|
+
${name}
|
|
2376
|
+
</h1>
|
|
2377
|
+
</header>
|
|
2378
|
+
|
|
2379
|
+
<form onSubmit={handleSubmit} className="bg-white rounded-xl border shadow-sm p-6 max-w-lg space-y-4">
|
|
2380
|
+
<div>
|
|
2381
|
+
<Label htmlFor="name">Name</Label>
|
|
2382
|
+
<Input
|
|
2383
|
+
id="name"
|
|
2384
|
+
value={formData.name}
|
|
2385
|
+
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
2386
|
+
placeholder="Enter name..."
|
|
2387
|
+
required
|
|
2388
|
+
/>
|
|
2389
|
+
</div>
|
|
2390
|
+
<div>
|
|
2391
|
+
<Label htmlFor="email">Email</Label>
|
|
2392
|
+
<Input
|
|
2393
|
+
id="email"
|
|
2394
|
+
type="email"
|
|
2395
|
+
value={formData.email}
|
|
2396
|
+
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
|
|
2397
|
+
placeholder="Enter email..."
|
|
2398
|
+
/>
|
|
2399
|
+
</div>
|
|
2400
|
+
<div>
|
|
2401
|
+
<Label htmlFor="notes">Notes</Label>
|
|
2402
|
+
<Input
|
|
2403
|
+
id="notes"
|
|
2404
|
+
value={formData.notes}
|
|
2405
|
+
onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
|
|
2406
|
+
placeholder="Additional notes..."
|
|
2407
|
+
/>
|
|
2408
|
+
</div>
|
|
2409
|
+
<Button type="submit" disabled={saving} className="w-full">
|
|
2410
|
+
<Save className="w-4 h-4 mr-2" />
|
|
2411
|
+
{saving ? 'Saving...' : 'Save'}
|
|
2412
|
+
</Button>
|
|
2413
|
+
</form>
|
|
2414
|
+
</div>
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
`,
|
|
2418
|
+
dataProvider: `import type { FeatureConfig } from '@/sdk';
|
|
2419
|
+
|
|
2420
|
+
export const featureConfig: FeatureConfig = {
|
|
2421
|
+
dataSources: {
|
|
2422
|
+
mainTable: {
|
|
2423
|
+
type: 'table',
|
|
2424
|
+
module: 'default',
|
|
2425
|
+
entity: 'submissions',
|
|
2426
|
+
label: 'Form Submissions'
|
|
2427
|
+
}
|
|
2428
|
+
},
|
|
2429
|
+
inputs: {},
|
|
2430
|
+
seedData: {
|
|
2431
|
+
description: { es: 'Formulario', en: 'Form' },
|
|
2432
|
+
data: {},
|
|
2433
|
+
order: []
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
`,
|
|
2437
|
+
});
|
|
2438
|
+
// Get the right template
|
|
2439
|
+
let template;
|
|
2440
|
+
switch (params.template) {
|
|
2441
|
+
case "dashboard":
|
|
2442
|
+
template = getDashboardTemplate(params.name, safeName);
|
|
2443
|
+
break;
|
|
2444
|
+
case "table":
|
|
2445
|
+
template = getTableTemplate(params.name, safeName);
|
|
2446
|
+
break;
|
|
2447
|
+
case "form":
|
|
2448
|
+
template = getFormTemplate(params.name, safeName);
|
|
2449
|
+
break;
|
|
2450
|
+
default:
|
|
2451
|
+
template = getBlankTemplate(params.name, safeName);
|
|
2452
|
+
}
|
|
2453
|
+
// Need package_id to create a view
|
|
2454
|
+
if (!params.package_id) {
|
|
2455
|
+
return {
|
|
2456
|
+
error: "package_id is required to create a view",
|
|
2457
|
+
hint: "First create a package with gufi_package_create, then pass its ID",
|
|
2458
|
+
available_templates: ["dashboard", "table", "form", "blank"],
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
// Create the view via API
|
|
2462
|
+
const response = await apiRequest(`/api/marketplace/packages/${params.package_id}/views`, {
|
|
2463
|
+
method: "POST",
|
|
2464
|
+
body: JSON.stringify({
|
|
2465
|
+
name: params.name,
|
|
2466
|
+
description: params.description || `${params.template} view created via MCP`,
|
|
2467
|
+
view_type: "custom",
|
|
2468
|
+
}),
|
|
2469
|
+
});
|
|
2470
|
+
const viewId = response.pk_id;
|
|
2471
|
+
// Now update with template files
|
|
2472
|
+
const files = [
|
|
2473
|
+
{ file_path: "index.tsx", content: template.index, language: "typescript" },
|
|
2474
|
+
{ file_path: "core/dataProvider.ts", content: template.dataProvider, language: "typescript" },
|
|
2475
|
+
];
|
|
2476
|
+
await saveViewFiles(viewId, files);
|
|
2477
|
+
return {
|
|
2478
|
+
success: true,
|
|
2479
|
+
view_id: viewId,
|
|
2480
|
+
name: params.name,
|
|
2481
|
+
template: params.template,
|
|
2482
|
+
package_id: params.package_id,
|
|
2483
|
+
files_created: files.map(f => f.file_path),
|
|
2484
|
+
next_steps: [
|
|
2485
|
+
`1. Download locally: gufi view:pull ${viewId}`,
|
|
2486
|
+
`2. Edit files in: ~/gufi-dev/view_${viewId}/`,
|
|
2487
|
+
`3. Upload changes: gufi view:push`,
|
|
2488
|
+
`4. Publish: gufi package:sync ${params.package_id}`,
|
|
2489
|
+
],
|
|
2490
|
+
};
|
|
2491
|
+
},
|
|
1395
2492
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1396
2493
|
// Packages
|
|
1397
2494
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1544,6 +2641,31 @@ const toolHandlers = {
|
|
|
1544
2641
|
const response = await developerRequest(`/packages/${params.package_id}/entities`);
|
|
1545
2642
|
return { entities: response.data || [] };
|
|
1546
2643
|
},
|
|
2644
|
+
async gufi_link_view(params) {
|
|
2645
|
+
const entity = {
|
|
2646
|
+
kind: "marketplace_view",
|
|
2647
|
+
marketplace_view_id: params.view_id,
|
|
2648
|
+
label: params.label,
|
|
2649
|
+
icon: params.icon || "Sparkles",
|
|
2650
|
+
};
|
|
2651
|
+
const response = await developerRequest(`/packages/${params.package_id}/modules/${params.module_id}/add-entity`, {
|
|
2652
|
+
method: "POST",
|
|
2653
|
+
body: JSON.stringify({
|
|
2654
|
+
submodule_name: params.submodule_name,
|
|
2655
|
+
entity,
|
|
2656
|
+
}),
|
|
2657
|
+
});
|
|
2658
|
+
// Invalidate schema cache on main backend
|
|
2659
|
+
try {
|
|
2660
|
+
await apiRequest("/api/company/schema/invalidate", { method: "POST" });
|
|
2661
|
+
}
|
|
2662
|
+
catch { }
|
|
2663
|
+
return {
|
|
2664
|
+
success: true,
|
|
2665
|
+
message: `View "${params.label}" linked to ${params.submodule_name}`,
|
|
2666
|
+
entity: response.data?.entity,
|
|
2667
|
+
};
|
|
2668
|
+
},
|
|
1547
2669
|
};
|
|
1548
2670
|
// ════════════════════════════════════════════════════════════════════════════
|
|
1549
2671
|
// Context Generation Helpers
|