@pimia/sdk 0.6.0 → 0.8.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 +50 -0
- package/dist/api.d.ts +3319 -756
- package/dist/api.js +4 -0
- package/dist/client.d.ts +137 -52
- package/dist/client.js +160 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/api.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* This file was auto-generated by openapi-typescript.
|
|
3
3
|
* Do not make direct changes to the file.
|
|
4
|
+
*
|
|
5
|
+
* Generado con `npm run generate:types` (scripts/generate-types.mjs), que
|
|
6
|
+
* añade UN transform al generador: `format: binary` sale como `Blob` y no
|
|
7
|
+
* como `string`. El porqué, en ese fichero.
|
|
4
8
|
*/
|
|
5
9
|
export {};
|
package/dist/client.d.ts
CHANGED
|
@@ -64,8 +64,43 @@ export interface RequestOptions {
|
|
|
64
64
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
65
65
|
/** Query string. Los `undefined`/`null` se omiten; los arrays se repiten. */
|
|
66
66
|
query?: Record<string, string | number | boolean | undefined | null | Array<string | number>>;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* El cuerpo de la petición.
|
|
69
|
+
*
|
|
70
|
+
* Por defecto se manda como **JSON**, que es lo que pide casi toda la API.
|
|
71
|
+
*
|
|
72
|
+
* Diez operaciones del contrato son `multipart/form-data` —el justificante
|
|
73
|
+
* de un gasto, el documento de una factura recibida, importar un extracto
|
|
74
|
+
* bancario, el membrete de una plantilla, el certificado de firma…—, y para
|
|
75
|
+
* ésas se pasa un {@link FormData}: el cliente lo manda **tal cual** y **no
|
|
76
|
+
* le pone `content-type`**, para que el runtime escriba el suyo con su
|
|
77
|
+
* `boundary`. {@link toFormData} lo arma con las conversiones que el
|
|
78
|
+
* servidor espera.
|
|
79
|
+
*
|
|
80
|
+
* También pasan sin tocar `Blob`, `URLSearchParams`, `ArrayBuffer` y las
|
|
81
|
+
* vistas de `ArrayBuffer`.
|
|
82
|
+
*
|
|
83
|
+
* ⛔ Un `ReadableStream` **no**, y es a propósito: este cliente reintenta
|
|
84
|
+
* ante un 401 (tras refrescar) y ante un 429, y un stream ya consumido no se
|
|
85
|
+
* puede volver a mandar — el reintento fallaría con un error que no se
|
|
86
|
+
* parece en nada a su causa. Los cinco de arriba se pueden releer.
|
|
87
|
+
*/
|
|
68
88
|
body?: unknown;
|
|
89
|
+
/**
|
|
90
|
+
* Cómo leer una respuesta **correcta**.
|
|
91
|
+
*
|
|
92
|
+
* `'json'` (el defecto) es lo de siempre. `'blob'` es para las dos
|
|
93
|
+
* operaciones que devuelven un fichero (`application/octet-stream`):
|
|
94
|
+
* descargar el membrete de una plantilla y el documento escaneado de una
|
|
95
|
+
* factura recibida.
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ Sin esto, un PDF se lee con `response.text()` y **se corrompe en
|
|
98
|
+
* silencio**: el fichero «llega», pesa lo suyo y no se abre.
|
|
99
|
+
*
|
|
100
|
+
* Los errores se siguen leyendo como JSON aunque pidas `'blob'` — cuando la
|
|
101
|
+
* API falla contesta su sobre de error, no el fichero.
|
|
102
|
+
*/
|
|
103
|
+
responseType?: 'json' | 'blob';
|
|
69
104
|
headers?: Record<string, string>;
|
|
70
105
|
signal?: AbortSignal;
|
|
71
106
|
/**
|
|
@@ -164,12 +199,12 @@ export declare class PimiaClient {
|
|
|
164
199
|
invoice_date: string;
|
|
165
200
|
due_date: string | null;
|
|
166
201
|
invoice_number: string | null;
|
|
167
|
-
reference_number: string;
|
|
202
|
+
reference_number: string | null;
|
|
168
203
|
status: string;
|
|
169
204
|
paid_status: string;
|
|
170
|
-
tax_per_item: string;
|
|
205
|
+
tax_per_item: string | null;
|
|
171
206
|
tax_included: string;
|
|
172
|
-
discount_per_item: string;
|
|
207
|
+
discount_per_item: string | null;
|
|
173
208
|
notes: string | null;
|
|
174
209
|
discount_type: string | null;
|
|
175
210
|
discount: number;
|
|
@@ -178,7 +213,7 @@ export declare class PimiaClient {
|
|
|
178
213
|
total: number;
|
|
179
214
|
effective_total: string;
|
|
180
215
|
tax: number;
|
|
181
|
-
due_amount:
|
|
216
|
+
due_amount: number;
|
|
182
217
|
effective_due_amount: string;
|
|
183
218
|
sent: string;
|
|
184
219
|
viewed: string;
|
|
@@ -186,19 +221,19 @@ export declare class PimiaClient {
|
|
|
186
221
|
viewed_at: string;
|
|
187
222
|
unique_hash: string | null;
|
|
188
223
|
template_name: string | null;
|
|
189
|
-
invoice_series_id:
|
|
224
|
+
invoice_series_id: number | null;
|
|
190
225
|
customer_id: number | null;
|
|
191
226
|
external_ref: string | null;
|
|
192
|
-
payment_method_id:
|
|
193
|
-
recurring_invoice_id:
|
|
227
|
+
payment_method_id: number | null;
|
|
228
|
+
recurring_invoice_id: number | null;
|
|
194
229
|
sequence_number: number | null;
|
|
195
230
|
exchange_rate: number | null;
|
|
196
|
-
base_discount_val:
|
|
197
|
-
base_sub_total:
|
|
198
|
-
base_total:
|
|
231
|
+
base_discount_val: number;
|
|
232
|
+
base_sub_total: number;
|
|
233
|
+
base_total: number;
|
|
199
234
|
creator_id: number | null;
|
|
200
|
-
base_tax:
|
|
201
|
-
base_due_amount:
|
|
235
|
+
base_tax: number;
|
|
236
|
+
base_due_amount: number;
|
|
202
237
|
effective_base_total: string;
|
|
203
238
|
effective_base_due_amount: string;
|
|
204
239
|
credited_total: string;
|
|
@@ -223,7 +258,7 @@ export declare class PimiaClient {
|
|
|
223
258
|
rectified_invoice?: {
|
|
224
259
|
id: number;
|
|
225
260
|
invoice_number: string | null;
|
|
226
|
-
tax_per_item: string;
|
|
261
|
+
tax_per_item: string | null;
|
|
227
262
|
tax_included: string;
|
|
228
263
|
sub_total: number;
|
|
229
264
|
discount_val: number;
|
|
@@ -237,8 +272,8 @@ export declare class PimiaClient {
|
|
|
237
272
|
payments?: components["schemas"]["PaymentResource"][];
|
|
238
273
|
email_logs?: components["schemas"]["EmailLogResource"][];
|
|
239
274
|
customer?: components["schemas"]["CustomerResource"] | null;
|
|
240
|
-
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
241
|
-
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
275
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"] | null;
|
|
276
|
+
payment_method?: components["schemas"]["PaymentMethodResource"] | null;
|
|
242
277
|
creator?: components["schemas"]["UserResource"] | null;
|
|
243
278
|
taxes: components["schemas"]["TaxResource"][];
|
|
244
279
|
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
@@ -251,12 +286,12 @@ export declare class PimiaClient {
|
|
|
251
286
|
invoice_date: string;
|
|
252
287
|
due_date: string | null;
|
|
253
288
|
invoice_number: string | null;
|
|
254
|
-
reference_number: string;
|
|
289
|
+
reference_number: string | null;
|
|
255
290
|
status: string;
|
|
256
291
|
paid_status: string;
|
|
257
|
-
tax_per_item: string;
|
|
292
|
+
tax_per_item: string | null;
|
|
258
293
|
tax_included: string;
|
|
259
|
-
discount_per_item: string;
|
|
294
|
+
discount_per_item: string | null;
|
|
260
295
|
notes: string | null;
|
|
261
296
|
discount_type: string | null;
|
|
262
297
|
discount: number;
|
|
@@ -265,7 +300,7 @@ export declare class PimiaClient {
|
|
|
265
300
|
total: number;
|
|
266
301
|
effective_total: string;
|
|
267
302
|
tax: number;
|
|
268
|
-
due_amount:
|
|
303
|
+
due_amount: number;
|
|
269
304
|
effective_due_amount: string;
|
|
270
305
|
sent: string;
|
|
271
306
|
viewed: string;
|
|
@@ -273,19 +308,19 @@ export declare class PimiaClient {
|
|
|
273
308
|
viewed_at: string;
|
|
274
309
|
unique_hash: string | null;
|
|
275
310
|
template_name: string | null;
|
|
276
|
-
invoice_series_id:
|
|
311
|
+
invoice_series_id: number | null;
|
|
277
312
|
customer_id: number | null;
|
|
278
313
|
external_ref: string | null;
|
|
279
|
-
payment_method_id:
|
|
280
|
-
recurring_invoice_id:
|
|
314
|
+
payment_method_id: number | null;
|
|
315
|
+
recurring_invoice_id: number | null;
|
|
281
316
|
sequence_number: number | null;
|
|
282
317
|
exchange_rate: number | null;
|
|
283
|
-
base_discount_val:
|
|
284
|
-
base_sub_total:
|
|
285
|
-
base_total:
|
|
318
|
+
base_discount_val: number;
|
|
319
|
+
base_sub_total: number;
|
|
320
|
+
base_total: number;
|
|
286
321
|
creator_id: number | null;
|
|
287
|
-
base_tax:
|
|
288
|
-
base_due_amount:
|
|
322
|
+
base_tax: number;
|
|
323
|
+
base_due_amount: number;
|
|
289
324
|
effective_base_total: string;
|
|
290
325
|
effective_base_due_amount: string;
|
|
291
326
|
credited_total: string;
|
|
@@ -310,7 +345,7 @@ export declare class PimiaClient {
|
|
|
310
345
|
rectified_invoice?: {
|
|
311
346
|
id: number;
|
|
312
347
|
invoice_number: string | null;
|
|
313
|
-
tax_per_item: string;
|
|
348
|
+
tax_per_item: string | null;
|
|
314
349
|
tax_included: string;
|
|
315
350
|
sub_total: number;
|
|
316
351
|
discount_val: number;
|
|
@@ -324,8 +359,8 @@ export declare class PimiaClient {
|
|
|
324
359
|
payments?: components["schemas"]["PaymentResource"][];
|
|
325
360
|
email_logs?: components["schemas"]["EmailLogResource"][];
|
|
326
361
|
customer?: components["schemas"]["CustomerResource"] | null;
|
|
327
|
-
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
328
|
-
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
362
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"] | null;
|
|
363
|
+
payment_method?: components["schemas"]["PaymentMethodResource"] | null;
|
|
329
364
|
creator?: components["schemas"]["UserResource"] | null;
|
|
330
365
|
taxes: components["schemas"]["TaxResource"][];
|
|
331
366
|
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
@@ -362,17 +397,17 @@ export declare class PimiaClient {
|
|
|
362
397
|
enable_portal: boolean;
|
|
363
398
|
password_added: boolean;
|
|
364
399
|
currency_id: number | null;
|
|
365
|
-
payment_method_id:
|
|
400
|
+
payment_method_id: number | null;
|
|
366
401
|
company_id: number | null;
|
|
367
402
|
created_at: string | null;
|
|
368
403
|
formatted_created_at: string;
|
|
369
404
|
updated_at: string | null;
|
|
370
405
|
avatar: string;
|
|
371
|
-
due_amount:
|
|
372
|
-
base_due_amount:
|
|
406
|
+
due_amount: number | null;
|
|
407
|
+
base_due_amount: number | null;
|
|
373
408
|
prefix: string | null;
|
|
374
|
-
tax_id: string;
|
|
375
|
-
notes: string;
|
|
409
|
+
tax_id: string | null;
|
|
410
|
+
notes: string | null;
|
|
376
411
|
external_ref: string | null;
|
|
377
412
|
iban: string | null;
|
|
378
413
|
bic: string | null;
|
|
@@ -383,7 +418,7 @@ export declare class PimiaClient {
|
|
|
383
418
|
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
384
419
|
company?: components["schemas"]["CompanyResource"] | null;
|
|
385
420
|
currency?: components["schemas"]["CurrencyResource"] | null;
|
|
386
|
-
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
421
|
+
payment_method?: components["schemas"]["PaymentMethodResource"] | null;
|
|
387
422
|
}>>;
|
|
388
423
|
};
|
|
389
424
|
get estimates(): {
|
|
@@ -444,12 +479,12 @@ export declare class PimiaClient {
|
|
|
444
479
|
invoice_date: string;
|
|
445
480
|
due_date: string | null;
|
|
446
481
|
invoice_number: string | null;
|
|
447
|
-
reference_number: string;
|
|
482
|
+
reference_number: string | null;
|
|
448
483
|
status: string;
|
|
449
484
|
paid_status: string;
|
|
450
|
-
tax_per_item: string;
|
|
485
|
+
tax_per_item: string | null;
|
|
451
486
|
tax_included: string;
|
|
452
|
-
discount_per_item: string;
|
|
487
|
+
discount_per_item: string | null;
|
|
453
488
|
notes: string | null;
|
|
454
489
|
discount_type: string | null;
|
|
455
490
|
discount: number;
|
|
@@ -458,7 +493,7 @@ export declare class PimiaClient {
|
|
|
458
493
|
total: number;
|
|
459
494
|
effective_total: string;
|
|
460
495
|
tax: number;
|
|
461
|
-
due_amount:
|
|
496
|
+
due_amount: number;
|
|
462
497
|
effective_due_amount: string;
|
|
463
498
|
sent: string;
|
|
464
499
|
viewed: string;
|
|
@@ -466,19 +501,19 @@ export declare class PimiaClient {
|
|
|
466
501
|
viewed_at: string;
|
|
467
502
|
unique_hash: string | null;
|
|
468
503
|
template_name: string | null;
|
|
469
|
-
invoice_series_id:
|
|
504
|
+
invoice_series_id: number | null;
|
|
470
505
|
customer_id: number | null;
|
|
471
506
|
external_ref: string | null;
|
|
472
|
-
payment_method_id:
|
|
473
|
-
recurring_invoice_id:
|
|
507
|
+
payment_method_id: number | null;
|
|
508
|
+
recurring_invoice_id: number | null;
|
|
474
509
|
sequence_number: number | null;
|
|
475
510
|
exchange_rate: number | null;
|
|
476
|
-
base_discount_val:
|
|
477
|
-
base_sub_total:
|
|
478
|
-
base_total:
|
|
511
|
+
base_discount_val: number;
|
|
512
|
+
base_sub_total: number;
|
|
513
|
+
base_total: number;
|
|
479
514
|
creator_id: number | null;
|
|
480
|
-
base_tax:
|
|
481
|
-
base_due_amount:
|
|
515
|
+
base_tax: number;
|
|
516
|
+
base_due_amount: number;
|
|
482
517
|
effective_base_total: string;
|
|
483
518
|
effective_base_due_amount: string;
|
|
484
519
|
credited_total: string;
|
|
@@ -503,7 +538,7 @@ export declare class PimiaClient {
|
|
|
503
538
|
rectified_invoice?: {
|
|
504
539
|
id: number;
|
|
505
540
|
invoice_number: string | null;
|
|
506
|
-
tax_per_item: string;
|
|
541
|
+
tax_per_item: string | null;
|
|
507
542
|
tax_included: string;
|
|
508
543
|
sub_total: number;
|
|
509
544
|
discount_val: number;
|
|
@@ -517,8 +552,8 @@ export declare class PimiaClient {
|
|
|
517
552
|
payments?: components["schemas"]["PaymentResource"][];
|
|
518
553
|
email_logs?: components["schemas"]["EmailLogResource"][];
|
|
519
554
|
customer?: components["schemas"]["CustomerResource"] | null;
|
|
520
|
-
invoice_series?: components["schemas"]["InvoiceSeriesResource"];
|
|
521
|
-
payment_method?: components["schemas"]["PaymentMethodResource"];
|
|
555
|
+
invoice_series?: components["schemas"]["InvoiceSeriesResource"] | null;
|
|
556
|
+
payment_method?: components["schemas"]["PaymentMethodResource"] | null;
|
|
522
557
|
creator?: components["schemas"]["UserResource"] | null;
|
|
523
558
|
taxes: components["schemas"]["TaxResource"][];
|
|
524
559
|
fields?: components["schemas"]["CustomFieldValueResource"][];
|
|
@@ -531,6 +566,25 @@ export declare class PimiaClient {
|
|
|
531
566
|
put<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
532
567
|
patch<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
533
568
|
delete<T = unknown>(path: string, options?: ReadOptions): Promise<T>;
|
|
569
|
+
/**
|
|
570
|
+
* Descarga un fichero de la API y te lo da como `Blob`.
|
|
571
|
+
*
|
|
572
|
+
* Son dos operaciones: el membrete de una plantilla
|
|
573
|
+
* (`GET /invoice-templates/{id}/letterhead`) y el documento escaneado de una
|
|
574
|
+
* factura recibida (`GET /received-invoices/{id}/show/document`).
|
|
575
|
+
*
|
|
576
|
+
* Existe porque `get()` **corrompe un binario sin decirlo**: lee la
|
|
577
|
+
* respuesta con `response.text()`, y un PDF pasado por ahí llega entero de
|
|
578
|
+
* tamaño y no se abre. Ese es el peor final posible para una descarga, así
|
|
579
|
+
* que la forma correcta tiene nombre propio en vez de ser una bandera que
|
|
580
|
+
* hay que acordarse de poner.
|
|
581
|
+
*
|
|
582
|
+
* ```ts
|
|
583
|
+
* const pdf = await client.download(`/received-invoices/${id}/show/document`)
|
|
584
|
+
* const url = URL.createObjectURL(pdf)
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
download(path: string, query?: RequestOptions['query'], options?: ReadOptions): Promise<Blob>;
|
|
534
588
|
/**
|
|
535
589
|
* Petición cruda contra `/api/v1`. `path` puede llevar el prefijo o no:
|
|
536
590
|
* `/invoices` y `/api/v1/invoices` son lo mismo.
|
|
@@ -565,4 +619,35 @@ export declare class PimiaClient {
|
|
|
565
619
|
private captureRateLimit;
|
|
566
620
|
private retryDelay;
|
|
567
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Arma el `FormData` de una operación multipart con las conversiones que el
|
|
624
|
+
* servidor de Pimia espera, que **no** son las que hace `FormData` sola.
|
|
625
|
+
*
|
|
626
|
+
* Tres reglas, y las tres salen del contrato, no de la costumbre:
|
|
627
|
+
*
|
|
628
|
+
* - **Los booleanos viajan como `1` y `0`.** Lo dice el propio spec en
|
|
629
|
+
* `ExpenseRequest.is_attachment_receipt_removed`: «en `multipart/form-data`
|
|
630
|
+
* viaja como `1` o `0`». Un `String(false)` daría `"false"`, que PHP lee
|
|
631
|
+
* como verdadero.
|
|
632
|
+
* - **Los objetos y arrays viajan como JSON en una cadena.** También del
|
|
633
|
+
* spec, en `ExpenseRequest.customFields`: «viaja como cadena JSON:
|
|
634
|
+
* `[{"id":3,"value":"REF-42"}]`».
|
|
635
|
+
* - **`null` y `undefined` se omiten**, en vez de mandar `"null"`. Un campo
|
|
636
|
+
* ausente es un campo ausente; la cadena `"null"` es un valor.
|
|
637
|
+
*
|
|
638
|
+
* Un `Blob` o un `File` se añaden tal cual. Con un `File` el runtime manda ya
|
|
639
|
+
* su nombre; con un `Blob` suelto se puede dar uno pasando `[blob, 'x.pdf']`,
|
|
640
|
+
* que es la forma que el tercer argumento de `append` admite.
|
|
641
|
+
*
|
|
642
|
+
* ```ts
|
|
643
|
+
* await client.post('/expenses', toFormData({
|
|
644
|
+
* expense_date: '2026-08-24',
|
|
645
|
+
* expense_category_id: 3,
|
|
646
|
+
* amount: 12100,
|
|
647
|
+
* attachment_receipt: ficheroPdf,
|
|
648
|
+
* customFields: [{ id: 3, value: 'REF-42' }],
|
|
649
|
+
* }))
|
|
650
|
+
* ```
|
|
651
|
+
*/
|
|
652
|
+
export declare function toFormData(fields: Record<string, unknown | [Blob, string]>): FormData;
|
|
568
653
|
export {};
|
package/dist/client.js
CHANGED
|
@@ -127,6 +127,32 @@ export class PimiaClient {
|
|
|
127
127
|
delete(path, options) {
|
|
128
128
|
return this.request(path, { ...options, method: 'DELETE' });
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Descarga un fichero de la API y te lo da como `Blob`.
|
|
132
|
+
*
|
|
133
|
+
* Son dos operaciones: el membrete de una plantilla
|
|
134
|
+
* (`GET /invoice-templates/{id}/letterhead`) y el documento escaneado de una
|
|
135
|
+
* factura recibida (`GET /received-invoices/{id}/show/document`).
|
|
136
|
+
*
|
|
137
|
+
* Existe porque `get()` **corrompe un binario sin decirlo**: lee la
|
|
138
|
+
* respuesta con `response.text()`, y un PDF pasado por ahí llega entero de
|
|
139
|
+
* tamaño y no se abre. Ese es el peor final posible para una descarga, así
|
|
140
|
+
* que la forma correcta tiene nombre propio en vez de ser una bandera que
|
|
141
|
+
* hay que acordarse de poner.
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* const pdf = await client.download(`/received-invoices/${id}/show/document`)
|
|
145
|
+
* const url = URL.createObjectURL(pdf)
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
download(path, query, options) {
|
|
149
|
+
return this.request(path, {
|
|
150
|
+
...options,
|
|
151
|
+
method: 'GET',
|
|
152
|
+
query,
|
|
153
|
+
responseType: 'blob',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
130
156
|
/**
|
|
131
157
|
* Petición cruda contra `/api/v1`. `path` puede llevar el prefijo o no:
|
|
132
158
|
* `/invoices` y `/api/v1/invoices` son lo mismo.
|
|
@@ -152,6 +178,13 @@ export class PimiaClient {
|
|
|
152
178
|
* ```
|
|
153
179
|
*/
|
|
154
180
|
async requestWithMeta(path, options = {}) {
|
|
181
|
+
/* El cuerpo se clasifica UNA vez, fuera del bucle: lo que se manda no
|
|
182
|
+
cambia entre el intento y su reintento, y decidirlo dentro invitaría a
|
|
183
|
+
que algún día dejaran de coincidir. */
|
|
184
|
+
const cuerpoNativo = esCuerpoNativo(options.body);
|
|
185
|
+
if (cuerpoNativo) {
|
|
186
|
+
exigirSinContentType(options.body, { ...this.extraHeaders, ...options.headers });
|
|
187
|
+
}
|
|
155
188
|
let tokens = await this.currentTokens();
|
|
156
189
|
if (isExpired(tokens, this.skew)) {
|
|
157
190
|
tokens = await this.refreshTokens(tokens);
|
|
@@ -162,8 +195,17 @@ export class PimiaClient {
|
|
|
162
195
|
const response = await this.doFetch(this.urlFor(path, options.query), {
|
|
163
196
|
method: options.method ?? 'GET',
|
|
164
197
|
headers: {
|
|
165
|
-
|
|
166
|
-
|
|
198
|
+
/* Una descarga no pide JSON: si se dejara `application/json` fijo, un
|
|
199
|
+
servidor que negocie el tipo tendría derecho a contestar 406 —o a
|
|
200
|
+
mandar un JSON de error donde se esperaba el fichero. */
|
|
201
|
+
accept: options.responseType === 'blob' ? '*/*' : 'application/json',
|
|
202
|
+
/* Un cuerpo nativo trae su propio tipo: el runtime le pone
|
|
203
|
+
`multipart/form-data` CON su `boundary`, o el de un `Blob`, o
|
|
204
|
+
`application/x-www-form-urlencoded`. Escribirlo aquí a mano se lo
|
|
205
|
+
quitaría, y sin `boundary` el servidor no puede parsear nada. */
|
|
206
|
+
...(options.body === undefined || cuerpoNativo
|
|
207
|
+
? {}
|
|
208
|
+
: { 'content-type': 'application/json' }),
|
|
167
209
|
...this.extraHeaders,
|
|
168
210
|
...options.headers,
|
|
169
211
|
// Después de `options.headers` para que la opción con nombre mande
|
|
@@ -174,13 +216,23 @@ export class PimiaClient {
|
|
|
174
216
|
: { 'idempotency-key': options.idempotencyKey }),
|
|
175
217
|
authorization: `Bearer ${tokens.accessToken}`,
|
|
176
218
|
},
|
|
177
|
-
body: options.body === undefined
|
|
219
|
+
body: options.body === undefined
|
|
220
|
+
? undefined
|
|
221
|
+
: cuerpoNativo
|
|
222
|
+
? options.body
|
|
223
|
+
: JSON.stringify(options.body),
|
|
178
224
|
signal: options.signal,
|
|
179
225
|
});
|
|
180
226
|
this.captureRateLimit(response);
|
|
181
227
|
if (response.ok) {
|
|
182
228
|
return {
|
|
183
|
-
|
|
229
|
+
/* Una descarga se devuelve como `Blob` SIN pasar por `parseBody`,
|
|
230
|
+
que hace `response.text()`: un PDF leído como texto se corrompe en
|
|
231
|
+
la primera secuencia que no sea UTF-8 válido, y lo hace en
|
|
232
|
+
silencio — el fichero «llega» y no se abre. */
|
|
233
|
+
data: (options.responseType === 'blob'
|
|
234
|
+
? await response.blob()
|
|
235
|
+
: await parseBody(response)),
|
|
184
236
|
meta: {
|
|
185
237
|
status: response.status,
|
|
186
238
|
// Presente solo cuando Pimia reproduce; su ausencia significa
|
|
@@ -287,6 +339,110 @@ export class PimiaClient {
|
|
|
287
339
|
return Math.min(base, this.maxRetryDelayMs);
|
|
288
340
|
}
|
|
289
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* ¿Es un cuerpo que el runtime serializa por su cuenta?
|
|
344
|
+
*
|
|
345
|
+
* Los cinco de la lista tienen dos cosas en común, y las dos importan: `fetch`
|
|
346
|
+
* sabe convertirlos y **se pueden releer**. Lo segundo es lo que decide quién
|
|
347
|
+
* entra: este cliente reintenta ante un 401 (después de refrescar) y ante un
|
|
348
|
+
* 429, así que un cuerpo de un solo uso —un `ReadableStream`— reventaría en el
|
|
349
|
+
* reintento con un «body already used» que no se parece en nada a su causa.
|
|
350
|
+
*
|
|
351
|
+
* Los `typeof … !== 'undefined'` no son celo: este paquete corre en Node y en
|
|
352
|
+
* el navegador, y aunque Node 20 los trae todos, un runtime recortado que no
|
|
353
|
+
* tenga `FormData` debe fallar en el `instanceof`, no al evaluarlo.
|
|
354
|
+
*/
|
|
355
|
+
function esCuerpoNativo(body) {
|
|
356
|
+
if (body === undefined || body === null)
|
|
357
|
+
return false;
|
|
358
|
+
return ((typeof FormData !== 'undefined' && body instanceof FormData) ||
|
|
359
|
+
(typeof Blob !== 'undefined' && body instanceof Blob) ||
|
|
360
|
+
(typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) ||
|
|
361
|
+
body instanceof ArrayBuffer ||
|
|
362
|
+
ArrayBuffer.isView(body));
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Un `FormData` con un `content-type` puesto a mano **no se manda**: se avisa.
|
|
366
|
+
*
|
|
367
|
+
* La cabecera de un multipart lleva el `boundary` que separa las partes, y lo
|
|
368
|
+
* genera el runtime al serializar. Escribir `content-type:
|
|
369
|
+
* multipart/form-data` a mano se lo quita, y entonces el servidor recibe un
|
|
370
|
+
* cuerpo que no puede parsear: contesta un 422 sobre un campo obligatorio que
|
|
371
|
+
* el cliente **sí mandó**, y el rastro no lleva a ninguna parte.
|
|
372
|
+
*
|
|
373
|
+
* Es un error de quien llama, no de la API, así que se lanza aquí y no se
|
|
374
|
+
* intenta arreglar por su cuenta: quitarle la cabecera en silencio dejaría en
|
|
375
|
+
* pie la creencia de que hacía falta.
|
|
376
|
+
*/
|
|
377
|
+
function exigirSinContentType(body, headers) {
|
|
378
|
+
if (typeof FormData === 'undefined' || !(body instanceof FormData))
|
|
379
|
+
return;
|
|
380
|
+
const puesta = Object.keys(headers).find((k) => k.toLowerCase() === 'content-type');
|
|
381
|
+
if (puesta === undefined)
|
|
382
|
+
return;
|
|
383
|
+
throw new TypeError('No le pongas `content-type` a un cuerpo FormData: el runtime escribe el suyo ' +
|
|
384
|
+
'con el `boundary` que separa las partes, y una cabecera a mano se lo quita ' +
|
|
385
|
+
`(el servidor respondería 422 sobre un campo que sí mandaste). Quita \`${puesta}\` ` +
|
|
386
|
+
'de las cabeceras de esta petición.');
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Arma el `FormData` de una operación multipart con las conversiones que el
|
|
390
|
+
* servidor de Pimia espera, que **no** son las que hace `FormData` sola.
|
|
391
|
+
*
|
|
392
|
+
* Tres reglas, y las tres salen del contrato, no de la costumbre:
|
|
393
|
+
*
|
|
394
|
+
* - **Los booleanos viajan como `1` y `0`.** Lo dice el propio spec en
|
|
395
|
+
* `ExpenseRequest.is_attachment_receipt_removed`: «en `multipart/form-data`
|
|
396
|
+
* viaja como `1` o `0`». Un `String(false)` daría `"false"`, que PHP lee
|
|
397
|
+
* como verdadero.
|
|
398
|
+
* - **Los objetos y arrays viajan como JSON en una cadena.** También del
|
|
399
|
+
* spec, en `ExpenseRequest.customFields`: «viaja como cadena JSON:
|
|
400
|
+
* `[{"id":3,"value":"REF-42"}]`».
|
|
401
|
+
* - **`null` y `undefined` se omiten**, en vez de mandar `"null"`. Un campo
|
|
402
|
+
* ausente es un campo ausente; la cadena `"null"` es un valor.
|
|
403
|
+
*
|
|
404
|
+
* Un `Blob` o un `File` se añaden tal cual. Con un `File` el runtime manda ya
|
|
405
|
+
* su nombre; con un `Blob` suelto se puede dar uno pasando `[blob, 'x.pdf']`,
|
|
406
|
+
* que es la forma que el tercer argumento de `append` admite.
|
|
407
|
+
*
|
|
408
|
+
* ```ts
|
|
409
|
+
* await client.post('/expenses', toFormData({
|
|
410
|
+
* expense_date: '2026-08-24',
|
|
411
|
+
* expense_category_id: 3,
|
|
412
|
+
* amount: 12100,
|
|
413
|
+
* attachment_receipt: ficheroPdf,
|
|
414
|
+
* customFields: [{ id: 3, value: 'REF-42' }],
|
|
415
|
+
* }))
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
export function toFormData(fields) {
|
|
419
|
+
const form = new FormData();
|
|
420
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
421
|
+
if (value === undefined || value === null)
|
|
422
|
+
continue;
|
|
423
|
+
if (Array.isArray(value) && value.length === 2 && esBlob(value[0]) && typeof value[1] === 'string') {
|
|
424
|
+
form.append(name, value[0], value[1]);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (esBlob(value)) {
|
|
428
|
+
form.append(name, value);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (typeof value === 'boolean') {
|
|
432
|
+
form.append(name, value ? '1' : '0');
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (typeof value === 'object') {
|
|
436
|
+
form.append(name, JSON.stringify(value));
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
form.append(name, String(value));
|
|
440
|
+
}
|
|
441
|
+
return form;
|
|
442
|
+
}
|
|
443
|
+
function esBlob(value) {
|
|
444
|
+
return typeof Blob !== 'undefined' && value instanceof Blob;
|
|
445
|
+
}
|
|
290
446
|
function retryAfterSeconds(response) {
|
|
291
447
|
const header = response.headers.get('retry-after');
|
|
292
448
|
if (header === null)
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* por README.md; el contrato completo de endpoints está en el OpenAPI del que
|
|
6
6
|
* salen los tipos de `./api`.
|
|
7
7
|
*/
|
|
8
|
-
export { PimiaClient } from './client.js';
|
|
8
|
+
export { PimiaClient, toFormData } from './client.js';
|
|
9
9
|
export type { CustomerRequest, CustomerResource, EstimateResource, EstimatesRequest, InvoiceResource, InvoicesRequest, PimiaClientOptions, RateLimit, ReadOptions, RequestOptions, ResourceEnvelope, ResponseMeta, ResponseWithMeta, WriteOptions, } from './client.js';
|
|
10
10
|
export { OAuth, createPkceChallenge, createState } from './oauth.js';
|
|
11
11
|
export type { AuthorizationServerMetadata, AuthorizeUrlOptions, OAuthConfig, PkceChallenge, } from './oauth.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* por README.md; el contrato completo de endpoints está en el OpenAPI del que
|
|
6
6
|
* salen los tipos de `./api`.
|
|
7
7
|
*/
|
|
8
|
-
export { PimiaClient } from './client.js';
|
|
8
|
+
export { PimiaClient, toFormData } from './client.js';
|
|
9
9
|
export { OAuth, createPkceChallenge, createState } from './oauth.js';
|
|
10
10
|
export { MemoryTokenStore, isExpired, tokenSetFromResponse } from './tokens.js';
|
|
11
11
|
export { DuplicateExternalRefError, ForbiddenError, MissingScopeError, NotAuthenticatedError, NotFoundError, OAuthError, PimiaApiError, PimiaError, RateLimitError, UnauthorizedError, ValidationError, } from './errors.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pimia/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Cliente TypeScript de la API de Pimia para apps de partner: OAuth con PKCE, rotación de refresh persistida, reintentos de rate limit y tipos generados del OpenAPI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Pimia (https://pimia.es)",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -p tsconfig.json",
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
-
"generate:types": "
|
|
43
|
+
"generate:types": "node scripts/generate-types.mjs",
|
|
44
44
|
"test": "node --test test/*.test.js",
|
|
45
45
|
"prepublishOnly": "npm run build"
|
|
46
46
|
},
|