alegra-api-client 0.1.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/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/client.d.ts +86 -0
- package/dist/client.js +195 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +33 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +38 -0
- package/dist/pagination.d.ts +37 -0
- package/dist/pagination.js +47 -0
- package/dist/resources/contacts.d.ts +22 -0
- package/dist/resources/contacts.js +38 -0
- package/dist/resources/invoices.d.ts +28 -0
- package/dist/resources/invoices.js +47 -0
- package/dist/resources/items.d.ts +21 -0
- package/dist/resources/items.js +40 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +8 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gary Dormoi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# alegra-api-client
|
|
2
|
+
|
|
3
|
+
[](https://github.com/garydormoi/alegra-api-client/actions/workflows/ci.yml) [](./LICENSE)
|
|
4
|
+
|
|
5
|
+
Cliente ligero y **tipado** (TypeScript) para la [API de Alegra](https://developer.alegra.com/).
|
|
6
|
+
Cubre lo esencial de la capa de integración: **autenticación**, **paginación** y
|
|
7
|
+
**manejo de rate limits** con reintentos, sobre `contactos`, `ítems` y `facturas`.
|
|
8
|
+
|
|
9
|
+
> Proyecto **no oficial** y sin afiliación con Alegra. Alegra® es marca de sus
|
|
10
|
+
> respectivos dueños. Documentación en español porque hay poca disponible.
|
|
11
|
+
|
|
12
|
+
## Características
|
|
13
|
+
|
|
14
|
+
- Autenticación HTTP Basic (`correo:token`), portable (Node, Deno, edge).
|
|
15
|
+
- Paginación por `start`/`limit` (máx. 30 por página) con generador asíncrono
|
|
16
|
+
para no cargar todo en memoria.
|
|
17
|
+
- Manejo de **rate limit (HTTP 429)** respetando `Retry-After`, con reintentos
|
|
18
|
+
y retroceso exponencial con jitter.
|
|
19
|
+
- Reintentos también ante errores de red y `5xx`; los `4xx` no se reintentan.
|
|
20
|
+
- Throttle preventivo opcional entre solicitudes.
|
|
21
|
+
- Timeout por solicitud con `AbortController`.
|
|
22
|
+
- Cero dependencias en tiempo de ejecución (usa `fetch` nativo de Node ≥ 18).
|
|
23
|
+
|
|
24
|
+
## Instalación
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install alegra-api-client
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requiere Node.js 18 o superior (por `fetch` nativo).
|
|
31
|
+
|
|
32
|
+
## Autenticación
|
|
33
|
+
|
|
34
|
+
La API de Alegra usa autenticación **HTTP Basic** con tu **correo** y un **token**
|
|
35
|
+
de API (no tu contraseña). El token se genera en Alegra, en la sección de
|
|
36
|
+
integraciones / API de tu cuenta.
|
|
37
|
+
|
|
38
|
+
Guarda las credenciales en variables de entorno, nunca en el código:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cp .env.example .env
|
|
42
|
+
# edita .env con tus valores reales
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Uso rápido
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { Alegra } from "alegra-api-client";
|
|
49
|
+
|
|
50
|
+
const alegra = new Alegra({
|
|
51
|
+
email: process.env.ALEGRA_EMAIL!,
|
|
52
|
+
token: process.env.ALEGRA_TOKEN!,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Una página (máx. 30)
|
|
56
|
+
const clientes = await alegra.contactos.listar({ type: "client", limit: 30 });
|
|
57
|
+
|
|
58
|
+
// Descargar todo (paginando internamente)
|
|
59
|
+
const todos = await alegra.contactos.listarTodos({ type: "client" });
|
|
60
|
+
|
|
61
|
+
// Un recurso por id
|
|
62
|
+
const factura = await alegra.facturas.obtener("123");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Paginación
|
|
66
|
+
|
|
67
|
+
La API devuelve como máximo 30 registros por página. Usa el generador para
|
|
68
|
+
procesar página por página sin agotar memoria:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
for await (const pagina of alegra.facturas.paginar({
|
|
72
|
+
date_afterOrNow: "2026-01-01",
|
|
73
|
+
date_beforeOrNow: "2026-01-31",
|
|
74
|
+
})) {
|
|
75
|
+
for (const factura of pagina) {
|
|
76
|
+
// procesar
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
O junta todo de una vez con `listarTodas` / `listarTodos` / `collectAll`.
|
|
82
|
+
|
|
83
|
+
## Manejo de rate limits y reintentos
|
|
84
|
+
|
|
85
|
+
La API de Alegra permite **150 solicitudes por minuto por usuario** (≈ 2.5/seg).
|
|
86
|
+
Al excederlo responde **HTTP 429** e informa el estado en cabeceras
|
|
87
|
+
`X-Rate-Limit-Limit`, `X-Rate-Limit-Remaining` y `X-Rate-Limit-Reset` (segundos
|
|
88
|
+
que faltan para reiniciar la ventana). Alegra **no** envía `Retry-After`.
|
|
89
|
+
|
|
90
|
+
El cliente reintenta automáticamente ante `429` y `5xx`. Ante `429` espera lo que
|
|
91
|
+
indique `X-Rate-Limit-Reset` (o `Retry-After` si estuviera presente) y, si no hay
|
|
92
|
+
ninguno, aplica retroceso exponencial con jitter.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const alegra = new Alegra({
|
|
96
|
+
email,
|
|
97
|
+
token,
|
|
98
|
+
minRequestIntervalMs: 400, // ≈150/min: espaciado preventivo para no llegar al límite
|
|
99
|
+
maxRetries: 3, // reintentos ante 429 / 5xx
|
|
100
|
+
retryBaseMs: 500, // backoff base
|
|
101
|
+
timeoutMs: 30000, // timeout por solicitud
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Si se agotan los reintentos ante `429`, se lanza `AlegraRateLimitError` (con
|
|
106
|
+
`retryAfterSeconds` cuando la respuesta lo permite calcular).
|
|
107
|
+
|
|
108
|
+
## Manejo de errores
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { AlegraApiError, AlegraRateLimitError } from "alegra-api-client";
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await alegra.facturas.obtener("no-existe");
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err instanceof AlegraRateLimitError) {
|
|
117
|
+
// esperar y reintentar más tarde
|
|
118
|
+
} else if (err instanceof AlegraApiError) {
|
|
119
|
+
console.error(err.status, err.endpoint, err.body);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Recursos disponibles
|
|
125
|
+
|
|
126
|
+
| Recurso | Métodos |
|
|
127
|
+
| ----------- | --------------------------------------------------- |
|
|
128
|
+
| `contactos` | `listar`, `obtener`, `paginar`, `listarTodos` |
|
|
129
|
+
| `items` | `listar`, `obtener`, `paginar`, `listarTodos` |
|
|
130
|
+
| `facturas` | `listar`, `obtener`, `paginar`, `listarTodas` |
|
|
131
|
+
|
|
132
|
+
Para endpoints no cubiertos por los recursos, usa el cliente directo:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const data = await alegra.client.request<MiTipo>("otro-endpoint", {
|
|
136
|
+
query: { limit: 30 },
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Referencia rápida de la API de Alegra
|
|
141
|
+
|
|
142
|
+
Resumen de los hechos de la API en que se basa este cliente (fuente:
|
|
143
|
+
documentación oficial de Alegra para desarrolladores).
|
|
144
|
+
|
|
145
|
+
| Tema | Detalle |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| URL base | `https://api.alegra.com/api/v1` |
|
|
148
|
+
| Autenticación | HTTP **Basic**: `Authorization: Basic base64(correo:token)` |
|
|
149
|
+
| Token | Alegra → Configuración → *API - Integraciones con otros sistemas* |
|
|
150
|
+
| Error de autenticación | HTTP **401** |
|
|
151
|
+
| Límite de uso | **150 solicitudes/minuto por usuario** |
|
|
152
|
+
| Límite excedido | HTTP **429** + cabeceras `X-Rate-Limit-Limit` / `X-Rate-Limit-Remaining` / `X-Rate-Limit-Reset` |
|
|
153
|
+
| Paginación | Parámetros `start` (offset) y `limit`; **máximo 30** por página |
|
|
154
|
+
|
|
155
|
+
Referencias:
|
|
156
|
+
[Autenticación](https://developer.alegra.com/reference/autenticaci%C3%B3n) ·
|
|
157
|
+
[Límite de request](https://developer.alegra.com/reference/l%C3%ADmite-de-request) ·
|
|
158
|
+
[Documentación general](https://developer.alegra.com/docs)
|
|
159
|
+
|
|
160
|
+
> Este resumen existe porque la documentación en español de la API de Alegra es
|
|
161
|
+
> escasa. Si algo cambia en la API oficial, esta tabla debe actualizarse.
|
|
162
|
+
|
|
163
|
+
## Desarrollo
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm install
|
|
167
|
+
npm run build # compila a dist/
|
|
168
|
+
npm test # pruebas con vitest (fetch simulado, datos ficticios)
|
|
169
|
+
npm run lint:types # verificación de tipos sin emitir
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Los ejemplos en `examples/` usan datos **ficticios** y credenciales por variables
|
|
173
|
+
de entorno.
|
|
174
|
+
|
|
175
|
+
## Contribuir
|
|
176
|
+
|
|
177
|
+
Los issues y PRs son bienvenidos. Al reportar un problema, incluye el endpoint,
|
|
178
|
+
los parámetros usados (sin credenciales) y el comportamiento esperado.
|
|
179
|
+
|
|
180
|
+
## Licencia
|
|
181
|
+
|
|
182
|
+
[MIT](./LICENSE)
|
|
183
|
+
|
|
184
|
+
## Changelog
|
|
185
|
+
- 0.1.0 - primera version publica
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Opciones de construcción del cliente. */
|
|
2
|
+
export interface AlegraClientOptions {
|
|
3
|
+
/** Correo de la cuenta de Alegra (usuario en la autenticación Basic). */
|
|
4
|
+
email: string;
|
|
5
|
+
/** Token de la API de Alegra. */
|
|
6
|
+
token: string;
|
|
7
|
+
/** URL base de la API. Por defecto: https://api.alegra.com/api/v1 */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Milisegundos mínimos entre solicitudes (throttle preventivo del lado cliente).
|
|
11
|
+
* Útil para no acercarse al límite de la API. Por defecto: 0 (sin throttle).
|
|
12
|
+
*/
|
|
13
|
+
minRequestIntervalMs?: number;
|
|
14
|
+
/** Número máximo de reintentos ante 429 y errores 5xx. Por defecto: 3. */
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
/** Backoff base (ms) para el retroceso exponencial. Por defecto: 500. */
|
|
17
|
+
retryBaseMs?: number;
|
|
18
|
+
/** Tiempo máximo por solicitud (ms) antes de abortar. Por defecto: 30000. */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
/** Implementación de fetch a usar (inyectable para pruebas). Por defecto: globalThis.fetch. */
|
|
21
|
+
fetchImpl?: typeof fetch;
|
|
22
|
+
/** Cadena User-Agent enviada en cada solicitud. */
|
|
23
|
+
userAgent?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Opciones por solicitud individual. */
|
|
26
|
+
export interface RequestOptions {
|
|
27
|
+
method?: string;
|
|
28
|
+
/** Parámetros de query. Los `undefined` se omiten. */
|
|
29
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
30
|
+
/** Cuerpo a serializar como JSON (para POST/PUT). */
|
|
31
|
+
body?: unknown;
|
|
32
|
+
/** Señal externa de cancelación. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Cliente HTTP para la API de Alegra.
|
|
37
|
+
*
|
|
38
|
+
* Responsabilidades:
|
|
39
|
+
* - Autenticación HTTP Basic (`email:token`).
|
|
40
|
+
* - Serialización de query y cuerpo JSON.
|
|
41
|
+
* - Manejo de rate limit (HTTP 429) respetando `Retry-After`.
|
|
42
|
+
* - Reintentos con retroceso exponencial ante 429 y 5xx.
|
|
43
|
+
* - Throttle preventivo opcional entre solicitudes.
|
|
44
|
+
* - Timeout por solicitud.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const client = new AlegraClient({ email: "demo@ejemplo.com", token: "TU_TOKEN" });
|
|
48
|
+
* const contactos = await client.request<Contacto[]>("contacts", { query: { limit: 30 } });
|
|
49
|
+
*/
|
|
50
|
+
export declare class AlegraClient {
|
|
51
|
+
private readonly baseUrl;
|
|
52
|
+
private readonly authHeader;
|
|
53
|
+
private readonly minRequestIntervalMs;
|
|
54
|
+
private readonly maxRetries;
|
|
55
|
+
private readonly retryBaseMs;
|
|
56
|
+
private readonly timeoutMs;
|
|
57
|
+
private readonly fetchImpl;
|
|
58
|
+
private readonly userAgent;
|
|
59
|
+
private lastRequestAt;
|
|
60
|
+
constructor(options: AlegraClientOptions);
|
|
61
|
+
/** Construye la URL completa a partir del endpoint relativo y la query. */
|
|
62
|
+
private buildUrl;
|
|
63
|
+
/** Lee `Retry-After` (segundos o fecha HTTP) y lo convierte a milisegundos. */
|
|
64
|
+
private parseRetryAfter;
|
|
65
|
+
/**
|
|
66
|
+
* Lee `X-Rate-Limit-Reset` (segundos que faltan para reiniciar la ventana) y lo
|
|
67
|
+
* convierte a milisegundos. Alegra devuelve este header en cada respuesta y NO
|
|
68
|
+
* envía `Retry-After`, por lo que es la señal de espera fiable ante un 429.
|
|
69
|
+
* Ref.: https://developer.alegra.com/reference/límite-de-request
|
|
70
|
+
*/
|
|
71
|
+
private parseResetSeconds;
|
|
72
|
+
/**
|
|
73
|
+
* Realiza una solicitud a la API y devuelve el JSON tipado.
|
|
74
|
+
*
|
|
75
|
+
* @typeParam T - Forma esperada de la respuesta.
|
|
76
|
+
* @throws {AlegraRateLimitError} si se agotan los reintentos ante 429.
|
|
77
|
+
* @throws {AlegraApiError} ante cualquier otra respuesta no 2xx.
|
|
78
|
+
*/
|
|
79
|
+
request<T>(endpoint: string, options?: RequestOptions): Promise<T>;
|
|
80
|
+
/** Espera lo necesario para respetar `minRequestIntervalMs`. */
|
|
81
|
+
private throttle;
|
|
82
|
+
/** Backoff exponencial con jitter. */
|
|
83
|
+
private backoffMs;
|
|
84
|
+
/** Intenta parsear JSON; si no es JSON, devuelve el texto. */
|
|
85
|
+
private parseBody;
|
|
86
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { AlegraApiError, AlegraRateLimitError } from "./errors.js";
|
|
2
|
+
const DEFAULT_BASE_URL = "https://api.alegra.com/api/v1";
|
|
3
|
+
/** Codifica `usuario:token` en base64, de forma portable (Node, Deno, edge). */
|
|
4
|
+
function toBase64(input) {
|
|
5
|
+
if (typeof btoa === "function")
|
|
6
|
+
return btoa(input);
|
|
7
|
+
// eslint-disable-next-line no-undef
|
|
8
|
+
return Buffer.from(input, "utf-8").toString("base64");
|
|
9
|
+
}
|
|
10
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
/**
|
|
12
|
+
* Cliente HTTP para la API de Alegra.
|
|
13
|
+
*
|
|
14
|
+
* Responsabilidades:
|
|
15
|
+
* - Autenticación HTTP Basic (`email:token`).
|
|
16
|
+
* - Serialización de query y cuerpo JSON.
|
|
17
|
+
* - Manejo de rate limit (HTTP 429) respetando `Retry-After`.
|
|
18
|
+
* - Reintentos con retroceso exponencial ante 429 y 5xx.
|
|
19
|
+
* - Throttle preventivo opcional entre solicitudes.
|
|
20
|
+
* - Timeout por solicitud.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const client = new AlegraClient({ email: "demo@ejemplo.com", token: "TU_TOKEN" });
|
|
24
|
+
* const contactos = await client.request<Contacto[]>("contacts", { query: { limit: 30 } });
|
|
25
|
+
*/
|
|
26
|
+
export class AlegraClient {
|
|
27
|
+
baseUrl;
|
|
28
|
+
authHeader;
|
|
29
|
+
minRequestIntervalMs;
|
|
30
|
+
maxRetries;
|
|
31
|
+
retryBaseMs;
|
|
32
|
+
timeoutMs;
|
|
33
|
+
fetchImpl;
|
|
34
|
+
userAgent;
|
|
35
|
+
lastRequestAt = 0;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
if (!options.email || !options.token) {
|
|
38
|
+
throw new Error("AlegraClient requiere `email` y `token`.");
|
|
39
|
+
}
|
|
40
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
41
|
+
this.authHeader = `Basic ${toBase64(`${options.email}:${options.token}`)}`;
|
|
42
|
+
this.minRequestIntervalMs = options.minRequestIntervalMs ?? 0;
|
|
43
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
44
|
+
this.retryBaseMs = options.retryBaseMs ?? 500;
|
|
45
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
46
|
+
const injected = options.fetchImpl ?? globalThis.fetch;
|
|
47
|
+
if (typeof injected !== "function") {
|
|
48
|
+
throw new Error("No se encontró `fetch`. Usa Node >=18, o pasa `fetchImpl` en las opciones.");
|
|
49
|
+
}
|
|
50
|
+
this.fetchImpl = injected;
|
|
51
|
+
this.userAgent = options.userAgent ?? "alegra-api-client";
|
|
52
|
+
}
|
|
53
|
+
/** Construye la URL completa a partir del endpoint relativo y la query. */
|
|
54
|
+
buildUrl(endpoint, query) {
|
|
55
|
+
const path = endpoint.replace(/^\/+/, "");
|
|
56
|
+
const url = new URL(`${this.baseUrl}/${path}`);
|
|
57
|
+
if (query) {
|
|
58
|
+
for (const [key, value] of Object.entries(query)) {
|
|
59
|
+
if (value !== undefined)
|
|
60
|
+
url.searchParams.set(key, String(value));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return url.toString();
|
|
64
|
+
}
|
|
65
|
+
/** Lee `Retry-After` (segundos o fecha HTTP) y lo convierte a milisegundos. */
|
|
66
|
+
parseRetryAfter(header) {
|
|
67
|
+
if (!header)
|
|
68
|
+
return undefined;
|
|
69
|
+
const asSeconds = Number(header);
|
|
70
|
+
if (!Number.isNaN(asSeconds))
|
|
71
|
+
return Math.max(0, asSeconds * 1000);
|
|
72
|
+
const asDate = Date.parse(header);
|
|
73
|
+
if (!Number.isNaN(asDate))
|
|
74
|
+
return Math.max(0, asDate - Date.now());
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Lee `X-Rate-Limit-Reset` (segundos que faltan para reiniciar la ventana) y lo
|
|
79
|
+
* convierte a milisegundos. Alegra devuelve este header en cada respuesta y NO
|
|
80
|
+
* envía `Retry-After`, por lo que es la señal de espera fiable ante un 429.
|
|
81
|
+
* Ref.: https://developer.alegra.com/reference/límite-de-request
|
|
82
|
+
*/
|
|
83
|
+
parseResetSeconds(header) {
|
|
84
|
+
if (!header)
|
|
85
|
+
return undefined;
|
|
86
|
+
const asSeconds = Number(header);
|
|
87
|
+
if (Number.isNaN(asSeconds))
|
|
88
|
+
return undefined;
|
|
89
|
+
return Math.max(0, asSeconds * 1000);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Realiza una solicitud a la API y devuelve el JSON tipado.
|
|
93
|
+
*
|
|
94
|
+
* @typeParam T - Forma esperada de la respuesta.
|
|
95
|
+
* @throws {AlegraRateLimitError} si se agotan los reintentos ante 429.
|
|
96
|
+
* @throws {AlegraApiError} ante cualquier otra respuesta no 2xx.
|
|
97
|
+
*/
|
|
98
|
+
async request(endpoint, options = {}) {
|
|
99
|
+
const url = this.buildUrl(endpoint, options.query);
|
|
100
|
+
const method = options.method ?? "GET";
|
|
101
|
+
let attempt = 0;
|
|
102
|
+
// Reintentos: 429 y 5xx. 4xx (salvo 429) no se reintentan.
|
|
103
|
+
for (;;) {
|
|
104
|
+
await this.throttle();
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
107
|
+
if (options.signal) {
|
|
108
|
+
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
109
|
+
}
|
|
110
|
+
let response;
|
|
111
|
+
try {
|
|
112
|
+
response = await this.fetchImpl(url, {
|
|
113
|
+
method,
|
|
114
|
+
headers: {
|
|
115
|
+
Accept: "application/json",
|
|
116
|
+
Authorization: this.authHeader,
|
|
117
|
+
"User-Agent": this.userAgent,
|
|
118
|
+
...(options.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
119
|
+
},
|
|
120
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
// Errores de red / abort: reintentar con backoff si quedan intentos.
|
|
127
|
+
if (attempt < this.maxRetries) {
|
|
128
|
+
await sleep(this.backoffMs(attempt));
|
|
129
|
+
attempt += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
throw new AlegraApiError(`Fallo de red al llamar a Alegra (${endpoint}): ${err.message}`, { status: 0, body: null, endpoint });
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
}
|
|
137
|
+
if (response.ok) {
|
|
138
|
+
return (await this.parseBody(response));
|
|
139
|
+
}
|
|
140
|
+
const body = await this.parseBody(response);
|
|
141
|
+
if (response.status === 429) {
|
|
142
|
+
// Alegra: 150 req/min por usuario. No envía Retry-After; usa X-Rate-Limit-Reset
|
|
143
|
+
// (segundos que faltan para reiniciar la ventana de 1 minuto).
|
|
144
|
+
const retryAfterMs = this.parseRetryAfter(response.headers.get("Retry-After"));
|
|
145
|
+
const resetMs = this.parseResetSeconds(response.headers.get("X-Rate-Limit-Reset"));
|
|
146
|
+
const waitMs = retryAfterMs ?? resetMs;
|
|
147
|
+
if (attempt < this.maxRetries) {
|
|
148
|
+
await sleep(waitMs ?? this.backoffMs(attempt));
|
|
149
|
+
attempt += 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
throw new AlegraRateLimitError(`Rate limit de Alegra (429) tras ${this.maxRetries} reintentos en ${endpoint}.`, {
|
|
153
|
+
status: 429,
|
|
154
|
+
body,
|
|
155
|
+
endpoint,
|
|
156
|
+
retryAfterSeconds: waitMs !== undefined ? waitMs / 1000 : undefined,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
160
|
+
await sleep(this.backoffMs(attempt));
|
|
161
|
+
attempt += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
throw new AlegraApiError(`Alegra respondió ${response.status} en ${endpoint}.`, { status: response.status, body, endpoint });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Espera lo necesario para respetar `minRequestIntervalMs`. */
|
|
168
|
+
async throttle() {
|
|
169
|
+
if (this.minRequestIntervalMs <= 0)
|
|
170
|
+
return;
|
|
171
|
+
const elapsed = Date.now() - this.lastRequestAt;
|
|
172
|
+
const wait = this.minRequestIntervalMs - elapsed;
|
|
173
|
+
if (wait > 0)
|
|
174
|
+
await sleep(wait);
|
|
175
|
+
this.lastRequestAt = Date.now();
|
|
176
|
+
}
|
|
177
|
+
/** Backoff exponencial con jitter. */
|
|
178
|
+
backoffMs(attempt) {
|
|
179
|
+
const base = this.retryBaseMs * 2 ** attempt;
|
|
180
|
+
const jitter = Math.random() * this.retryBaseMs;
|
|
181
|
+
return base + jitter;
|
|
182
|
+
}
|
|
183
|
+
/** Intenta parsear JSON; si no es JSON, devuelve el texto. */
|
|
184
|
+
async parseBody(response) {
|
|
185
|
+
const text = await response.text();
|
|
186
|
+
if (!text)
|
|
187
|
+
return null;
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(text);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errores del cliente de Alegra.
|
|
3
|
+
*
|
|
4
|
+
* Todas las respuestas no exitosas de la API se envuelven en {@link AlegraApiError}.
|
|
5
|
+
* Cuando el servidor responde 429 (demasiadas solicitudes) se usa la subclase
|
|
6
|
+
* {@link AlegraRateLimitError}, que expone el tiempo de espera sugerido.
|
|
7
|
+
*/
|
|
8
|
+
/** Error genérico de la API de Alegra (respuesta HTTP no 2xx). */
|
|
9
|
+
export declare class AlegraApiError extends Error {
|
|
10
|
+
/** Código de estado HTTP devuelto por la API. */
|
|
11
|
+
readonly status: number;
|
|
12
|
+
/** Cuerpo de la respuesta, ya parseado si era JSON; si no, el texto crudo. */
|
|
13
|
+
readonly body: unknown;
|
|
14
|
+
/** Endpoint (ruta relativa) que originó el error. */
|
|
15
|
+
readonly endpoint: string;
|
|
16
|
+
constructor(message: string, params: {
|
|
17
|
+
status: number;
|
|
18
|
+
body: unknown;
|
|
19
|
+
endpoint: string;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Error específico de rate limit (HTTP 429). */
|
|
23
|
+
export declare class AlegraRateLimitError extends AlegraApiError {
|
|
24
|
+
/** Segundos sugeridos de espera antes de reintentar (cabecera Retry-After), si vino. */
|
|
25
|
+
readonly retryAfterSeconds?: number;
|
|
26
|
+
constructor(message: string, params: {
|
|
27
|
+
status: number;
|
|
28
|
+
body: unknown;
|
|
29
|
+
endpoint: string;
|
|
30
|
+
retryAfterSeconds?: number;
|
|
31
|
+
});
|
|
32
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errores del cliente de Alegra.
|
|
3
|
+
*
|
|
4
|
+
* Todas las respuestas no exitosas de la API se envuelven en {@link AlegraApiError}.
|
|
5
|
+
* Cuando el servidor responde 429 (demasiadas solicitudes) se usa la subclase
|
|
6
|
+
* {@link AlegraRateLimitError}, que expone el tiempo de espera sugerido.
|
|
7
|
+
*/
|
|
8
|
+
/** Error genérico de la API de Alegra (respuesta HTTP no 2xx). */
|
|
9
|
+
export class AlegraApiError extends Error {
|
|
10
|
+
/** Código de estado HTTP devuelto por la API. */
|
|
11
|
+
status;
|
|
12
|
+
/** Cuerpo de la respuesta, ya parseado si era JSON; si no, el texto crudo. */
|
|
13
|
+
body;
|
|
14
|
+
/** Endpoint (ruta relativa) que originó el error. */
|
|
15
|
+
endpoint;
|
|
16
|
+
constructor(message, params) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "AlegraApiError";
|
|
19
|
+
this.status = params.status;
|
|
20
|
+
this.body = params.body;
|
|
21
|
+
this.endpoint = params.endpoint;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Error específico de rate limit (HTTP 429). */
|
|
25
|
+
export class AlegraRateLimitError extends AlegraApiError {
|
|
26
|
+
/** Segundos sugeridos de espera antes de reintentar (cabecera Retry-After), si vino. */
|
|
27
|
+
retryAfterSeconds;
|
|
28
|
+
constructor(message, params) {
|
|
29
|
+
super(message, params);
|
|
30
|
+
this.name = "AlegraRateLimitError";
|
|
31
|
+
this.retryAfterSeconds = params.retryAfterSeconds;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alegra-api-client
|
|
3
|
+
*
|
|
4
|
+
* Cliente ligero y tipado para la API de Alegra.
|
|
5
|
+
* Punto de entrada: exporta el cliente HTTP, los recursos, los helpers de
|
|
6
|
+
* paginación, los tipos y los errores.
|
|
7
|
+
*/
|
|
8
|
+
export { AlegraClient } from "./client.js";
|
|
9
|
+
export type { AlegraClientOptions, RequestOptions } from "./client.js";
|
|
10
|
+
export { paginate, collectAll, MAX_LIMIT } from "./pagination.js";
|
|
11
|
+
export type { PaginateOptions } from "./pagination.js";
|
|
12
|
+
export { Contactos } from "./resources/contacts.js";
|
|
13
|
+
export { Items } from "./resources/items.js";
|
|
14
|
+
export { Facturas } from "./resources/invoices.js";
|
|
15
|
+
export { AlegraApiError, AlegraRateLimitError } from "./errors.js";
|
|
16
|
+
export type * from "./types.js";
|
|
17
|
+
import { AlegraClient, type AlegraClientOptions } from "./client.js";
|
|
18
|
+
import { Contactos } from "./resources/contacts.js";
|
|
19
|
+
import { Items } from "./resources/items.js";
|
|
20
|
+
import { Facturas } from "./resources/invoices.js";
|
|
21
|
+
/**
|
|
22
|
+
* Fachada de conveniencia: crea el cliente y expone los recursos ya cableados.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const alegra = new Alegra({ email: "demo@ejemplo.com", token: "TU_TOKEN" });
|
|
26
|
+
* const clientes = await alegra.contactos.listarTodos({ type: "client" });
|
|
27
|
+
* const facturas = await alegra.facturas.listar({ limit: 30 });
|
|
28
|
+
*/
|
|
29
|
+
export declare class Alegra {
|
|
30
|
+
/** Cliente HTTP subyacente (por si necesitas endpoints no cubiertos por los recursos). */
|
|
31
|
+
readonly client: AlegraClient;
|
|
32
|
+
readonly contactos: Contactos;
|
|
33
|
+
readonly items: Items;
|
|
34
|
+
readonly facturas: Facturas;
|
|
35
|
+
constructor(options: AlegraClientOptions);
|
|
36
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alegra-api-client
|
|
3
|
+
*
|
|
4
|
+
* Cliente ligero y tipado para la API de Alegra.
|
|
5
|
+
* Punto de entrada: exporta el cliente HTTP, los recursos, los helpers de
|
|
6
|
+
* paginación, los tipos y los errores.
|
|
7
|
+
*/
|
|
8
|
+
export { AlegraClient } from "./client.js";
|
|
9
|
+
export { paginate, collectAll, MAX_LIMIT } from "./pagination.js";
|
|
10
|
+
export { Contactos } from "./resources/contacts.js";
|
|
11
|
+
export { Items } from "./resources/items.js";
|
|
12
|
+
export { Facturas } from "./resources/invoices.js";
|
|
13
|
+
export { AlegraApiError, AlegraRateLimitError } from "./errors.js";
|
|
14
|
+
import { AlegraClient } from "./client.js";
|
|
15
|
+
import { Contactos } from "./resources/contacts.js";
|
|
16
|
+
import { Items } from "./resources/items.js";
|
|
17
|
+
import { Facturas } from "./resources/invoices.js";
|
|
18
|
+
/**
|
|
19
|
+
* Fachada de conveniencia: crea el cliente y expone los recursos ya cableados.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const alegra = new Alegra({ email: "demo@ejemplo.com", token: "TU_TOKEN" });
|
|
23
|
+
* const clientes = await alegra.contactos.listarTodos({ type: "client" });
|
|
24
|
+
* const facturas = await alegra.facturas.listar({ limit: 30 });
|
|
25
|
+
*/
|
|
26
|
+
export class Alegra {
|
|
27
|
+
/** Cliente HTTP subyacente (por si necesitas endpoints no cubiertos por los recursos). */
|
|
28
|
+
client;
|
|
29
|
+
contactos;
|
|
30
|
+
items;
|
|
31
|
+
facturas;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.client = new AlegraClient(options);
|
|
34
|
+
this.contactos = new Contactos(this.client);
|
|
35
|
+
this.items = new Items(this.client);
|
|
36
|
+
this.facturas = new Facturas(this.client);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AlegraClient, RequestOptions } from "./client.js";
|
|
2
|
+
/** Tamaño de página máximo admitido por la API de Alegra. */
|
|
3
|
+
export declare const MAX_LIMIT = 30;
|
|
4
|
+
/** Opciones para recorrer páginas de un recurso listable. */
|
|
5
|
+
export interface PaginateOptions {
|
|
6
|
+
/** Tamaño de página (1..30). Por defecto 30. */
|
|
7
|
+
limit?: number;
|
|
8
|
+
/** Offset inicial. Por defecto 0. */
|
|
9
|
+
start?: number;
|
|
10
|
+
/** Query adicional a repetir en cada página (filtros, orden, etc.). */
|
|
11
|
+
query?: RequestOptions["query"];
|
|
12
|
+
/**
|
|
13
|
+
* Tope de páginas a recorrer (salvaguarda ante bucles). Por defecto sin tope.
|
|
14
|
+
* Útil para no agotar cuota si un filtro trae demasiados resultados.
|
|
15
|
+
*/
|
|
16
|
+
maxPages?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Itera página por página un endpoint de lista de Alegra (paginación por `start`/`limit`).
|
|
20
|
+
*
|
|
21
|
+
* La API devuelve arreglos; la paginación termina cuando una página trae menos
|
|
22
|
+
* elementos que `limit`. Se usa un generador asíncrono para no cargar todo en memoria.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam T - Tipo de cada elemento.
|
|
25
|
+
* @example
|
|
26
|
+
* for await (const pagina of paginate<Factura>(client, "invoices", { limit: 30 })) {
|
|
27
|
+
* procesar(pagina);
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
export declare function paginate<T>(client: AlegraClient, endpoint: string, options?: PaginateOptions): AsyncGenerator<T[], void, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Recorre todas las páginas y devuelve el arreglo completo.
|
|
33
|
+
*
|
|
34
|
+
* Cómodo para conjuntos moderados. Para volúmenes grandes prefiere {@link paginate}
|
|
35
|
+
* y procesa página por página.
|
|
36
|
+
*/
|
|
37
|
+
export declare function collectAll<T>(client: AlegraClient, endpoint: string, options?: PaginateOptions): Promise<T[]>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Tamaño de página máximo admitido por la API de Alegra. */
|
|
2
|
+
export const MAX_LIMIT = 30;
|
|
3
|
+
/**
|
|
4
|
+
* Itera página por página un endpoint de lista de Alegra (paginación por `start`/`limit`).
|
|
5
|
+
*
|
|
6
|
+
* La API devuelve arreglos; la paginación termina cuando una página trae menos
|
|
7
|
+
* elementos que `limit`. Se usa un generador asíncrono para no cargar todo en memoria.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam T - Tipo de cada elemento.
|
|
10
|
+
* @example
|
|
11
|
+
* for await (const pagina of paginate<Factura>(client, "invoices", { limit: 30 })) {
|
|
12
|
+
* procesar(pagina);
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export async function* paginate(client, endpoint, options = {}) {
|
|
16
|
+
const limit = Math.min(Math.max(options.limit ?? MAX_LIMIT, 1), MAX_LIMIT);
|
|
17
|
+
let start = options.start ?? 0;
|
|
18
|
+
let pages = 0;
|
|
19
|
+
for (;;) {
|
|
20
|
+
if (options.maxPages !== undefined && pages >= options.maxPages)
|
|
21
|
+
return;
|
|
22
|
+
const page = await client.request(endpoint, {
|
|
23
|
+
query: { ...options.query, start, limit },
|
|
24
|
+
});
|
|
25
|
+
const items = Array.isArray(page) ? page : [];
|
|
26
|
+
if (items.length === 0)
|
|
27
|
+
return;
|
|
28
|
+
yield items;
|
|
29
|
+
pages += 1;
|
|
30
|
+
if (items.length < limit)
|
|
31
|
+
return;
|
|
32
|
+
start += limit;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recorre todas las páginas y devuelve el arreglo completo.
|
|
37
|
+
*
|
|
38
|
+
* Cómodo para conjuntos moderados. Para volúmenes grandes prefiere {@link paginate}
|
|
39
|
+
* y procesa página por página.
|
|
40
|
+
*/
|
|
41
|
+
export async function collectAll(client, endpoint, options = {}) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for await (const page of paginate(client, endpoint, options)) {
|
|
44
|
+
out.push(...page);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AlegraClient } from "../client.js";
|
|
2
|
+
import type { Contacto, ParametrosContactos } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Recurso de contactos (clientes y proveedores).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const alegra = new AlegraClient({ email, token });
|
|
8
|
+
* const contactos = new Contactos(alegra);
|
|
9
|
+
* const primeros = await contactos.listar({ type: "client", limit: 30 });
|
|
10
|
+
*/
|
|
11
|
+
export declare class Contactos {
|
|
12
|
+
private readonly client;
|
|
13
|
+
constructor(client: AlegraClient);
|
|
14
|
+
/** Lista una sola página de contactos según los parámetros dados. */
|
|
15
|
+
listar(params?: ParametrosContactos): Promise<Contacto[]>;
|
|
16
|
+
/** Obtiene un contacto por su id. */
|
|
17
|
+
obtener(id: string): Promise<Contacto>;
|
|
18
|
+
/** Recorre todos los contactos página por página (generador asíncrono). */
|
|
19
|
+
paginar(params?: ParametrosContactos): AsyncGenerator<Contacto[], void, unknown>;
|
|
20
|
+
/** Descarga todos los contactos que cumplan el filtro. */
|
|
21
|
+
listarTodos(params?: ParametrosContactos): Promise<Contacto[]>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { collectAll, paginate } from "../pagination.js";
|
|
2
|
+
/**
|
|
3
|
+
* Recurso de contactos (clientes y proveedores).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const alegra = new AlegraClient({ email, token });
|
|
7
|
+
* const contactos = new Contactos(alegra);
|
|
8
|
+
* const primeros = await contactos.listar({ type: "client", limit: 30 });
|
|
9
|
+
*/
|
|
10
|
+
export class Contactos {
|
|
11
|
+
client;
|
|
12
|
+
constructor(client) {
|
|
13
|
+
this.client = client;
|
|
14
|
+
}
|
|
15
|
+
/** Lista una sola página de contactos según los parámetros dados. */
|
|
16
|
+
listar(params = {}) {
|
|
17
|
+
return this.client.request("contacts", { query: { ...params } });
|
|
18
|
+
}
|
|
19
|
+
/** Obtiene un contacto por su id. */
|
|
20
|
+
obtener(id) {
|
|
21
|
+
return this.client.request(`contacts/${encodeURIComponent(id)}`);
|
|
22
|
+
}
|
|
23
|
+
/** Recorre todos los contactos página por página (generador asíncrono). */
|
|
24
|
+
paginar(params = {}) {
|
|
25
|
+
const { start, limit, ...query } = params;
|
|
26
|
+
const opts = { start, limit, query: query };
|
|
27
|
+
return paginate(this.client, "contacts", opts);
|
|
28
|
+
}
|
|
29
|
+
/** Descarga todos los contactos que cumplan el filtro. */
|
|
30
|
+
listarTodos(params = {}) {
|
|
31
|
+
const { start, limit, ...query } = params;
|
|
32
|
+
return collectAll(this.client, "contacts", {
|
|
33
|
+
start,
|
|
34
|
+
limit,
|
|
35
|
+
query: query,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AlegraClient } from "../client.js";
|
|
2
|
+
import type { Factura, ParametrosFacturas } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Recurso de facturas de venta.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const facturas = new Facturas(alegra);
|
|
8
|
+
* for await (const pagina of facturas.paginar({ date_afterOrNow: "2026-01-01" })) {
|
|
9
|
+
* // procesar cada página sin cargar todo en memoria
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export declare class Facturas {
|
|
13
|
+
private readonly client;
|
|
14
|
+
constructor(client: AlegraClient);
|
|
15
|
+
/** Lista una sola página de facturas. */
|
|
16
|
+
listar(params?: ParametrosFacturas): Promise<Factura[]>;
|
|
17
|
+
/** Obtiene una factura por su id. */
|
|
18
|
+
obtener(id: string): Promise<Factura>;
|
|
19
|
+
/** Recorre todas las facturas página por página. */
|
|
20
|
+
paginar(params?: ParametrosFacturas): AsyncGenerator<Factura[], void, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Descarga todas las facturas que cumplan el filtro.
|
|
23
|
+
*
|
|
24
|
+
* Nota: para rangos de fechas muy amplios conviene usar {@link paginar} y procesar
|
|
25
|
+
* por página, o acotar con `date_afterOrNow` / `date_beforeOrNow`.
|
|
26
|
+
*/
|
|
27
|
+
listarTodas(params?: ParametrosFacturas): Promise<Factura[]>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { collectAll, paginate } from "../pagination.js";
|
|
2
|
+
/**
|
|
3
|
+
* Recurso de facturas de venta.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const facturas = new Facturas(alegra);
|
|
7
|
+
* for await (const pagina of facturas.paginar({ date_afterOrNow: "2026-01-01" })) {
|
|
8
|
+
* // procesar cada página sin cargar todo en memoria
|
|
9
|
+
* }
|
|
10
|
+
*/
|
|
11
|
+
export class Facturas {
|
|
12
|
+
client;
|
|
13
|
+
constructor(client) {
|
|
14
|
+
this.client = client;
|
|
15
|
+
}
|
|
16
|
+
/** Lista una sola página de facturas. */
|
|
17
|
+
listar(params = {}) {
|
|
18
|
+
return this.client.request("invoices", { query: { ...params } });
|
|
19
|
+
}
|
|
20
|
+
/** Obtiene una factura por su id. */
|
|
21
|
+
obtener(id) {
|
|
22
|
+
return this.client.request(`invoices/${encodeURIComponent(id)}`);
|
|
23
|
+
}
|
|
24
|
+
/** Recorre todas las facturas página por página. */
|
|
25
|
+
paginar(params = {}) {
|
|
26
|
+
const { start, limit, ...query } = params;
|
|
27
|
+
return paginate(this.client, "invoices", {
|
|
28
|
+
start,
|
|
29
|
+
limit,
|
|
30
|
+
query: query,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Descarga todas las facturas que cumplan el filtro.
|
|
35
|
+
*
|
|
36
|
+
* Nota: para rangos de fechas muy amplios conviene usar {@link paginar} y procesar
|
|
37
|
+
* por página, o acotar con `date_afterOrNow` / `date_beforeOrNow`.
|
|
38
|
+
*/
|
|
39
|
+
listarTodas(params = {}) {
|
|
40
|
+
const { start, limit, ...query } = params;
|
|
41
|
+
return collectAll(this.client, "invoices", {
|
|
42
|
+
start,
|
|
43
|
+
limit,
|
|
44
|
+
query: query,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AlegraClient } from "../client.js";
|
|
2
|
+
import type { Item, ParametrosItems } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Recurso de ítems (productos y servicios).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const items = new Items(alegra);
|
|
8
|
+
* const todos = await items.listarTodos({ status: "active" });
|
|
9
|
+
*/
|
|
10
|
+
export declare class Items {
|
|
11
|
+
private readonly client;
|
|
12
|
+
constructor(client: AlegraClient);
|
|
13
|
+
/** Lista una sola página de ítems. */
|
|
14
|
+
listar(params?: ParametrosItems): Promise<Item[]>;
|
|
15
|
+
/** Obtiene un ítem por su id. */
|
|
16
|
+
obtener(id: string): Promise<Item>;
|
|
17
|
+
/** Recorre todos los ítems página por página. */
|
|
18
|
+
paginar(params?: ParametrosItems): AsyncGenerator<Item[], void, unknown>;
|
|
19
|
+
/** Descarga todos los ítems que cumplan el filtro. */
|
|
20
|
+
listarTodos(params?: ParametrosItems): Promise<Item[]>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { collectAll, paginate } from "../pagination.js";
|
|
2
|
+
/**
|
|
3
|
+
* Recurso de ítems (productos y servicios).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const items = new Items(alegra);
|
|
7
|
+
* const todos = await items.listarTodos({ status: "active" });
|
|
8
|
+
*/
|
|
9
|
+
export class Items {
|
|
10
|
+
client;
|
|
11
|
+
constructor(client) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
}
|
|
14
|
+
/** Lista una sola página de ítems. */
|
|
15
|
+
listar(params = {}) {
|
|
16
|
+
return this.client.request("items", { query: { ...params } });
|
|
17
|
+
}
|
|
18
|
+
/** Obtiene un ítem por su id. */
|
|
19
|
+
obtener(id) {
|
|
20
|
+
return this.client.request(`items/${encodeURIComponent(id)}`);
|
|
21
|
+
}
|
|
22
|
+
/** Recorre todos los ítems página por página. */
|
|
23
|
+
paginar(params = {}) {
|
|
24
|
+
const { start, limit, ...query } = params;
|
|
25
|
+
return paginate(this.client, "items", {
|
|
26
|
+
start,
|
|
27
|
+
limit,
|
|
28
|
+
query: query,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Descarga todos los ítems que cumplan el filtro. */
|
|
32
|
+
listarTodos(params = {}) {
|
|
33
|
+
const { start, limit, ...query } = params;
|
|
34
|
+
return collectAll(this.client, "items", {
|
|
35
|
+
start,
|
|
36
|
+
limit,
|
|
37
|
+
query: query,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de los objetos de la API de Alegra usados por este cliente.
|
|
3
|
+
*
|
|
4
|
+
* Los tipos reflejan los campos que la API devuelve con más frecuencia. No pretenden
|
|
5
|
+
* ser exhaustivos: la API puede incluir campos adicionales según el plan y la
|
|
6
|
+
* configuración de cada cuenta, por eso las interfaces admiten propiedades extra.
|
|
7
|
+
*/
|
|
8
|
+
/** Estado activo/inactivo común a varios recursos. */
|
|
9
|
+
export type EstadoRecurso = "active" | "inactive";
|
|
10
|
+
/** Moneda tal como la expone Alegra. */
|
|
11
|
+
export interface Moneda {
|
|
12
|
+
code: string;
|
|
13
|
+
symbol?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Precio de un ítem dentro de una lista de precios. */
|
|
16
|
+
export interface PrecioItem {
|
|
17
|
+
idPriceList?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
price: number;
|
|
21
|
+
currency?: Moneda;
|
|
22
|
+
main?: boolean;
|
|
23
|
+
edited?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Contacto (cliente o proveedor). */
|
|
26
|
+
export interface Contacto {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
identification?: string | null;
|
|
30
|
+
email?: string | null;
|
|
31
|
+
phonePrimary?: string | null;
|
|
32
|
+
phoneSecondary?: string | null;
|
|
33
|
+
mobile?: string | null;
|
|
34
|
+
status?: EstadoRecurso;
|
|
35
|
+
type?: string[];
|
|
36
|
+
observations?: string | null;
|
|
37
|
+
address?: Record<string, unknown> | null;
|
|
38
|
+
created_at?: string;
|
|
39
|
+
updated_at?: string;
|
|
40
|
+
/** Campos adicionales devueltos por la API. */
|
|
41
|
+
[extra: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
/** Ítem / producto o servicio. */
|
|
44
|
+
export interface Item {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
description?: string | null;
|
|
48
|
+
reference?: string | null;
|
|
49
|
+
status?: EstadoRecurso;
|
|
50
|
+
price?: PrecioItem[];
|
|
51
|
+
type?: string;
|
|
52
|
+
[extra: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/** Numeración/plantilla de una factura. */
|
|
55
|
+
export interface NumeracionFactura {
|
|
56
|
+
id?: string;
|
|
57
|
+
prefix?: string;
|
|
58
|
+
number?: string;
|
|
59
|
+
fullNumber?: string;
|
|
60
|
+
documentType?: string;
|
|
61
|
+
isElectronic?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Referencia mínima al cliente dentro de una factura. */
|
|
64
|
+
export interface ClienteFactura {
|
|
65
|
+
id: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
identification?: string;
|
|
68
|
+
}
|
|
69
|
+
/** Factura de venta. */
|
|
70
|
+
export interface Factura {
|
|
71
|
+
id: string;
|
|
72
|
+
date: string;
|
|
73
|
+
dueDate?: string;
|
|
74
|
+
status?: string;
|
|
75
|
+
subtotal?: number;
|
|
76
|
+
discount?: number;
|
|
77
|
+
tax?: number;
|
|
78
|
+
total: number;
|
|
79
|
+
totalPaid?: number;
|
|
80
|
+
balance?: number;
|
|
81
|
+
numberTemplate?: NumeracionFactura;
|
|
82
|
+
client?: ClienteFactura;
|
|
83
|
+
[extra: string]: unknown;
|
|
84
|
+
}
|
|
85
|
+
/** Parámetros de listado con paginación (comunes a los recursos). */
|
|
86
|
+
export interface ParametrosListado {
|
|
87
|
+
/** Desplazamiento (offset). Por defecto 0. */
|
|
88
|
+
start?: number;
|
|
89
|
+
/** Tamaño de página. Máximo permitido por Alegra: 30. */
|
|
90
|
+
limit?: number;
|
|
91
|
+
/** Campo por el cual ordenar. */
|
|
92
|
+
order_field?: string;
|
|
93
|
+
/** Dirección del orden. */
|
|
94
|
+
order_direction?: "ASC" | "DESC";
|
|
95
|
+
}
|
|
96
|
+
/** Parámetros específicos para listar contactos. */
|
|
97
|
+
export interface ParametrosContactos extends ParametrosListado {
|
|
98
|
+
type?: "client" | "provider";
|
|
99
|
+
identification?: string;
|
|
100
|
+
query?: string;
|
|
101
|
+
}
|
|
102
|
+
/** Parámetros específicos para listar facturas. */
|
|
103
|
+
export interface ParametrosFacturas extends ParametrosListado {
|
|
104
|
+
/** Fecha exacta de creación (YYYY-MM-DD). */
|
|
105
|
+
date?: string;
|
|
106
|
+
/** Creadas en o después de (YYYY-MM-DD). */
|
|
107
|
+
date_afterOrNow?: string;
|
|
108
|
+
/** Creadas en o antes de (YYYY-MM-DD). */
|
|
109
|
+
date_beforeOrNow?: string;
|
|
110
|
+
/** Estado: open, closed, draft, void (admite lista separada por comas). */
|
|
111
|
+
status?: string;
|
|
112
|
+
client_id?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Parámetros específicos para listar ítems. */
|
|
115
|
+
export interface ParametrosItems extends ParametrosListado {
|
|
116
|
+
name?: string;
|
|
117
|
+
reference?: string;
|
|
118
|
+
query?: string;
|
|
119
|
+
status?: EstadoRecurso;
|
|
120
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de los objetos de la API de Alegra usados por este cliente.
|
|
3
|
+
*
|
|
4
|
+
* Los tipos reflejan los campos que la API devuelve con más frecuencia. No pretenden
|
|
5
|
+
* ser exhaustivos: la API puede incluir campos adicionales según el plan y la
|
|
6
|
+
* configuración de cada cuenta, por eso las interfaces admiten propiedades extra.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "alegra-api-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cliente ligero y tipado para la API de Alegra (contactos, ítems y facturas): autenticación, paginación y manejo de rate limits.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"lint:types": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"alegra",
|
|
31
|
+
"alegra-api",
|
|
32
|
+
"facturacion",
|
|
33
|
+
"contabilidad",
|
|
34
|
+
"api-client",
|
|
35
|
+
"typescript"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.14.0",
|
|
40
|
+
"typescript": "^5.5.0",
|
|
41
|
+
"vitest": "^2.0.0"
|
|
42
|
+
},
|
|
43
|
+
"author": "Gary Dormoi",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/garydormoi/alegra-api-client.git"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/garydormoi/alegra-api-client#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/garydormoi/alegra-api-client/issues"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|