@pimia/sdk 0.1.0 → 0.2.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 +33 -7
- package/dist/api.d.ts +1135 -127
- package/dist/client.d.ts +63 -8
- package/dist/client.js +48 -12
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -34,12 +34,50 @@ export interface RequestOptions {
|
|
|
34
34
|
body?: unknown;
|
|
35
35
|
headers?: Record<string, string>;
|
|
36
36
|
signal?: AbortSignal;
|
|
37
|
+
/**
|
|
38
|
+
* Clave de idempotencia para este `POST`. Manda una única por operación —un
|
|
39
|
+
* UUID nuevo— y reúsala SOLO en los reintentos de esa misma operación:
|
|
40
|
+
* Pimia ejecuta la escritura una vez y reproduce la respuesta original en
|
|
41
|
+
* los reintentos. La misma clave con otro cuerpo responde 422.
|
|
42
|
+
*
|
|
43
|
+
* Para saber si lo que recibiste es un eco y no una escritura nueva, usa
|
|
44
|
+
* {@link PimiaClient.requestWithMeta} y mira `meta.idempotentReplay`.
|
|
45
|
+
*/
|
|
46
|
+
idempotencyKey?: string;
|
|
37
47
|
}
|
|
38
48
|
/** Cabeceras de rate limit que devuelve la API en cada respuesta. */
|
|
39
49
|
export interface RateLimit {
|
|
40
50
|
limit?: number;
|
|
41
51
|
remaining?: number;
|
|
42
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Lo que la respuesta dice ADEMÁS del cuerpo.
|
|
55
|
+
*
|
|
56
|
+
* Va por petición y no como estado del cliente —al contrario que
|
|
57
|
+
* {@link PimiaClient.rateLimit}— a propósito: `idempotentReplay` solo
|
|
58
|
+
* significa algo referido a UNA llamada concreta, y justo se consulta cuando
|
|
59
|
+
* hay reintentos, que es cuando puede haber varias en vuelo. Un campo
|
|
60
|
+
* compartido en el cliente daría la respuesta de otra.
|
|
61
|
+
*/
|
|
62
|
+
export interface ResponseMeta {
|
|
63
|
+
status: number;
|
|
64
|
+
/**
|
|
65
|
+
* `true` si Pimia reprodujo la respuesta de una petición anterior con la
|
|
66
|
+
* misma `Idempotency-Key` en vez de volver a escribir. Es la diferencia
|
|
67
|
+
* entre «he creado la factura» y «ya estaba creada»: sin esto, un partner
|
|
68
|
+
* no puede distinguirlas en sus propios registros.
|
|
69
|
+
*/
|
|
70
|
+
idempotentReplay: boolean;
|
|
71
|
+
requestId?: string;
|
|
72
|
+
rateLimit: RateLimit;
|
|
73
|
+
}
|
|
74
|
+
/** Cuerpo y metadatos de una misma respuesta. */
|
|
75
|
+
export interface ResponseWithMeta<T> {
|
|
76
|
+
data: T;
|
|
77
|
+
meta: ResponseMeta;
|
|
78
|
+
}
|
|
79
|
+
/** Lo que se puede afinar en una escritura (`post`/`put`/`patch`). */
|
|
80
|
+
export type WriteOptions = Pick<RequestOptions, 'headers' | 'query' | 'signal' | 'idempotencyKey'>;
|
|
43
81
|
export declare class PimiaClient {
|
|
44
82
|
readonly oauth: OAuth;
|
|
45
83
|
private readonly baseUrl;
|
|
@@ -58,30 +96,47 @@ export declare class PimiaClient {
|
|
|
58
96
|
get invoices(): {
|
|
59
97
|
list: (query?: RequestOptions["query"]) => Promise<unknown>;
|
|
60
98
|
get: (id: number | string) => Promise<unknown>;
|
|
61
|
-
create: (body: unknown) => Promise<unknown>;
|
|
62
|
-
update: (id: number | string, body: unknown) => Promise<unknown>;
|
|
99
|
+
create: (body: unknown, options?: WriteOptions) => Promise<unknown>;
|
|
100
|
+
update: (id: number | string, body: unknown, options?: WriteOptions) => Promise<unknown>;
|
|
63
101
|
};
|
|
64
102
|
get customers(): {
|
|
65
103
|
list: (query?: RequestOptions["query"]) => Promise<unknown>;
|
|
66
104
|
get: (id: number | string) => Promise<unknown>;
|
|
67
|
-
create: (body: unknown) => Promise<unknown>;
|
|
68
|
-
update: (id: number | string, body: unknown) => Promise<unknown>;
|
|
105
|
+
create: (body: unknown, options?: WriteOptions) => Promise<unknown>;
|
|
106
|
+
update: (id: number | string, body: unknown, options?: WriteOptions) => Promise<unknown>;
|
|
69
107
|
};
|
|
70
108
|
get estimates(): {
|
|
71
109
|
list: (query?: RequestOptions["query"]) => Promise<unknown>;
|
|
72
110
|
get: (id: number | string) => Promise<unknown>;
|
|
73
|
-
create: (body: unknown) => Promise<unknown>;
|
|
111
|
+
create: (body: unknown, options?: WriteOptions) => Promise<unknown>;
|
|
74
112
|
};
|
|
75
113
|
get<T = unknown>(path: string, query?: RequestOptions['query']): Promise<T>;
|
|
76
|
-
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
77
|
-
put<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
78
|
-
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
114
|
+
post<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
115
|
+
put<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
116
|
+
patch<T = unknown>(path: string, body?: unknown, options?: WriteOptions): Promise<T>;
|
|
79
117
|
delete<T = unknown>(path: string): Promise<T>;
|
|
80
118
|
/**
|
|
81
119
|
* Petición cruda contra `/api/v1`. `path` puede llevar el prefijo o no:
|
|
82
120
|
* `/invoices` y `/api/v1/invoices` son lo mismo.
|
|
83
121
|
*/
|
|
84
122
|
request<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
123
|
+
/**
|
|
124
|
+
* Lo mismo que {@link request}, pero devuelve también los metadatos de la
|
|
125
|
+
* respuesta.
|
|
126
|
+
*
|
|
127
|
+
* Existe por la idempotencia: tras un reintento, el cuerpo es idéntico al de
|
|
128
|
+
* la primera llamada —ese es justo el contrato—, así que el cuerpo solo no
|
|
129
|
+
* dice si Pimia escribió o se limitó a repetirse. `meta.idempotentReplay` sí.
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* const clave = crypto.randomUUID()
|
|
133
|
+
* const { data, meta } = await client.requestWithMeta('/estimates', {
|
|
134
|
+
* method: 'POST', body, idempotencyKey: clave,
|
|
135
|
+
* })
|
|
136
|
+
* if (meta.idempotentReplay) log('el presupuesto ya existía; no se duplicó')
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
requestWithMeta<T = unknown>(path: string, options?: RequestOptions): Promise<ResponseWithMeta<T>>;
|
|
85
140
|
private currentTokens;
|
|
86
141
|
/**
|
|
87
142
|
* Refresca UNA sola vez aunque lo pidan N peticiones en paralelo, y persiste
|
package/dist/client.js
CHANGED
|
@@ -46,36 +46,36 @@ export class PimiaClient {
|
|
|
46
46
|
return {
|
|
47
47
|
list: (query) => this.get('/invoices', query),
|
|
48
48
|
get: (id) => this.get(`/invoices/${id}`),
|
|
49
|
-
create: (body) => this.post('/invoices', body),
|
|
50
|
-
update: (id, body) => this.put(`/invoices/${id}`, body),
|
|
49
|
+
create: (body, options) => this.post('/invoices', body, options),
|
|
50
|
+
update: (id, body, options) => this.put(`/invoices/${id}`, body, options),
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
get customers() {
|
|
54
54
|
return {
|
|
55
55
|
list: (query) => this.get('/customers', query),
|
|
56
56
|
get: (id) => this.get(`/customers/${id}`),
|
|
57
|
-
create: (body) => this.post('/customers', body),
|
|
58
|
-
update: (id, body) => this.put(`/customers/${id}`, body),
|
|
57
|
+
create: (body, options) => this.post('/customers', body, options),
|
|
58
|
+
update: (id, body, options) => this.put(`/customers/${id}`, body, options),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
get estimates() {
|
|
62
62
|
return {
|
|
63
63
|
list: (query) => this.get('/estimates', query),
|
|
64
64
|
get: (id) => this.get(`/estimates/${id}`),
|
|
65
|
-
create: (body) => this.post('/estimates', body),
|
|
65
|
+
create: (body, options) => this.post('/estimates', body, options),
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
get(path, query) {
|
|
69
69
|
return this.request(path, { method: 'GET', query });
|
|
70
70
|
}
|
|
71
|
-
post(path, body) {
|
|
72
|
-
return this.request(path, { method: 'POST', body });
|
|
71
|
+
post(path, body, options) {
|
|
72
|
+
return this.request(path, { ...options, method: 'POST', body });
|
|
73
73
|
}
|
|
74
|
-
put(path, body) {
|
|
75
|
-
return this.request(path, { method: 'PUT', body });
|
|
74
|
+
put(path, body, options) {
|
|
75
|
+
return this.request(path, { ...options, method: 'PUT', body });
|
|
76
76
|
}
|
|
77
|
-
patch(path, body) {
|
|
78
|
-
return this.request(path, { method: 'PATCH', body });
|
|
77
|
+
patch(path, body, options) {
|
|
78
|
+
return this.request(path, { ...options, method: 'PATCH', body });
|
|
79
79
|
}
|
|
80
80
|
delete(path) {
|
|
81
81
|
return this.request(path, { method: 'DELETE' });
|
|
@@ -85,6 +85,26 @@ export class PimiaClient {
|
|
|
85
85
|
* `/invoices` y `/api/v1/invoices` son lo mismo.
|
|
86
86
|
*/
|
|
87
87
|
async request(path, options = {}) {
|
|
88
|
+
const { data } = await this.requestWithMeta(path, options);
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Lo mismo que {@link request}, pero devuelve también los metadatos de la
|
|
93
|
+
* respuesta.
|
|
94
|
+
*
|
|
95
|
+
* Existe por la idempotencia: tras un reintento, el cuerpo es idéntico al de
|
|
96
|
+
* la primera llamada —ese es justo el contrato—, así que el cuerpo solo no
|
|
97
|
+
* dice si Pimia escribió o se limitó a repetirse. `meta.idempotentReplay` sí.
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* const clave = crypto.randomUUID()
|
|
101
|
+
* const { data, meta } = await client.requestWithMeta('/estimates', {
|
|
102
|
+
* method: 'POST', body, idempotencyKey: clave,
|
|
103
|
+
* })
|
|
104
|
+
* if (meta.idempotentReplay) log('el presupuesto ya existía; no se duplicó')
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
async requestWithMeta(path, options = {}) {
|
|
88
108
|
let tokens = await this.currentTokens();
|
|
89
109
|
if (isExpired(tokens, this.skew)) {
|
|
90
110
|
tokens = await this.refreshTokens(tokens);
|
|
@@ -99,6 +119,12 @@ export class PimiaClient {
|
|
|
99
119
|
...(options.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
100
120
|
...this.extraHeaders,
|
|
101
121
|
...options.headers,
|
|
122
|
+
// Después de `options.headers` para que la opción con nombre mande
|
|
123
|
+
// sobre una cabecera puesta a mano: si alguien usa las dos, la
|
|
124
|
+
// explícita del API es la que quiso de verdad.
|
|
125
|
+
...(options.idempotencyKey === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { 'idempotency-key': options.idempotencyKey }),
|
|
102
128
|
authorization: `Bearer ${tokens.accessToken}`,
|
|
103
129
|
},
|
|
104
130
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
@@ -106,7 +132,17 @@ export class PimiaClient {
|
|
|
106
132
|
});
|
|
107
133
|
this.captureRateLimit(response);
|
|
108
134
|
if (response.ok) {
|
|
109
|
-
return
|
|
135
|
+
return {
|
|
136
|
+
data: (await parseBody(response)),
|
|
137
|
+
meta: {
|
|
138
|
+
status: response.status,
|
|
139
|
+
// Presente solo cuando Pimia reproduce; su ausencia significa
|
|
140
|
+
// «esta escritura ocurrió de verdad».
|
|
141
|
+
idempotentReplay: response.headers.get('idempotency-replayed') === 'true',
|
|
142
|
+
requestId: response.headers.get('x-request-id') ?? undefined,
|
|
143
|
+
rateLimit: this.lastRateLimit,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
110
146
|
}
|
|
111
147
|
const body = await parseBody(response);
|
|
112
148
|
const requestId = response.headers.get('x-request-id') ?? undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* salen los tipos de `./api`.
|
|
7
7
|
*/
|
|
8
8
|
export { PimiaClient } from './client.js';
|
|
9
|
-
export type { PimiaClientOptions, RateLimit, RequestOptions } from './client.js';
|
|
9
|
+
export type { PimiaClientOptions, RateLimit, RequestOptions, 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';
|
|
12
12
|
export { MemoryTokenStore, isExpired, tokenSetFromResponse } from './tokens.js';
|
|
@@ -33,5 +33,12 @@ export declare const SCOPES: {
|
|
|
33
33
|
readonly agendaRead: "agenda:read";
|
|
34
34
|
readonly agendaWrite: "agenda:write";
|
|
35
35
|
readonly reportsRead: "reports:read";
|
|
36
|
+
/**
|
|
37
|
+
* Proponer cambios que el dueño del tenant aprueba antes de aplicarse.
|
|
38
|
+
* `approvalsSubmit` es un alias del mismo permiso, aceptado por el
|
|
39
|
+
* Authorization Server.
|
|
40
|
+
*/
|
|
41
|
+
readonly approvalsWrite: "approvals:write";
|
|
42
|
+
readonly approvalsSubmit: "approvals:submit";
|
|
36
43
|
};
|
|
37
44
|
export type Scope = (typeof SCOPES)[keyof typeof SCOPES];
|
package/dist/index.js
CHANGED
|
@@ -30,4 +30,11 @@ export const SCOPES = {
|
|
|
30
30
|
agendaRead: 'agenda:read',
|
|
31
31
|
agendaWrite: 'agenda:write',
|
|
32
32
|
reportsRead: 'reports:read',
|
|
33
|
+
/**
|
|
34
|
+
* Proponer cambios que el dueño del tenant aprueba antes de aplicarse.
|
|
35
|
+
* `approvalsSubmit` es un alias del mismo permiso, aceptado por el
|
|
36
|
+
* Authorization Server.
|
|
37
|
+
*/
|
|
38
|
+
approvalsWrite: 'approvals:write',
|
|
39
|
+
approvalsSubmit: 'approvals:submit',
|
|
33
40
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pimia/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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)",
|