@nubiia/mcp-freematica 0.7.0 → 0.9.0
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/README.md +20 -7
- package/dist/clients/fiql-builder.d.ts +32 -8
- package/dist/clients/fiql-builder.js +42 -14
- package/dist/clients/fiql-builder.js.map +1 -1
- package/dist/clients/freematica-client.d.ts +165 -34
- package/dist/clients/freematica-client.js +355 -96
- package/dist/clients/freematica-client.js.map +1 -1
- package/dist/schemas/cartera.d.ts +7 -5
- package/dist/schemas/cartera.js +13 -11
- package/dist/schemas/cartera.js.map +1 -1
- package/dist/schemas/clientes.d.ts +164 -0
- package/dist/schemas/clientes.js +205 -0
- package/dist/schemas/clientes.js.map +1 -0
- package/dist/schemas/facturas-ventas.d.ts +20 -16
- package/dist/schemas/facturas-ventas.js +36 -32
- package/dist/schemas/facturas-ventas.js.map +1 -1
- package/dist/schemas/localizaciones.d.ts +90 -0
- package/dist/schemas/localizaciones.js +145 -0
- package/dist/schemas/localizaciones.js.map +1 -0
- package/dist/schemas/pedidos-compras.d.ts +8 -6
- package/dist/schemas/pedidos-compras.js +8 -6
- package/dist/schemas/pedidos-compras.js.map +1 -1
- package/dist/server-instructions.js +43 -0
- package/dist/server-instructions.js.map +1 -1
- package/dist/server.js +5 -3
- package/dist/server.js.map +1 -1
- package/dist/tools/articulos.d.ts +14 -0
- package/dist/tools/articulos.js +183 -0
- package/dist/tools/articulos.js.map +1 -0
- package/dist/tools/cartera.js +2 -2
- package/dist/tools/cartera.js.map +1 -1
- package/dist/tools/clientes.d.ts +2 -1
- package/dist/tools/clientes.js +63 -2
- package/dist/tools/clientes.js.map +1 -1
- package/dist/tools/contabilidad.js +7 -7
- package/dist/tools/contabilidad.js.map +1 -1
- package/dist/tools/contactos-clientes.d.ts +2 -1
- package/dist/tools/contactos-clientes.js +58 -4
- package/dist/tools/contactos-clientes.js.map +1 -1
- package/dist/tools/contratos/cabecera.d.ts +2 -3
- package/dist/tools/contratos/cabecera.js.map +1 -1
- package/dist/tools/facturas-ventas.js +12 -12
- package/dist/tools/facturas-ventas.js.map +1 -1
- package/dist/tools/helpers.d.ts +4 -0
- package/dist/tools/helpers.js.map +1 -1
- package/dist/tools/localizaciones.d.ts +3 -2
- package/dist/tools/localizaciones.js +151 -13
- package/dist/tools/localizaciones.js.map +1 -1
- package/dist/tools/personal.js +13 -17
- package/dist/tools/personal.js.map +1 -1
- package/dist/tools/prl.js +15 -49
- package/dist/tools/prl.js.map +1 -1
- package/dist/tools/proveedores.d.ts +2 -2
- package/dist/tools/proveedores.js +8 -6
- package/dist/tools/proveedores.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,41 @@ import { buildFiql, appendRquery } from './fiql-builder.js';
|
|
|
3
3
|
import { CATALOG_ENDPOINTS } from '../schemas/master-data.js';
|
|
4
4
|
import { ESTADO_CARTERA_FIQL_MAP, } from '../schemas/cartera.js';
|
|
5
5
|
import { buildEstadoPedidoFiql, } from '../schemas/pedidos-compras.js';
|
|
6
|
+
import { LOC_FIELD_MAP } from '../schemas/localizaciones.js';
|
|
6
7
|
import { logger } from '../logger.js';
|
|
7
8
|
import { loadMaxResponseSizeMb } from '../utils/size-guardrail.js';
|
|
9
|
+
/**
|
|
10
|
+
* Día siguiente a una fecha ISO (YYYY-MM-DD), en formato ISO.
|
|
11
|
+
*
|
|
12
|
+
* Se usa para emular "hasta X inclusive" como `=lt='X+1'` en los endpoints
|
|
13
|
+
* donde el operador `=le=` responde 400/500 (facturas-cabecera, cartera,
|
|
14
|
+
* export-asientos — verificado contra el API real).
|
|
15
|
+
*/
|
|
16
|
+
function nextDayIso(isoDate) {
|
|
17
|
+
const fmt = (d) => {
|
|
18
|
+
// Formateo manual: toISOString() cambia de formato con años de 5+ dígitos
|
|
19
|
+
// (p.ej. 9999-12-31 + 1 día → '+010000-01-01…').
|
|
20
|
+
const y = String(d.getUTCFullYear()).padStart(4, '0');
|
|
21
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
22
|
+
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
23
|
+
return `${y}-${m}-${day}`;
|
|
24
|
+
};
|
|
25
|
+
const d = new Date(`${isoDate}T00:00:00Z`);
|
|
26
|
+
// Round-trip: V8 "arrastra" días fuera de rango (2026-02-30 → 2026-03-02)
|
|
27
|
+
// en vez de fallar; solo el round-trip detecta ambos casos (NaN y arrastre).
|
|
28
|
+
if (Number.isNaN(d.getTime()) || fmt(d) !== isoDate) {
|
|
29
|
+
throw new Error(`Fecha inválida: ${isoDate}. Usa una fecha de calendario real en formato YYYY-MM-DD.`);
|
|
30
|
+
}
|
|
31
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
32
|
+
return fmt(d);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Mensaje de error para los endpoints donde el API de Freemática devuelve
|
|
36
|
+
* 0 filas si se combinan dos condiciones de rango sobre el MISMO campo
|
|
37
|
+
* (verificado con ge+lt, ge+le, orden inverso y paréntesis).
|
|
38
|
+
*/
|
|
39
|
+
const RANGO_NO_SOPORTADO = 'El API de Freemática no soporta combinar fecha-desde y fecha-hasta a la vez ' +
|
|
40
|
+
'en este endpoint (devuelve 0 resultados). Usa solo uno de los dos límites por consulta.';
|
|
8
41
|
/**
|
|
9
42
|
* Typed client for the Freemática REST API.
|
|
10
43
|
*
|
|
@@ -31,6 +64,96 @@ export class FreematicaClient extends BaseClient {
|
|
|
31
64
|
const data = await this.get('/pvss/v2/contratos-servicios-material');
|
|
32
65
|
return { items: data.items, total: Number(data.total) };
|
|
33
66
|
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Artículos — catálogo de inventario (v0.9.0)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/**
|
|
71
|
+
* Lista paginada de artículos del catálogo de inventario.
|
|
72
|
+
*
|
|
73
|
+
* Endpoint: GET /part/v1/articulos
|
|
74
|
+
*
|
|
75
|
+
* Todos los filtros son de coincidencia EXACTA (el endpoint no soporta
|
|
76
|
+
* búsqueda parcial: =lk= responde 400 y los wildcards % devuelven 0
|
|
77
|
+
* resultados — verificado contra el API real). OJO: COD_ARTICULO puede
|
|
78
|
+
* llevar espacios iniciales en Freemática (ej. " QQ10615") que forman
|
|
79
|
+
* parte del código.
|
|
80
|
+
*
|
|
81
|
+
* El filtro `activo` mapea a MOTIVO_BAJA (columna de texto — las columnas
|
|
82
|
+
* de fecha no permiten comparar con vacío):
|
|
83
|
+
* - `true` → MOTIVO_BAJA=='' (sin motivo de baja).
|
|
84
|
+
* - `false` → MOTIVO_BAJA!='' (con motivo de baja).
|
|
85
|
+
* Los artículos con MOTIVO_BAJA nulo (≈36 de 6304 en producción) no
|
|
86
|
+
* matchean ninguno de los dos valores.
|
|
87
|
+
*
|
|
88
|
+
* @param opts - Paginación y filtros opcionales.
|
|
89
|
+
* @returns Lista paginada de artículos (cada item incluye `idReg`).
|
|
90
|
+
*/
|
|
91
|
+
async listArticulos(opts) {
|
|
92
|
+
const url = new URL('placeholder://x/part/v1/articulos');
|
|
93
|
+
if (opts.items !== undefined)
|
|
94
|
+
url.searchParams.set('items', String(opts.items));
|
|
95
|
+
if (opts.page !== undefined)
|
|
96
|
+
url.searchParams.set('page', String(opts.page));
|
|
97
|
+
const fiqlGroup = {};
|
|
98
|
+
if (opts.codArticulo !== undefined)
|
|
99
|
+
fiqlGroup['COD_ARTICULO'] = opts.codArticulo;
|
|
100
|
+
if (opts.tipoCodigo !== undefined)
|
|
101
|
+
fiqlGroup['TIPO_CODIGO'] = opts.tipoCodigo;
|
|
102
|
+
if (opts.codProveedor !== undefined)
|
|
103
|
+
fiqlGroup['COD_PROVEEDOR'] = opts.codProveedor;
|
|
104
|
+
if (opts.linea !== undefined)
|
|
105
|
+
fiqlGroup['COD_LIN_ART'] = opts.linea;
|
|
106
|
+
if (opts.familia !== undefined)
|
|
107
|
+
fiqlGroup['COD_FAMILIA'] = opts.familia;
|
|
108
|
+
if (opts.subfamilia !== undefined)
|
|
109
|
+
fiqlGroup['COD_SUBFAM'] = opts.subfamilia;
|
|
110
|
+
if (opts.descripcion !== undefined)
|
|
111
|
+
fiqlGroup['DESC_ART'] = opts.descripcion;
|
|
112
|
+
if (opts.codigoBarras !== undefined)
|
|
113
|
+
fiqlGroup['CODIGO_BARRAS'] = opts.codigoBarras;
|
|
114
|
+
if (opts.activo === true)
|
|
115
|
+
fiqlGroup['MOTIVO_BAJA'] = '';
|
|
116
|
+
if (opts.activo === false)
|
|
117
|
+
fiqlGroup['MOTIVO_BAJA'] = { op: 'ne', value: '' };
|
|
118
|
+
appendRquery(url, buildFiql(fiqlGroup));
|
|
119
|
+
const qs = url.searchParams.toString();
|
|
120
|
+
const path = qs ? `/part/v1/articulos?${qs}` : '/part/v1/articulos';
|
|
121
|
+
const data = await this.get(path);
|
|
122
|
+
return { items: data.items, total: Number(data.total) };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Detalle de un artículo por `idReg` opaco.
|
|
126
|
+
*
|
|
127
|
+
* Endpoint: GET /part/v1/articulos/{idreg}
|
|
128
|
+
*
|
|
129
|
+
* El API devuelve un envelope de LISTA con un único item (verificado contra
|
|
130
|
+
* el API real), no un objeto detalle; este método lo desenvuelve.
|
|
131
|
+
*
|
|
132
|
+
* @param idReg - Identificador opaco (base64) del artículo.
|
|
133
|
+
* @returns Objeto con todos los campos del artículo.
|
|
134
|
+
*/
|
|
135
|
+
async getArticulo(idReg) {
|
|
136
|
+
const data = await this.get(`/part/v1/articulos/${encodeURIComponent(idReg)}`);
|
|
137
|
+
const item = data.items?.[0];
|
|
138
|
+
if (item === undefined) {
|
|
139
|
+
throw new FreematicaError('not_found', `Artículo no encontrado: ${idReg}`);
|
|
140
|
+
}
|
|
141
|
+
return item;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Precios de venta de un artículo por `idReg` opaco.
|
|
145
|
+
*
|
|
146
|
+
* Endpoint: GET /pgrl/v1/precio-articulo/{idreg}
|
|
147
|
+
*
|
|
148
|
+
* Devuelve un objeto con PRECIO_VENTA, DESCUENTO y FACTURABLE.
|
|
149
|
+
*
|
|
150
|
+
* @param idReg - Identificador opaco (base64) del artículo (el mismo que
|
|
151
|
+
* devuelve freematica_list_articulos).
|
|
152
|
+
* @returns Objeto de precios del artículo.
|
|
153
|
+
*/
|
|
154
|
+
async getPrecioArticulo(idReg) {
|
|
155
|
+
return this.get(`/pgrl/v1/precio-articulo/${encodeURIComponent(idReg)}`);
|
|
156
|
+
}
|
|
34
157
|
/**
|
|
35
158
|
* Obtener un catálogo de datos maestros.
|
|
36
159
|
*/
|
|
@@ -163,13 +286,15 @@ export class FreematicaClient extends BaseClient {
|
|
|
163
286
|
*
|
|
164
287
|
* Endpoint: GET /pprl/v1/vigilancia-salud
|
|
165
288
|
*
|
|
166
|
-
*
|
|
167
|
-
* codifican en FIQL y se envían como `rquery`.
|
|
168
|
-
*
|
|
169
|
-
* @param opts - Opciones de paginación y filtrado.
|
|
289
|
+
* @param opts - Paginación y filtro opcional idRegPersona.
|
|
170
290
|
* @returns Lista paginada de registros VS.
|
|
171
291
|
*/
|
|
172
292
|
async listVigilanciaSalud(opts) {
|
|
293
|
+
// El endpoint IGNORA el parámetro `rquery` por completo: cualquier FIQL
|
|
294
|
+
// (incluso con campos inexistentes) responde 200 con el dataset completo
|
|
295
|
+
// sin filtrar (verificado contra el API real). El único filtro operativo
|
|
296
|
+
// es el query param nativo `idRegPersona` (el idReg de la persona en
|
|
297
|
+
// freematica_list_personal), que sí filtra correctamente.
|
|
173
298
|
const url = new URL('placeholder://x/pprl/v1/vigilancia-salud');
|
|
174
299
|
if (opts.items !== undefined)
|
|
175
300
|
url.searchParams.set('items', String(opts.items));
|
|
@@ -177,28 +302,6 @@ export class FreematicaClient extends BaseClient {
|
|
|
177
302
|
url.searchParams.set('page', String(opts.page));
|
|
178
303
|
if (opts.idRegPersona !== undefined)
|
|
179
304
|
url.searchParams.set('idRegPersona', opts.idRegPersona);
|
|
180
|
-
// Filtros FIQL
|
|
181
|
-
const fiqlParts = {};
|
|
182
|
-
if (opts.empresa !== undefined)
|
|
183
|
-
fiqlParts['PERVS_EMP'] = opts.empresa;
|
|
184
|
-
if (opts.delegacion !== undefined)
|
|
185
|
-
fiqlParts['PERVS_DELEG'] = opts.delegacion;
|
|
186
|
-
if (opts.codPersona !== undefined)
|
|
187
|
-
fiqlParts['PERVS_PERSO'] = opts.codPersona;
|
|
188
|
-
if (opts.tipoRevision !== undefined)
|
|
189
|
-
fiqlParts['PERVS_TIPO_REVISION'] = opts.tipoRevision;
|
|
190
|
-
if (opts.resultado !== undefined)
|
|
191
|
-
fiqlParts['PERVS_RESULTADO'] = opts.resultado;
|
|
192
|
-
if (opts.fechaCitaDesde !== undefined)
|
|
193
|
-
fiqlParts['PERVS_FCH_CITA'] = { op: 'ge', value: opts.fechaCitaDesde };
|
|
194
|
-
// If both desde and hasta, we need to combine with AND
|
|
195
|
-
const fiqlParts2 = {};
|
|
196
|
-
if (opts.fechaCitaHasta !== undefined)
|
|
197
|
-
fiqlParts2['PERVS_FCH_CITA'] = { op: 'le', value: opts.fechaCitaHasta };
|
|
198
|
-
const fiql1 = buildFiql(fiqlParts);
|
|
199
|
-
const fiql2 = buildFiql(fiqlParts2);
|
|
200
|
-
const combinedFiql = [fiql1, fiql2].filter(Boolean).join(';');
|
|
201
|
-
appendRquery(url, combinedFiql);
|
|
202
305
|
const qs = url.searchParams.toString();
|
|
203
306
|
const path = qs ? `/pprl/v1/vigilancia-salud?${qs}` : '/pprl/v1/vigilancia-salud';
|
|
204
307
|
const data = await this.get(path);
|
|
@@ -263,18 +366,23 @@ export class FreematicaClient extends BaseClient {
|
|
|
263
366
|
/**
|
|
264
367
|
* Lista paginada de personas (empleados) con filtros FIQL opcionales.
|
|
265
368
|
*
|
|
266
|
-
* Endpoint: GET /pers/
|
|
369
|
+
* Endpoint: GET /pers/v1/personal
|
|
267
370
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* para estos campos).
|
|
371
|
+
* Todos los filtros son de coincidencia EXACTA: el endpoint no soporta
|
|
372
|
+
* búsqueda parcial (=lk= responde 400 y los wildcards % / * devuelven 0
|
|
373
|
+
* resultados — verificado contra el API real).
|
|
272
374
|
*
|
|
273
375
|
* @param opts - Opciones de paginación y filtrado.
|
|
274
376
|
* @returns Lista paginada de personas.
|
|
275
377
|
*/
|
|
276
378
|
async listPersonal(opts) {
|
|
277
|
-
|
|
379
|
+
// v1, no v2: /pers/v2/personal es un endpoint de sincronización incremental
|
|
380
|
+
// que exige el parámetro nativo `fchmodificacion` (sin él → 400 "Parámetro
|
|
381
|
+
// [fchmodificacion] obligatorio") y devuelve un subconjunto del dataset.
|
|
382
|
+
// /pers/v1/personal devuelve el dataset completo con idReg. Verificado
|
|
383
|
+
// contra el API real (v1: 5482 personas; v2 con fchmodificacion=1900-01-01:
|
|
384
|
+
// 3984). El campo VSSPER_ACTIVO no existe en v1 (filtrarlo → 400).
|
|
385
|
+
const url = new URL('placeholder://x/pers/v1/personal');
|
|
278
386
|
if (opts.items !== undefined)
|
|
279
387
|
url.searchParams.set('items', String(opts.items));
|
|
280
388
|
if (opts.page !== undefined)
|
|
@@ -298,24 +406,32 @@ export class FreematicaClient extends BaseClient {
|
|
|
298
406
|
fiqlGroup['VSSPER_DPTO'] = opts.departamento;
|
|
299
407
|
if (opts.seccion !== undefined)
|
|
300
408
|
fiqlGroup['VSSPER_SECCION'] = opts.seccion;
|
|
301
|
-
if (opts.activo !== undefined)
|
|
302
|
-
fiqlGroup['VSSPER_ACTIVO'] = opts.activo ? 'S' : 'N';
|
|
303
409
|
appendRquery(url, buildFiql(fiqlGroup));
|
|
304
410
|
const qs = url.searchParams.toString();
|
|
305
|
-
const path = qs ? `/pers/
|
|
411
|
+
const path = qs ? `/pers/v1/personal?${qs}` : '/pers/v1/personal';
|
|
306
412
|
const data = await this.get(path);
|
|
307
413
|
return { items: data.items, total: Number(data.total) };
|
|
308
414
|
}
|
|
309
415
|
/**
|
|
310
|
-
* Detalle de una persona por `idReg` opaco.
|
|
416
|
+
* Detalle de una persona por su `idReg` opaco.
|
|
311
417
|
*
|
|
312
|
-
* Endpoint: GET /pers/
|
|
418
|
+
* Endpoint: GET /pers/v1/personal/{idreg}
|
|
313
419
|
*
|
|
314
|
-
*
|
|
420
|
+
* v1, igual que el listado: garantiza que todo idReg devuelto por
|
|
421
|
+
* freematica_list_personal funciona aquí (la vista v2 es un subconjunto
|
|
422
|
+
* de sincronización incremental). El API devuelve un envelope de LISTA
|
|
423
|
+
* con un único item (verificado contra el API real); se desenvuelve.
|
|
424
|
+
*
|
|
425
|
+
* @param idReg - Identificador opaco (base64) de la persona.
|
|
315
426
|
* @returns Objeto con todos los campos de la persona.
|
|
316
427
|
*/
|
|
317
428
|
async getPersona(idReg) {
|
|
318
|
-
|
|
429
|
+
const data = await this.get(`/pers/v1/personal/${encodeURIComponent(idReg)}`);
|
|
430
|
+
const item = data.items?.[0];
|
|
431
|
+
if (item === undefined) {
|
|
432
|
+
throw new FreematicaError('not_found', `Persona no encontrada: ${idReg}`);
|
|
433
|
+
}
|
|
434
|
+
return item;
|
|
319
435
|
}
|
|
320
436
|
// ---------------------------------------------------------------------------
|
|
321
437
|
// Calendarios laborales (v0.5.0)
|
|
@@ -371,13 +487,19 @@ export class FreematicaClient extends BaseClient {
|
|
|
371
487
|
url.searchParams.set('cal', opts.cal);
|
|
372
488
|
if (opts.periodo !== undefined)
|
|
373
489
|
url.searchParams.set('periodo', opts.periodo);
|
|
374
|
-
// Construir FIQL para filtros adicionales
|
|
490
|
+
// Construir FIQL para filtros adicionales.
|
|
491
|
+
// ASI_FCHASI: `=ge=` funciona; `=le=` responde 400 (se emula con `=lt=`
|
|
492
|
+
// del día siguiente); combinar ambos límites devuelve 0 filas (bug del
|
|
493
|
+
// API verificado en producción) → se rechaza.
|
|
494
|
+
if (opts.fechaDesde !== undefined && opts.fechaHasta !== undefined) {
|
|
495
|
+
throw new Error(RANGO_NO_SOPORTADO);
|
|
496
|
+
}
|
|
375
497
|
const andGroups = [];
|
|
376
498
|
if (opts.fechaDesde !== undefined) {
|
|
377
499
|
andGroups.push({ ASI_FCHASI: { op: 'ge', value: opts.fechaDesde } });
|
|
378
500
|
}
|
|
379
501
|
if (opts.fechaHasta !== undefined) {
|
|
380
|
-
andGroups.push({ ASI_FCHASI: { op: '
|
|
502
|
+
andGroups.push({ ASI_FCHASI: { op: 'lt', value: nextDayIso(opts.fechaHasta) } });
|
|
381
503
|
}
|
|
382
504
|
if (opts.diario !== undefined) {
|
|
383
505
|
andGroups.push({ ASI_DIARIO: opts.diario });
|
|
@@ -495,12 +617,11 @@ export class FreematicaClient extends BaseClient {
|
|
|
495
617
|
* Construye la FIQL a partir de los filtros tipados y la añade como `rquery`
|
|
496
618
|
* usando `appendRquery()` (patrón canónico del foundation TD-117).
|
|
497
619
|
*
|
|
498
|
-
* El filtro `soloImpagados=true` genera `CARCL_FECIMPAG
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* consistente con el resto de filtros.
|
|
620
|
+
* El filtro `soloImpagados=true` genera `CARCL_FECIMPAG=ge='1900-01-01'`
|
|
621
|
+
* (cualquier fecha de impago con valor). El FIQL de Freemática no tiene
|
|
622
|
+
* IS NOT NULL: `!=null` devuelve 0 resultados silenciosamente y `!='null'`
|
|
623
|
+
* responde 500 (verificado contra el API real; con este rango devuelve
|
|
624
|
+
* 382 impagados de 72054 documentos).
|
|
504
625
|
*
|
|
505
626
|
* El filtro `estado` mapea a los valores numéricos de `CARCL_SITCAR`
|
|
506
627
|
* (1=pendiente, 2=cancelado, 3=derivado).
|
|
@@ -539,20 +660,27 @@ export class FreematicaClient extends BaseClient {
|
|
|
539
660
|
const eqFiql = buildFiql(eqGroup);
|
|
540
661
|
if (eqFiql)
|
|
541
662
|
parts.push(eqFiql);
|
|
542
|
-
// Range filters:
|
|
663
|
+
// Range filters. CARCL_FECDOC: `=ge=` funciona; `=le=` responde 500
|
|
664
|
+
// (hasta se emula con `=lt=` del día siguiente); combinar desde+hasta
|
|
665
|
+
// devuelve 0 filas (bug del API verificado en producción) → se rechaza.
|
|
666
|
+
// CARCL_FECVCTO: solo `=ge=` funciona (le/lt se ignoran silenciosamente
|
|
667
|
+
// devolviendo el dataset completo), así que fechaVencimientoHasta no
|
|
668
|
+
// existe como filtro.
|
|
669
|
+
if (opts.fechaDocDesde !== undefined && opts.fechaDocHasta !== undefined) {
|
|
670
|
+
throw new Error(RANGO_NO_SOPORTADO);
|
|
671
|
+
}
|
|
543
672
|
if (opts.fechaDocDesde !== undefined)
|
|
544
673
|
parts.push(buildFiql({ CARCL_FECDOC: { op: 'ge', value: opts.fechaDocDesde } }));
|
|
545
674
|
if (opts.fechaDocHasta !== undefined)
|
|
546
|
-
parts.push(buildFiql({ CARCL_FECDOC: { op: '
|
|
675
|
+
parts.push(buildFiql({ CARCL_FECDOC: { op: 'lt', value: nextDayIso(opts.fechaDocHasta) } }));
|
|
547
676
|
if (opts.fechaVencimientoDesde !== undefined)
|
|
548
677
|
parts.push(buildFiql({ CARCL_FECVCTO: { op: 'ge', value: opts.fechaVencimientoDesde } }));
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
// garantizar escaping uniforme y legibilidad del código.
|
|
678
|
+
// soloImpagados: el FIQL de Freemática no soporta IS NOT NULL (`!=null`
|
|
679
|
+
// devuelve 0 resultados y `!='null'` responde 500 — verificado contra el
|
|
680
|
+
// API real). El rango de fecha equivale: cualquier CARCL_FECIMPAG con
|
|
681
|
+
// valor es >= 1900-01-01.
|
|
554
682
|
if (opts.soloImpagados === true) {
|
|
555
|
-
parts.push(buildFiql({ CARCL_FECIMPAG: { op: '
|
|
683
|
+
parts.push(buildFiql({ CARCL_FECIMPAG: { op: 'ge', value: '1900-01-01' } }));
|
|
556
684
|
}
|
|
557
685
|
appendRquery(url, parts.join(';'));
|
|
558
686
|
const path = url.pathname + (url.search ? url.search : '');
|
|
@@ -691,13 +819,16 @@ export class FreematicaClient extends BaseClient {
|
|
|
691
819
|
/**
|
|
692
820
|
* Lista paginada de proveedores con filtros FIQL.
|
|
693
821
|
*
|
|
694
|
-
* El filtro `activo` mapea a FECHA_BAJA
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
* - `
|
|
822
|
+
* El filtro `activo` mapea a FECHA_BAJA. El API no soporta IS NULL en FIQL
|
|
823
|
+
* (`==null` devuelve 0 resultados y `=='null'` responde 500 — verificado
|
|
824
|
+
* contra el API real), así que:
|
|
825
|
+
* - `false` → `FECHA_BAJA=ge='1900-01-01'` (cualquier fecha de baja).
|
|
826
|
+
* - `true` → sin filtro server-side; las bajas se descartan post-process
|
|
827
|
+
* sobre la página recibida (FECHA_BAJA con valor). El `total`
|
|
828
|
+
* devuelto es el del dataset sin filtrar.
|
|
699
829
|
*
|
|
700
|
-
* El filtro `nombre`
|
|
830
|
+
* El filtro `nombre` es coincidencia EXACTA: el operador `=lk=` responde
|
|
831
|
+
* 400 en este endpoint y los wildcards % devuelven 0 resultados.
|
|
701
832
|
*
|
|
702
833
|
* Endpoint: GET /pgrl/v2/proveedores
|
|
703
834
|
*/
|
|
@@ -717,21 +848,25 @@ export class FreematicaClient extends BaseClient {
|
|
|
717
848
|
COD_PAIS: opts.codPais,
|
|
718
849
|
};
|
|
719
850
|
if (opts.nombre !== undefined) {
|
|
720
|
-
filters['NOMBRE_PRO'] =
|
|
721
|
-
}
|
|
722
|
-
if (opts.activo === true) {
|
|
723
|
-
// Activos: FECHA_BAJA nula. FIQL: FECHA_BAJA==null
|
|
724
|
-
filters['FECHA_BAJA'] = 'null';
|
|
851
|
+
filters['NOMBRE_PRO'] = opts.nombre;
|
|
725
852
|
}
|
|
726
|
-
|
|
727
|
-
// Dados de baja: FECHA_BAJA
|
|
728
|
-
|
|
853
|
+
if (opts.activo === false) {
|
|
854
|
+
// Dados de baja: cualquier FECHA_BAJA con valor. IS NOT NULL no existe
|
|
855
|
+
// en el FIQL de Freemática; el rango de fecha equivale.
|
|
856
|
+
filters['FECHA_BAJA'] = { op: 'ge', value: '1900-01-01' };
|
|
729
857
|
}
|
|
730
858
|
const fiql = buildFiql(filters);
|
|
731
859
|
appendRquery(url, fiql);
|
|
732
860
|
const path = url.pathname + (url.search ? url.search : '');
|
|
733
861
|
const data = await this.get(path);
|
|
734
|
-
|
|
862
|
+
// activo=true: descartar bajas de la página (no expresable en FIQL).
|
|
863
|
+
const items = opts.activo === true
|
|
864
|
+
? data.items.filter((p) => {
|
|
865
|
+
const fb = p['FECHA_BAJA'];
|
|
866
|
+
return fb === undefined || fb === null || String(fb).trim() === '';
|
|
867
|
+
})
|
|
868
|
+
: data.items;
|
|
869
|
+
return { items, total: Number(data.total) };
|
|
735
870
|
}
|
|
736
871
|
// ---------------------------------------------------------------------------
|
|
737
872
|
// Facturas de ventas (v0.5.0)
|
|
@@ -751,33 +886,43 @@ export class FreematicaClient extends BaseClient {
|
|
|
751
886
|
if (opts.page !== undefined)
|
|
752
887
|
url.searchParams.set('page', String(opts.page));
|
|
753
888
|
const fiqlParts = [];
|
|
754
|
-
// Standard equality filters
|
|
889
|
+
// Standard equality filters. Nombres de columna verificados contra el API
|
|
890
|
+
// real: la vista de facturas-cabecera expone FVC_CODEMP, FVC_CODCLI,
|
|
891
|
+
// FVC_CODREPRES, FVC_SERIEFRA, FVC_NUMFRA, FVC_FPAGO, FVC_FCHFAC y
|
|
892
|
+
// FVC_TRASP_CONTAB ('1'/'0'). Los nombres anteriores (FVC_EMP, FVC_CODAUX,
|
|
893
|
+
// FVC_CODREP, FVC_SERFAC, FVC_NUMFAC, FVC_CODFPAG, FVC_FECFAC,
|
|
894
|
+
// FVC_TRSCONT) no existen y provocaban 500 en cada consulta con filtro.
|
|
755
895
|
const eqGroup = {};
|
|
756
896
|
if (opts.empresa !== undefined)
|
|
757
|
-
eqGroup['
|
|
897
|
+
eqGroup['FVC_CODEMP'] = opts.empresa;
|
|
758
898
|
if (opts.codCliente !== undefined)
|
|
759
|
-
eqGroup['
|
|
899
|
+
eqGroup['FVC_CODCLI'] = opts.codCliente;
|
|
760
900
|
if (opts.representante !== undefined)
|
|
761
|
-
eqGroup['
|
|
901
|
+
eqGroup['FVC_CODREPRES'] = opts.representante;
|
|
762
902
|
if (opts.serie !== undefined)
|
|
763
|
-
eqGroup['
|
|
903
|
+
eqGroup['FVC_SERIEFRA'] = opts.serie;
|
|
764
904
|
if (opts.numFactura !== undefined)
|
|
765
|
-
eqGroup['
|
|
905
|
+
eqGroup['FVC_NUMFRA'] = opts.numFactura;
|
|
766
906
|
if (opts.formaPago !== undefined)
|
|
767
|
-
eqGroup['
|
|
907
|
+
eqGroup['FVC_FPAGO'] = opts.formaPago;
|
|
768
908
|
if (opts.delegacion !== undefined)
|
|
769
909
|
eqGroup['FVC_DELEG'] = opts.delegacion;
|
|
770
910
|
const eqFiql = buildFiql(eqGroup);
|
|
771
911
|
if (eqFiql)
|
|
772
912
|
fiqlParts.push(eqFiql);
|
|
773
|
-
// Date range filters
|
|
913
|
+
// Date range filters. FVC_FCHFAC: `=ge=` funciona; `=le=`/`=gt=` responden
|
|
914
|
+
// 500 (hasta se emula con `=lt=` del día siguiente); combinar desde+hasta
|
|
915
|
+
// devuelve 0 filas (bug del API verificado en producción) → se rechaza.
|
|
916
|
+
if (opts.fechaFacturaDesde !== undefined && opts.fechaFacturaHasta !== undefined) {
|
|
917
|
+
throw new Error(RANGO_NO_SOPORTADO);
|
|
918
|
+
}
|
|
774
919
|
if (opts.fechaFacturaDesde !== undefined)
|
|
775
|
-
fiqlParts.push(buildFiql({
|
|
920
|
+
fiqlParts.push(buildFiql({ FVC_FCHFAC: { op: 'ge', value: opts.fechaFacturaDesde } }));
|
|
776
921
|
if (opts.fechaFacturaHasta !== undefined)
|
|
777
|
-
fiqlParts.push(buildFiql({
|
|
778
|
-
// Boolean: traspasadoContabilidad
|
|
922
|
+
fiqlParts.push(buildFiql({ FVC_FCHFAC: { op: 'lt', value: nextDayIso(opts.fechaFacturaHasta) } }));
|
|
923
|
+
// Boolean: traspasadoContabilidad — la columna real usa '1'/'0'
|
|
779
924
|
if (opts.traspasadoContabilidad !== undefined) {
|
|
780
|
-
fiqlParts.push(buildFiql({
|
|
925
|
+
fiqlParts.push(buildFiql({ FVC_TRASP_CONTAB: opts.traspasadoContabilidad ? '1' : '0' }));
|
|
781
926
|
}
|
|
782
927
|
appendRquery(url, fiqlParts.join(';'));
|
|
783
928
|
const path = url.pathname + (url.search ? url.search : '');
|
|
@@ -827,7 +972,11 @@ export class FreematicaClient extends BaseClient {
|
|
|
827
972
|
* Lista paginada de localizaciones de servicio de clientes.
|
|
828
973
|
*
|
|
829
974
|
* Filtros soportados: codCliente (COD_CLI), grupoCliente (GRUPO_CLI), codPais (COD_PAIS),
|
|
830
|
-
* codProvincia (COD_PROVINCIA), representante (COD_REPRES)
|
|
975
|
+
* codProvincia (COD_PROVINCIA), representante (COD_REPRES).
|
|
976
|
+
*
|
|
977
|
+
* La vista NO tiene columna FECHA_BAJA (filtrar por ella responde 400
|
|
978
|
+
* "Error en rquery" — verificado contra el API real), así que no existe
|
|
979
|
+
* filtro activo/baja en este recurso.
|
|
831
980
|
*
|
|
832
981
|
* Endpoint: GET /pgrl/v2/localizaciones-servicio-clientes
|
|
833
982
|
*/
|
|
@@ -839,12 +988,6 @@ export class FreematicaClient extends BaseClient {
|
|
|
839
988
|
COD_PROVINCIA: opts.codProvincia,
|
|
840
989
|
COD_REPRES: opts.representante,
|
|
841
990
|
};
|
|
842
|
-
if (opts.activo === true) {
|
|
843
|
-
fiqlFilters['FECHA_BAJA'] = 'null';
|
|
844
|
-
}
|
|
845
|
-
else if (opts.activo === false) {
|
|
846
|
-
fiqlFilters['FECHA_BAJA'] = { op: 'ne', value: 'null' };
|
|
847
|
-
}
|
|
848
991
|
return this.listResourceWithFiql('/pgrl/v2/localizaciones-servicio-clientes', opts, fiqlFilters);
|
|
849
992
|
}
|
|
850
993
|
/**
|
|
@@ -873,13 +1016,15 @@ export class FreematicaClient extends BaseClient {
|
|
|
873
1016
|
url.searchParams.set('items', String(opts.items));
|
|
874
1017
|
if (opts.page !== undefined)
|
|
875
1018
|
url.searchParams.set('page', String(opts.page));
|
|
1019
|
+
// Nombres de columna verificados contra el API real (las líneas exponen
|
|
1020
|
+
// FVL_CODARTIC, FVL_COD_FAMILIA, FVL_COD_SUBFAM y FVL_DELEG).
|
|
876
1021
|
const fiqlGroup = {};
|
|
877
1022
|
if (opts.codArticulo !== undefined)
|
|
878
|
-
fiqlGroup['
|
|
1023
|
+
fiqlGroup['FVL_CODARTIC'] = opts.codArticulo;
|
|
879
1024
|
if (opts.codFamilia !== undefined)
|
|
880
|
-
fiqlGroup['
|
|
1025
|
+
fiqlGroup['FVL_COD_FAMILIA'] = opts.codFamilia;
|
|
881
1026
|
if (opts.codSubfamilia !== undefined)
|
|
882
|
-
fiqlGroup['
|
|
1027
|
+
fiqlGroup['FVL_COD_SUBFAM'] = opts.codSubfamilia;
|
|
883
1028
|
if (opts.delegacion !== undefined)
|
|
884
1029
|
fiqlGroup['FVL_DELEG'] = opts.delegacion;
|
|
885
1030
|
appendRquery(url, buildFiql(fiqlGroup));
|
|
@@ -902,9 +1047,10 @@ export class FreematicaClient extends BaseClient {
|
|
|
902
1047
|
url.searchParams.set('items', String(opts.items));
|
|
903
1048
|
if (opts.page !== undefined)
|
|
904
1049
|
url.searchParams.set('page', String(opts.page));
|
|
1050
|
+
// Columna real: FVI_TIPO_IVA (verificado contra el API; FVI_TIPIVA no existe).
|
|
905
1051
|
const fiqlGroup = {};
|
|
906
1052
|
if (opts.tipoIva !== undefined)
|
|
907
|
-
fiqlGroup['
|
|
1053
|
+
fiqlGroup['FVI_TIPO_IVA'] = opts.tipoIva;
|
|
908
1054
|
appendRquery(url, buildFiql(fiqlGroup));
|
|
909
1055
|
const path = url.pathname + (url.search ? url.search : '');
|
|
910
1056
|
const data = await this.get(path);
|
|
@@ -925,13 +1071,18 @@ export class FreematicaClient extends BaseClient {
|
|
|
925
1071
|
url.searchParams.set('items', String(opts.items));
|
|
926
1072
|
if (opts.page !== undefined)
|
|
927
1073
|
url.searchParams.set('page', String(opts.page));
|
|
1074
|
+
// Columnas reales: FVV_MODOPAGO y FVV_FCH_VTO (verificado contra el API;
|
|
1075
|
+
// FVV_CODMPAG y FVV_FECVCTO no existen). A diferencia de FVC_FCHFAC en la
|
|
1076
|
+
// cabecera, en este sub-recurso `=le=` responde 200 y ge+le combinados
|
|
1077
|
+
// devuelven el registro esperado (verificado en producción con una factura
|
|
1078
|
+
// de 1 vencimiento), así que se mantienen el =le= y el rango.
|
|
928
1079
|
const fiqlParts = [];
|
|
929
1080
|
if (opts.modoPago !== undefined)
|
|
930
|
-
fiqlParts.push(buildFiql({
|
|
1081
|
+
fiqlParts.push(buildFiql({ FVV_MODOPAGO: opts.modoPago }));
|
|
931
1082
|
if (opts.fechaVencimientoDesde !== undefined)
|
|
932
|
-
fiqlParts.push(buildFiql({
|
|
1083
|
+
fiqlParts.push(buildFiql({ FVV_FCH_VTO: { op: 'ge', value: opts.fechaVencimientoDesde } }));
|
|
933
1084
|
if (opts.fechaVencimientoHasta !== undefined)
|
|
934
|
-
fiqlParts.push(buildFiql({
|
|
1085
|
+
fiqlParts.push(buildFiql({ FVV_FCH_VTO: { op: 'le', value: opts.fechaVencimientoHasta } }));
|
|
935
1086
|
appendRquery(url, fiqlParts.join(';'));
|
|
936
1087
|
const path = url.pathname + (url.search ? url.search : '');
|
|
937
1088
|
const data = await this.get(path);
|
|
@@ -1519,6 +1670,114 @@ export class FreematicaClient extends BaseClient {
|
|
|
1519
1670
|
logger.debug({ operation, endpoint, body }, 'freematica write body');
|
|
1520
1671
|
}
|
|
1521
1672
|
// ---------------------------------------------------------------------------
|
|
1673
|
+
// Clientes, Contactos y Localizaciones — escritura (v0.8.0, sin delete)
|
|
1674
|
+
// ---------------------------------------------------------------------------
|
|
1675
|
+
/**
|
|
1676
|
+
* Alta de cliente.
|
|
1677
|
+
*
|
|
1678
|
+
* Endpoint: POST /pgrl/v2/clientes — body VoClientes.
|
|
1679
|
+
* Requeridos por el API: idReg, COD_GRUPO_CLI, COD_CLI, NOMBRE_CLI, NIF,
|
|
1680
|
+
* TIPO_IMPTO, COD_DIVISA, TIPO_FACT.
|
|
1681
|
+
*/
|
|
1682
|
+
async createCliente(body) {
|
|
1683
|
+
this.logWrite('createCliente', 'POST /pgrl/v2/clientes', body);
|
|
1684
|
+
return this.post('/pgrl/v2/clientes', body);
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Actualización de cliente.
|
|
1688
|
+
*
|
|
1689
|
+
* Endpoint: PUT /pgrl/v2/clientes/{idReg} — body VoClientes completo
|
|
1690
|
+
* (el caller hace fetch+merge antes de llamar).
|
|
1691
|
+
*/
|
|
1692
|
+
async updateCliente(idReg, body) {
|
|
1693
|
+
this.logWrite('updateCliente', `PUT /pgrl/v2/clientes/${idReg}`, body);
|
|
1694
|
+
return this.put(`/pgrl/v2/clientes/${encodeURIComponent(idReg)}`, body);
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Detalle de un contacto de cliente por `idReg` opaco.
|
|
1698
|
+
*
|
|
1699
|
+
* Endpoint: GET /pgrl/v1/contactos-clientes/{idreg}
|
|
1700
|
+
* (v2 solo expone el listado; el singular es v1 pero devuelve las mismas
|
|
1701
|
+
* columnas CC_*). Se usa en el fetch+merge del update.
|
|
1702
|
+
*/
|
|
1703
|
+
async getContactoCliente(idReg) {
|
|
1704
|
+
return this.get(`/pgrl/v1/contactos-clientes/${encodeURIComponent(idReg)}`);
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Alta de contacto de cliente.
|
|
1708
|
+
*
|
|
1709
|
+
* Endpoint: POST /pgrl/v2/contactos-clientes — body VoContactosClientes.
|
|
1710
|
+
* Requeridos por el API: CC_GRUPO_CLI, CC_CLI.
|
|
1711
|
+
*/
|
|
1712
|
+
async createContactoCliente(body) {
|
|
1713
|
+
this.logWrite('createContactoCliente', 'POST /pgrl/v2/contactos-clientes', body);
|
|
1714
|
+
return this.post('/pgrl/v2/contactos-clientes', body);
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Actualización de contacto de cliente.
|
|
1718
|
+
*
|
|
1719
|
+
* Endpoint: PUT /pgrl/v2/contactos-clientes/{idReg}.
|
|
1720
|
+
*/
|
|
1721
|
+
async updateContactoCliente(idReg, body) {
|
|
1722
|
+
this.logWrite('updateContactoCliente', `PUT /pgrl/v2/contactos-clientes/${idReg}`, body);
|
|
1723
|
+
return this.put(`/pgrl/v2/contactos-clientes/${encodeURIComponent(idReg)}`, body);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Detalle de una localización de cliente por tipo e `idReg` opaco.
|
|
1727
|
+
*
|
|
1728
|
+
* Usa el GET singular v2 del tipo (v1 para factura, que no tiene v2; las
|
|
1729
|
+
* columnas son las mismas). Se emplea en el fetch+merge de los updates.
|
|
1730
|
+
*/
|
|
1731
|
+
async getLocalizacionCliente(tipo, idReg) {
|
|
1732
|
+
const base = LOC_FIELD_MAP[tipo].getPath;
|
|
1733
|
+
return this.get(`${base}/${encodeURIComponent(idReg)}`);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Lista paginada de localizaciones de envío de clientes.
|
|
1737
|
+
*
|
|
1738
|
+
* Endpoint: GET /pgrl/v2/localizaciones-envio-clientes
|
|
1739
|
+
*/
|
|
1740
|
+
async listLocalizacionesEnvioClientes(opts = {}) {
|
|
1741
|
+
return this.listResourceWithFiql('/pgrl/v2/localizaciones-envio-clientes', opts, {
|
|
1742
|
+
COD_CLI: opts.codCliente,
|
|
1743
|
+
COD_GRUPO_CLI: opts.grupoCliente,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Lista paginada de localizaciones de factura de clientes.
|
|
1748
|
+
*
|
|
1749
|
+
* Endpoint: GET /pgrl/v1/localizaciones-factura-clientes
|
|
1750
|
+
* (la lista solo existe en v1; el v2 de este recurso es solo POST/PUT).
|
|
1751
|
+
*/
|
|
1752
|
+
async listLocalizacionesFacturaClientes(opts = {}) {
|
|
1753
|
+
// v1, no v2: la LISTA solo existe en /pgrl/v1 (v2 responde 404 — solo
|
|
1754
|
+
// tiene POST/PUT). Verificado contra el API real y el Postman.
|
|
1755
|
+
return this.listResourceWithFiql('/pgrl/v1/localizaciones-factura-clientes', opts, {
|
|
1756
|
+
COD_CLI: opts.codCliente,
|
|
1757
|
+
COD_GRUPO_CLI: opts.grupoCliente,
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Alta de localización de cliente (cobro, envío, factura o servicio).
|
|
1762
|
+
*
|
|
1763
|
+
* Endpoint: POST /pgrl/v2/localizaciones-{tipo}-clientes — body Vo del tipo.
|
|
1764
|
+
*/
|
|
1765
|
+
async createLocalizacionCliente(tipo, body) {
|
|
1766
|
+
const path = LOC_FIELD_MAP[tipo].path;
|
|
1767
|
+
this.logWrite('createLocalizacionCliente', `POST ${path}`, body);
|
|
1768
|
+
return this.post(path, body);
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Actualización de localización de cliente (cobro, envío, factura o servicio).
|
|
1772
|
+
*
|
|
1773
|
+
* Endpoint: PUT /pgrl/v2/localizaciones-{tipo}-clientes/{idReg}.
|
|
1774
|
+
*/
|
|
1775
|
+
async updateLocalizacionCliente(tipo, idReg, body) {
|
|
1776
|
+
const path = `${LOC_FIELD_MAP[tipo].path}/${encodeURIComponent(idReg)}`;
|
|
1777
|
+
this.logWrite('updateLocalizacionCliente', `PUT ${path}`, body);
|
|
1778
|
+
return this.put(path, body);
|
|
1779
|
+
}
|
|
1780
|
+
// ---------------------------------------------------------------------------
|
|
1522
1781
|
// Internal helpers
|
|
1523
1782
|
// ---------------------------------------------------------------------------
|
|
1524
1783
|
async listResource(path, opts) {
|