gufi-cli 0.1.2 → 0.1.4
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/CLAUDE.md +137 -6
- package/dist/commands/login.js +4 -3
- package/dist/commands/rows.d.ts +46 -0
- package/dist/commands/rows.js +243 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +51 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +42 -5
- package/dist/lib/config.d.ts +4 -1
- package/dist/lib/config.js +13 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -987,16 +987,117 @@ interface GufiProps {
|
|
|
987
987
|
|
|
988
988
|
---
|
|
989
989
|
|
|
990
|
+
### 💜 seedData - Datos de Ejemplo para Demo/Testing
|
|
991
|
+
|
|
992
|
+
Las vistas pueden incluir datos de ejemplo que se cargan automáticamente al instalar. Esto es útil para demos, testing, y onboarding de nuevos usuarios.
|
|
993
|
+
|
|
994
|
+
```typescript
|
|
995
|
+
// metadata/seedData.ts
|
|
996
|
+
export interface SeedDataConfig {
|
|
997
|
+
description: { es: string; en: string };
|
|
998
|
+
data: { [dataSourceKey: string]: Array<Record<string, any>> };
|
|
999
|
+
order: string[]; // Orden de creación (importante para referencias)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export const seedData: SeedDataConfig = {
|
|
1003
|
+
description: {
|
|
1004
|
+
es: 'Crea empresas de ejemplo (Singular, FitVending), proyectos y tareas de prueba',
|
|
1005
|
+
en: 'Creates sample companies, projects and test tasks',
|
|
1006
|
+
},
|
|
1007
|
+
|
|
1008
|
+
// Orden de creación - tablas con referencias van después
|
|
1009
|
+
order: ['empresasTable', 'proyectosTable', 'tareasTable'],
|
|
1010
|
+
|
|
1011
|
+
data: {
|
|
1012
|
+
// Empresas/Clientes
|
|
1013
|
+
empresasTable: [
|
|
1014
|
+
{
|
|
1015
|
+
nombre: 'Singular',
|
|
1016
|
+
contacto: 'Contacto Singular',
|
|
1017
|
+
email: 'contacto@singular.es',
|
|
1018
|
+
estado: 'activo'
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
nombre: 'FitVending',
|
|
1022
|
+
contacto: 'Equipo FitVending',
|
|
1023
|
+
email: 'info@fitvending.es',
|
|
1024
|
+
estado: 'activo'
|
|
1025
|
+
},
|
|
1026
|
+
],
|
|
1027
|
+
|
|
1028
|
+
// Proyectos
|
|
1029
|
+
proyectosTable: [
|
|
1030
|
+
{
|
|
1031
|
+
nombre: 'Gufi ERP',
|
|
1032
|
+
descripcion: 'Desarrollo del ERP Gufi',
|
|
1033
|
+
color: '#8B5CF6',
|
|
1034
|
+
estado: 'activo',
|
|
1035
|
+
},
|
|
1036
|
+
],
|
|
1037
|
+
|
|
1038
|
+
// Tareas - con referencias a otras tablas
|
|
1039
|
+
tareasTable: [
|
|
1040
|
+
{
|
|
1041
|
+
titulo: 'Revisar bug crítico',
|
|
1042
|
+
descripcion: 'Los usuarios reportan problemas',
|
|
1043
|
+
asignado_a: '@currentUser', // 💜 Token especial: usuario actual
|
|
1044
|
+
asignado_por: '@currentUser',
|
|
1045
|
+
prioridad: 'urgente',
|
|
1046
|
+
estado: 'pendiente',
|
|
1047
|
+
fecha_limite: '@today', // 💜 Token especial: fecha de hoy
|
|
1048
|
+
proyecto_id: '@ref:proyectosTable.0', // 💜 Referencia: ID del primer proyecto
|
|
1049
|
+
empresa_id: '@ref:empresasTable.0', // 💜 Referencia: ID de primera empresa
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
titulo: 'Preparar demo',
|
|
1053
|
+
descripcion: 'Demo del módulo de facturación',
|
|
1054
|
+
asignado_a: '@currentUser',
|
|
1055
|
+
prioridad: 'alta',
|
|
1056
|
+
fecha_limite: '@tomorrow', // 💜 Token especial: mañana
|
|
1057
|
+
empresa_id: '@ref:empresasTable.1',
|
|
1058
|
+
},
|
|
1059
|
+
],
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
**Tokens Especiales:**
|
|
1065
|
+
| Token | Descripción |
|
|
1066
|
+
|-------|-------------|
|
|
1067
|
+
| `@currentUser` | ID del usuario actual (para campos users) |
|
|
1068
|
+
| `@today` | Fecha de hoy (YYYY-MM-DD) |
|
|
1069
|
+
| `@tomorrow` | Fecha de mañana |
|
|
1070
|
+
| `@nextWeek` | Fecha dentro de 7 días |
|
|
1071
|
+
| `@ref:tableKey.index` | ID del registro creado en otra tabla |
|
|
1072
|
+
|
|
1073
|
+
**Agregar a featureConfig:**
|
|
1074
|
+
```typescript
|
|
1075
|
+
// core/dataProvider.ts
|
|
1076
|
+
import { seedData } from '../metadata/seedData';
|
|
1077
|
+
|
|
1078
|
+
export const featureConfig = {
|
|
1079
|
+
dataSources,
|
|
1080
|
+
inputs: featureInputs,
|
|
1081
|
+
seedData, // 💜 Agregar aquí
|
|
1082
|
+
};
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
**Cargar desde Developer Center:**
|
|
1086
|
+
En la página de edición de vista, el Developer Center muestra un botón "Load Sample Data" que ejecuta el seedData en la company seleccionada.
|
|
1087
|
+
|
|
1088
|
+
---
|
|
1089
|
+
|
|
990
1090
|
### 💜 Checklist para Nueva Vista
|
|
991
1091
|
|
|
992
1092
|
1. [ ] `core/dataProvider.ts` - dataSources + export featureConfig
|
|
993
1093
|
2. [ ] `metadata/inputs.ts` - featureInputs configurables
|
|
994
|
-
3. [ ] `metadata/
|
|
995
|
-
4. [ ] `
|
|
996
|
-
5. [ ]
|
|
997
|
-
6. [ ] Usar `gufi?.
|
|
998
|
-
7. [ ] Usar `gufi?.
|
|
999
|
-
8. [ ]
|
|
1094
|
+
3. [ ] `metadata/seedData.ts` - Datos de ejemplo (opcional pero recomendado)
|
|
1095
|
+
4. [ ] `metadata/help.es.ts` y `help.en.ts` - Documentación
|
|
1096
|
+
5. [ ] `index.tsx` - export featureConfig y default component
|
|
1097
|
+
6. [ ] Usar `gufi?.context?.viewSpec` para tablas e inputs
|
|
1098
|
+
7. [ ] Usar `gufi?.dataProvider` para CRUD
|
|
1099
|
+
8. [ ] Usar `gufi?.utils?.toast*` para notificaciones
|
|
1100
|
+
9. [ ] Si hay automation: carpeta `automations/` con .js
|
|
1000
1101
|
|
|
1001
1102
|
### View.tsx - Componente Principal
|
|
1002
1103
|
|
|
@@ -1318,6 +1419,36 @@ gufi automation calcular_stock -c 116 --edit
|
|
|
1318
1419
|
gufi automation calcular_stock -c 116 --file script.js
|
|
1319
1420
|
```
|
|
1320
1421
|
|
|
1422
|
+
### Row CRUD (Datos de Tablas)
|
|
1423
|
+
|
|
1424
|
+
```bash
|
|
1425
|
+
# Listar registros de una tabla
|
|
1426
|
+
gufi rows m360_t16192 # Últimos 20 registros
|
|
1427
|
+
gufi rows m360_t16192 -l 50 # Últimos 50 registros
|
|
1428
|
+
gufi rows m360_t16192 -f estado=activo # Filtrar por campo
|
|
1429
|
+
|
|
1430
|
+
# Ver un registro específico
|
|
1431
|
+
gufi row m360_t16192 123
|
|
1432
|
+
|
|
1433
|
+
# Crear registro
|
|
1434
|
+
gufi row:create m360_t16192 --data '{"nombre":"Test","estado":"activo"}'
|
|
1435
|
+
gufi row:create m360_t16192 --file nuevo.json
|
|
1436
|
+
|
|
1437
|
+
# Actualizar registro
|
|
1438
|
+
gufi row:update m360_t16192 123 --data '{"estado":"completado"}'
|
|
1439
|
+
gufi row:update m360_t16192 123 --file cambios.json
|
|
1440
|
+
|
|
1441
|
+
# Eliminar registro
|
|
1442
|
+
gufi row:delete m360_t16192 123
|
|
1443
|
+
|
|
1444
|
+
# Duplicar registro (copia sin id/timestamps)
|
|
1445
|
+
gufi row:duplicate m360_t16192 123
|
|
1446
|
+
|
|
1447
|
+
# Crear múltiples registros desde archivo JSON
|
|
1448
|
+
gufi rows:create m360_t16192 --file datos.json
|
|
1449
|
+
# datos.json debe ser un array: [{"nombre":"A"},{"nombre":"B"}]
|
|
1450
|
+
```
|
|
1451
|
+
|
|
1321
1452
|
### Desarrollo de Views
|
|
1322
1453
|
|
|
1323
1454
|
```bash
|
package/dist/commands/login.js
CHANGED
|
@@ -82,10 +82,11 @@ export async function loginCommand(options) {
|
|
|
82
82
|
}
|
|
83
83
|
const spinner = ora("Iniciando sesión...").start();
|
|
84
84
|
try {
|
|
85
|
-
const { token } = await login(email, password);
|
|
86
|
-
setToken(token, email);
|
|
85
|
+
const { token, refreshToken } = await login(email, password);
|
|
86
|
+
setToken(token, email, refreshToken);
|
|
87
87
|
spinner.succeed(chalk.green(`Sesión iniciada como ${email}`));
|
|
88
|
-
console.log(chalk.gray("\n
|
|
88
|
+
console.log(chalk.gray("\n Tu sesión se mantendrá activa automáticamente.\n"));
|
|
89
|
+
console.log(chalk.gray(" Ahora puedes usar: gufi pull <vista>\n"));
|
|
89
90
|
}
|
|
90
91
|
catch (error) {
|
|
91
92
|
spinner.fail(chalk.red(error.message || "Error al iniciar sesión"));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gufi rows - CRUD operations on table rows
|
|
3
|
+
* For quick testing and data manipulation
|
|
4
|
+
*/
|
|
5
|
+
interface RowsOptions {
|
|
6
|
+
company?: string;
|
|
7
|
+
limit?: string;
|
|
8
|
+
offset?: string;
|
|
9
|
+
sort?: string;
|
|
10
|
+
order?: string;
|
|
11
|
+
filter?: string;
|
|
12
|
+
}
|
|
13
|
+
interface RowOptions {
|
|
14
|
+
company?: string;
|
|
15
|
+
data?: string;
|
|
16
|
+
file?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* gufi rows <table> - List rows from a table
|
|
20
|
+
*/
|
|
21
|
+
export declare function rowsListCommand(table: string, options: RowsOptions): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* gufi row <table> <id> - Get a single row
|
|
24
|
+
*/
|
|
25
|
+
export declare function rowGetCommand(table: string, id: string, options: RowOptions): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* gufi row:create <table> --data '{...}' - Create a row
|
|
28
|
+
*/
|
|
29
|
+
export declare function rowCreateCommand(table: string, options: RowOptions): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* gufi row:update <table> <id> --data '{...}' - Update a row
|
|
32
|
+
*/
|
|
33
|
+
export declare function rowUpdateCommand(table: string, id: string, options: RowOptions): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* gufi row:delete <table> <id> - Delete a row
|
|
36
|
+
*/
|
|
37
|
+
export declare function rowDeleteCommand(table: string, id: string, options: RowOptions): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* gufi row:duplicate <table> <id> - Duplicate a row
|
|
40
|
+
*/
|
|
41
|
+
export declare function rowDuplicateCommand(table: string, id: string, options: RowOptions): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* gufi rows:create <table> --file datos.json - Bulk create rows
|
|
44
|
+
*/
|
|
45
|
+
export declare function rowsBulkCreateCommand(table: string, options: RowOptions): Promise<void>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gufi rows - CRUD operations on table rows
|
|
3
|
+
* For quick testing and data manipulation
|
|
4
|
+
*/
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import { getToken, getApiUrl } from "../lib/config.js";
|
|
9
|
+
async function apiRequest(endpoint, options = {}) {
|
|
10
|
+
const token = getToken();
|
|
11
|
+
if (!token) {
|
|
12
|
+
throw new Error("No estás logueado. Ejecuta: gufi login");
|
|
13
|
+
}
|
|
14
|
+
const url = `${getApiUrl()}${endpoint}`;
|
|
15
|
+
const response = await fetch(url, {
|
|
16
|
+
...options,
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${token}`,
|
|
20
|
+
"X-Client": "cli",
|
|
21
|
+
...options.headers,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
throw new Error(`API Error ${response.status}: ${text}`);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* gufi rows <table> - List rows from a table
|
|
32
|
+
*/
|
|
33
|
+
export async function rowsListCommand(table, options) {
|
|
34
|
+
const spinner = ora("Cargando registros...").start();
|
|
35
|
+
try {
|
|
36
|
+
const limit = options.limit || "20";
|
|
37
|
+
const offset = options.offset || "0";
|
|
38
|
+
const sort = options.sort || "id";
|
|
39
|
+
const order = options.order || "DESC";
|
|
40
|
+
let endpoint = `/api/tables/${table}?_start=${offset}&_end=${parseInt(offset) + parseInt(limit)}&_sort=${sort}&_order=${order}`;
|
|
41
|
+
if (options.filter) {
|
|
42
|
+
// Parse filter like "estado=pendiente"
|
|
43
|
+
const [field, value] = options.filter.split("=");
|
|
44
|
+
if (field && value) {
|
|
45
|
+
endpoint += `&${field}=${encodeURIComponent(value)}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const data = await apiRequest(endpoint);
|
|
49
|
+
spinner.stop();
|
|
50
|
+
console.log(chalk.magenta(`\n 📋 ${table}\n`));
|
|
51
|
+
if (!data || data.length === 0) {
|
|
52
|
+
console.log(chalk.gray(" No hay registros\n"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Show as table
|
|
56
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
57
|
+
// Get columns from first row
|
|
58
|
+
const columns = Object.keys(rows[0]).slice(0, 6); // Max 6 columns
|
|
59
|
+
// Header
|
|
60
|
+
console.log(chalk.gray(" " + columns.map((c) => c.padEnd(15)).join(" ")));
|
|
61
|
+
console.log(chalk.gray(" " + "─".repeat(columns.length * 16)));
|
|
62
|
+
// Rows
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const values = columns.map((col) => {
|
|
65
|
+
let val = row[col];
|
|
66
|
+
if (val === null || val === undefined)
|
|
67
|
+
val = "-";
|
|
68
|
+
if (typeof val === "object")
|
|
69
|
+
val = JSON.stringify(val).slice(0, 12);
|
|
70
|
+
return String(val).slice(0, 14).padEnd(15);
|
|
71
|
+
});
|
|
72
|
+
console.log(" " + values.join(" "));
|
|
73
|
+
}
|
|
74
|
+
console.log(chalk.gray(`\n Total: ${rows.length} registros\n`));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
spinner.fail(chalk.red(error.message));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* gufi row <table> <id> - Get a single row
|
|
83
|
+
*/
|
|
84
|
+
export async function rowGetCommand(table, id, options) {
|
|
85
|
+
const spinner = ora("Cargando registro...").start();
|
|
86
|
+
try {
|
|
87
|
+
const data = await apiRequest(`/api/tables/${table}/${id}`);
|
|
88
|
+
spinner.stop();
|
|
89
|
+
console.log(chalk.magenta(`\n 📄 ${table} #${id}\n`));
|
|
90
|
+
console.log(JSON.stringify(data, null, 2));
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
spinner.fail(chalk.red(error.message));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* gufi row:create <table> --data '{...}' - Create a row
|
|
100
|
+
*/
|
|
101
|
+
export async function rowCreateCommand(table, options) {
|
|
102
|
+
const spinner = ora("Creando registro...").start();
|
|
103
|
+
try {
|
|
104
|
+
let data;
|
|
105
|
+
if (options.file) {
|
|
106
|
+
const content = fs.readFileSync(options.file, "utf-8");
|
|
107
|
+
data = JSON.parse(content);
|
|
108
|
+
}
|
|
109
|
+
else if (options.data) {
|
|
110
|
+
data = JSON.parse(options.data);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
spinner.fail(chalk.red("Usa --data '{...}' o --file datos.json"));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const result = await apiRequest(`/api/tables/${table}`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: JSON.stringify(data),
|
|
119
|
+
});
|
|
120
|
+
spinner.succeed(chalk.green(`Registro creado: ID ${result.id || result.data?.id}`));
|
|
121
|
+
console.log(chalk.gray(JSON.stringify(result.data || result, null, 2)));
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
spinner.fail(chalk.red(error.message));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* gufi row:update <table> <id> --data '{...}' - Update a row
|
|
131
|
+
*/
|
|
132
|
+
export async function rowUpdateCommand(table, id, options) {
|
|
133
|
+
const spinner = ora("Actualizando registro...").start();
|
|
134
|
+
try {
|
|
135
|
+
let data;
|
|
136
|
+
if (options.file) {
|
|
137
|
+
const content = fs.readFileSync(options.file, "utf-8");
|
|
138
|
+
data = JSON.parse(content);
|
|
139
|
+
}
|
|
140
|
+
else if (options.data) {
|
|
141
|
+
data = JSON.parse(options.data);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
spinner.fail(chalk.red("Usa --data '{...}' o --file datos.json"));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const result = await apiRequest(`/api/tables/${table}/${id}`, {
|
|
148
|
+
method: "PUT",
|
|
149
|
+
body: JSON.stringify(data),
|
|
150
|
+
});
|
|
151
|
+
spinner.succeed(chalk.green(`Registro #${id} actualizado`));
|
|
152
|
+
console.log(chalk.gray(JSON.stringify(result.data || result, null, 2)));
|
|
153
|
+
console.log();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
spinner.fail(chalk.red(error.message));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* gufi row:delete <table> <id> - Delete a row
|
|
162
|
+
*/
|
|
163
|
+
export async function rowDeleteCommand(table, id, options) {
|
|
164
|
+
const spinner = ora("Eliminando registro...").start();
|
|
165
|
+
try {
|
|
166
|
+
await apiRequest(`/api/tables/${table}/${id}`, {
|
|
167
|
+
method: "DELETE",
|
|
168
|
+
});
|
|
169
|
+
spinner.succeed(chalk.green(`Registro #${id} eliminado`));
|
|
170
|
+
console.log();
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
spinner.fail(chalk.red(error.message));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* gufi row:duplicate <table> <id> - Duplicate a row
|
|
179
|
+
*/
|
|
180
|
+
export async function rowDuplicateCommand(table, id, options) {
|
|
181
|
+
const spinner = ora("Duplicando registro...").start();
|
|
182
|
+
try {
|
|
183
|
+
// Get original row
|
|
184
|
+
const original = await apiRequest(`/api/tables/${table}/${id}`);
|
|
185
|
+
// Remove id and timestamps
|
|
186
|
+
const { id: _, created_at, updated_at, ...data } = original;
|
|
187
|
+
// Create new row
|
|
188
|
+
const result = await apiRequest(`/api/tables/${table}`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
body: JSON.stringify(data),
|
|
191
|
+
});
|
|
192
|
+
spinner.succeed(chalk.green(`Registro duplicado: #${id} → #${result.id || result.data?.id}`));
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
spinner.fail(chalk.red(error.message));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* gufi rows:create <table> --file datos.json - Bulk create rows
|
|
202
|
+
*/
|
|
203
|
+
export async function rowsBulkCreateCommand(table, options) {
|
|
204
|
+
if (!options.file) {
|
|
205
|
+
console.log(chalk.red("\n ✗ Usa --file datos.json\n"));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
const spinner = ora("Creando registros...").start();
|
|
209
|
+
try {
|
|
210
|
+
const content = fs.readFileSync(options.file, "utf-8");
|
|
211
|
+
const rows = JSON.parse(content);
|
|
212
|
+
if (!Array.isArray(rows)) {
|
|
213
|
+
spinner.fail(chalk.red("El archivo debe contener un array de objetos"));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
let created = 0;
|
|
217
|
+
let errors = 0;
|
|
218
|
+
for (const row of rows) {
|
|
219
|
+
try {
|
|
220
|
+
await apiRequest(`/api/tables/${table}`, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
body: JSON.stringify(row),
|
|
223
|
+
});
|
|
224
|
+
created++;
|
|
225
|
+
spinner.text = `Creando registros... ${created}/${rows.length}`;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
errors++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (errors === 0) {
|
|
232
|
+
spinner.succeed(chalk.green(`${created} registros creados`));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
spinner.warn(chalk.yellow(`${created} creados, ${errors} errores`));
|
|
236
|
+
}
|
|
237
|
+
console.log();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
spinner.fail(chalk.red(error.message));
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,5 +20,14 @@
|
|
|
20
20
|
* gufi company:create Create a new company
|
|
21
21
|
* gufi automations List automation scripts
|
|
22
22
|
* gufi automation <name> View/edit automation code (--edit, --file)
|
|
23
|
+
*
|
|
24
|
+
* Row CRUD Commands:
|
|
25
|
+
* gufi rows <table> List rows from a table
|
|
26
|
+
* gufi row <table> <id> Get a single row
|
|
27
|
+
* gufi row:create <table> Create a new row (--data or --file)
|
|
28
|
+
* gufi row:update <table> <id> Update a row (--data or --file)
|
|
29
|
+
* gufi row:delete <table> <id> Delete a row
|
|
30
|
+
* gufi row:duplicate <table> <id> Duplicate a row
|
|
31
|
+
* gufi rows:create <table> Bulk create from JSON file
|
|
23
32
|
*/
|
|
24
33
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
* gufi company:create Create a new company
|
|
21
21
|
* gufi automations List automation scripts
|
|
22
22
|
* gufi automation <name> View/edit automation code (--edit, --file)
|
|
23
|
+
*
|
|
24
|
+
* Row CRUD Commands:
|
|
25
|
+
* gufi rows <table> List rows from a table
|
|
26
|
+
* gufi row <table> <id> Get a single row
|
|
27
|
+
* gufi row:create <table> Create a new row (--data or --file)
|
|
28
|
+
* gufi row:update <table> <id> Update a row (--data or --file)
|
|
29
|
+
* gufi row:delete <table> <id> Delete a row
|
|
30
|
+
* gufi row:duplicate <table> <id> Duplicate a row
|
|
31
|
+
* gufi rows:create <table> Bulk create from JSON file
|
|
23
32
|
*/
|
|
24
33
|
import { Command } from "commander";
|
|
25
34
|
import { loginCommand, logoutCommand, whoamiCommand } from "./commands/login.js";
|
|
@@ -29,6 +38,7 @@ import { watchCommand } from "./commands/watch.js";
|
|
|
29
38
|
import { listCommand } from "./commands/list.js";
|
|
30
39
|
import { logsCommand } from "./commands/logs.js";
|
|
31
40
|
import { companiesCommand, modulesCommand, moduleCommand, moduleUpdateCommand, companyCreateCommand, automationsCommand, automationCommand, } from "./commands/companies.js";
|
|
41
|
+
import { rowsListCommand, rowGetCommand, rowCreateCommand, rowUpdateCommand, rowDeleteCommand, rowDuplicateCommand, rowsBulkCreateCommand, } from "./commands/rows.js";
|
|
32
42
|
const program = new Command();
|
|
33
43
|
program
|
|
34
44
|
.name("gufi")
|
|
@@ -114,4 +124,45 @@ program
|
|
|
114
124
|
.option("-c, --company <id>", "Company ID (if not default)")
|
|
115
125
|
.option("-f, --file <path>", "Save code to file")
|
|
116
126
|
.action(automationCommand);
|
|
127
|
+
// ════════════════════════════════════════════════════════════════════
|
|
128
|
+
// Row CRUD Commands - Create, Read, Update, Delete rows
|
|
129
|
+
// ════════════════════════════════════════════════════════════════════
|
|
130
|
+
program
|
|
131
|
+
.command("rows <table>")
|
|
132
|
+
.description("List rows from a table")
|
|
133
|
+
.option("-l, --limit <n>", "Number of rows to fetch", "20")
|
|
134
|
+
.option("-o, --offset <n>", "Starting offset", "0")
|
|
135
|
+
.option("-s, --sort <field>", "Sort by field", "id")
|
|
136
|
+
.option("--order <dir>", "Sort order (ASC/DESC)", "DESC")
|
|
137
|
+
.option("-f, --filter <expr>", "Filter expression (field=value)")
|
|
138
|
+
.action(rowsListCommand);
|
|
139
|
+
program
|
|
140
|
+
.command("row <table> <id>")
|
|
141
|
+
.description("Get a single row by ID")
|
|
142
|
+
.action(rowGetCommand);
|
|
143
|
+
program
|
|
144
|
+
.command("row:create <table>")
|
|
145
|
+
.description("Create a new row")
|
|
146
|
+
.option("-d, --data <json>", "JSON data for the row")
|
|
147
|
+
.option("-f, --file <path>", "JSON file with row data")
|
|
148
|
+
.action(rowCreateCommand);
|
|
149
|
+
program
|
|
150
|
+
.command("row:update <table> <id>")
|
|
151
|
+
.description("Update a row by ID")
|
|
152
|
+
.option("-d, --data <json>", "JSON data to update")
|
|
153
|
+
.option("-f, --file <path>", "JSON file with update data")
|
|
154
|
+
.action(rowUpdateCommand);
|
|
155
|
+
program
|
|
156
|
+
.command("row:delete <table> <id>")
|
|
157
|
+
.description("Delete a row by ID")
|
|
158
|
+
.action(rowDeleteCommand);
|
|
159
|
+
program
|
|
160
|
+
.command("row:duplicate <table> <id>")
|
|
161
|
+
.description("Duplicate an existing row")
|
|
162
|
+
.action(rowDuplicateCommand);
|
|
163
|
+
program
|
|
164
|
+
.command("rows:create <table>")
|
|
165
|
+
.description("Bulk create rows from JSON file")
|
|
166
|
+
.option("-f, --file <path>", "JSON file with array of rows")
|
|
167
|
+
.action(rowsBulkCreateCommand);
|
|
117
168
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface Package {
|
|
|
25
25
|
}
|
|
26
26
|
export declare function login(email: string, password: string): Promise<{
|
|
27
27
|
token: string;
|
|
28
|
+
refreshToken?: string;
|
|
28
29
|
}>;
|
|
29
30
|
export declare function listPackages(): Promise<Package[]>;
|
|
30
31
|
export declare function listViews(packageId: number): Promise<View[]>;
|
package/dist/lib/api.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Gufi Dev CLI - API Client
|
|
3
3
|
* Communicates with Gufi Marketplace API
|
|
4
4
|
*/
|
|
5
|
-
import { getToken, getApiUrl } from "./config.js";
|
|
5
|
+
import { getToken, getApiUrl, getRefreshToken, setToken, loadConfig } from "./config.js";
|
|
6
6
|
class ApiError extends Error {
|
|
7
7
|
status;
|
|
8
8
|
constructor(status, message) {
|
|
@@ -11,8 +11,34 @@ class ApiError extends Error {
|
|
|
11
11
|
this.name = "ApiError";
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Auto-refresh the token if expired
|
|
15
|
+
async function refreshAccessToken() {
|
|
16
|
+
const refreshToken = getRefreshToken();
|
|
17
|
+
if (!refreshToken)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const url = `${getApiUrl()}/api/auth/refresh`;
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"X-Client": "cli",
|
|
26
|
+
"X-Refresh-Token": refreshToken,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
return null;
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
setToken(data.accessToken, config.email || "", data.refreshToken);
|
|
34
|
+
return data.accessToken;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function request(endpoint, options = {}, retryOnExpire = true) {
|
|
41
|
+
let token = getToken();
|
|
16
42
|
if (!token) {
|
|
17
43
|
throw new Error("No estás logueado. Ejecuta: gufi login");
|
|
18
44
|
}
|
|
@@ -22,9 +48,17 @@ async function request(endpoint, options = {}) {
|
|
|
22
48
|
headers: {
|
|
23
49
|
"Content-Type": "application/json",
|
|
24
50
|
Authorization: `Bearer ${token}`,
|
|
51
|
+
"X-Client": "cli",
|
|
25
52
|
...options.headers,
|
|
26
53
|
},
|
|
27
54
|
});
|
|
55
|
+
// Auto-refresh on 401/403 and retry once
|
|
56
|
+
if ((response.status === 401 || response.status === 403) && retryOnExpire) {
|
|
57
|
+
const newToken = await refreshAccessToken();
|
|
58
|
+
if (newToken) {
|
|
59
|
+
return request(endpoint, options, false); // Retry with new token
|
|
60
|
+
}
|
|
61
|
+
}
|
|
28
62
|
if (!response.ok) {
|
|
29
63
|
const text = await response.text();
|
|
30
64
|
throw new ApiError(response.status, `API Error ${response.status}: ${text}`);
|
|
@@ -36,14 +70,17 @@ export async function login(email, password) {
|
|
|
36
70
|
const url = `${getApiUrl()}/api/auth/login`;
|
|
37
71
|
const response = await fetch(url, {
|
|
38
72
|
method: "POST",
|
|
39
|
-
headers: {
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"X-Client": "cli", // 💜 Request refresh token in response
|
|
76
|
+
},
|
|
40
77
|
body: JSON.stringify({ email, password }),
|
|
41
78
|
});
|
|
42
79
|
if (!response.ok) {
|
|
43
80
|
throw new ApiError(response.status, "Credenciales inválidas");
|
|
44
81
|
}
|
|
45
82
|
const data = await response.json();
|
|
46
|
-
return { token: data.accessToken };
|
|
83
|
+
return { token: data.accessToken, refreshToken: data.refreshToken };
|
|
47
84
|
}
|
|
48
85
|
// ============ Packages ============
|
|
49
86
|
export async function listPackages() {
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export interface GufiConfig {
|
|
6
6
|
apiUrl: string;
|
|
7
7
|
token?: string;
|
|
8
|
+
refreshToken?: string;
|
|
8
9
|
email?: string;
|
|
9
10
|
currentView?: {
|
|
10
11
|
id: number;
|
|
@@ -17,7 +18,9 @@ export declare function ensureConfigDir(): void;
|
|
|
17
18
|
export declare function loadConfig(): GufiConfig;
|
|
18
19
|
export declare function saveConfig(config: GufiConfig): void;
|
|
19
20
|
export declare function getToken(): string | undefined;
|
|
20
|
-
export declare function setToken(token: string, email: string): void;
|
|
21
|
+
export declare function setToken(token: string, email: string, refreshToken?: string): void;
|
|
22
|
+
export declare function getRefreshToken(): string | undefined;
|
|
23
|
+
export declare function setRefreshToken(refreshToken: string): void;
|
|
21
24
|
export declare function clearToken(): void;
|
|
22
25
|
export declare function isLoggedIn(): boolean;
|
|
23
26
|
export declare function setCurrentView(view: GufiConfig["currentView"]): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -35,15 +35,27 @@ export function saveConfig(config) {
|
|
|
35
35
|
export function getToken() {
|
|
36
36
|
return loadConfig().token;
|
|
37
37
|
}
|
|
38
|
-
export function setToken(token, email) {
|
|
38
|
+
export function setToken(token, email, refreshToken) {
|
|
39
39
|
const config = loadConfig();
|
|
40
40
|
config.token = token;
|
|
41
41
|
config.email = email;
|
|
42
|
+
if (refreshToken) {
|
|
43
|
+
config.refreshToken = refreshToken;
|
|
44
|
+
}
|
|
45
|
+
saveConfig(config);
|
|
46
|
+
}
|
|
47
|
+
export function getRefreshToken() {
|
|
48
|
+
return loadConfig().refreshToken;
|
|
49
|
+
}
|
|
50
|
+
export function setRefreshToken(refreshToken) {
|
|
51
|
+
const config = loadConfig();
|
|
52
|
+
config.refreshToken = refreshToken;
|
|
42
53
|
saveConfig(config);
|
|
43
54
|
}
|
|
44
55
|
export function clearToken() {
|
|
45
56
|
const config = loadConfig();
|
|
46
57
|
delete config.token;
|
|
58
|
+
delete config.refreshToken;
|
|
47
59
|
delete config.email;
|
|
48
60
|
saveConfig(config);
|
|
49
61
|
}
|