@pokemontcgapi/sdk 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 +99 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +247 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +81 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +116 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +202 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +259 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pokemontcgapi.com
|
|
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
|
+
# @pokemontcgapi/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript client for the Pokémon TCG API at [pokemontcgapi.com](https://pokemontcgapi.com) —
|
|
4
|
+
**615 sets and 52,337 cards** across three print regions (379 Japanese, 176 international, 60
|
|
5
|
+
Simplified Chinese), card names in six languages, 399 illustrators, images, and prices that state
|
|
6
|
+
their source, basis, grade and sample size.
|
|
7
|
+
|
|
8
|
+
**Zero runtime dependencies.** Uses the global `fetch`, so it runs unchanged on Node ≥ 20, Bun, Deno,
|
|
9
|
+
Cloudflare Workers and in the browser.
|
|
10
|
+
|
|
11
|
+
Unofficial. Not produced, endorsed, supported by or affiliated with Nintendo, Creatures Inc.,
|
|
12
|
+
GAME FREAK inc. or The Pokémon Company International. Pokémon and all related marks are trademarks of
|
|
13
|
+
their respective owners.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @pokemontcgapi/sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { PokemonTcgApi } from '@pokemontcgapi/sdk';
|
|
25
|
+
|
|
26
|
+
const client = new PokemonTcgApi({ apiKey: process.env.PTCG_API_KEY });
|
|
27
|
+
|
|
28
|
+
const card = await client.cards.get('base1-4', { include: ['prices'] });
|
|
29
|
+
console.log(card.id, card.name, card.index_eur);
|
|
30
|
+
// bs-4 Charizard 561.84
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`base1-4` and `bs-4` both resolve: the id is the printed coordinate — set code, dash, collector
|
|
34
|
+
number — and the alternate legacy id resolves on the same route, so a catalogue you already have does
|
|
35
|
+
not start with a matching problem.
|
|
36
|
+
|
|
37
|
+
### Pagination that you never have to think about
|
|
38
|
+
|
|
39
|
+
Every list method returns a `Page`, which is also an `AsyncIterable`. Iterating it follows
|
|
40
|
+
`links.next` for you:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
for await (const set of await client.sets.list({ region: 'JP' })) {
|
|
44
|
+
console.log(set.code, set.name, set.release_date);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The cursor carries a signature of the sort order, so it must never be reconstructed by hand — the SDK
|
|
49
|
+
follows the URL the API returned, which is the failure mode this avoids. `.toArray({ max })` requires
|
|
50
|
+
an explicit ceiling, because the catalogue is large enough that an unbounded materialisation is a
|
|
51
|
+
mistake rather than a choice.
|
|
52
|
+
|
|
53
|
+
### One call for a hundred cards
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const { data, requested, found } = await client.cards.batch(['bs-4', 'sv3-001'], {
|
|
57
|
+
select: ['id', 'name', 'index_eur'],
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Ids that do not exist are omitted rather than reported one by one — compare `requested` with `found`.
|
|
62
|
+
|
|
63
|
+
### Japanese, and the other five locales
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const page = await client.sets.cards('sv8', { lang: 'ja', limit: 1 });
|
|
67
|
+
console.log(page.data[0]?.name); // タマタマ
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`lang` replaces the `name` field itself and falls back to English where a translation is missing.
|
|
71
|
+
Locales: `en`, `ja`, `fr`, `de`, `es`, `it`.
|
|
72
|
+
|
|
73
|
+
### Conditional requests are free
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const client = new PokemonTcgApi({ cache: 'etag' });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Every collection carries a strong ETag. With the cache on, the client stores it and replays a `304`
|
|
80
|
+
without a body — no quota consumed. A mirror that re-syncs often pays only for what changed.
|
|
81
|
+
|
|
82
|
+
### A photo instead of an id
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const { data } = await client.vision.identify(file, { set: 'sv3' });
|
|
86
|
+
|
|
87
|
+
// Read `decision` before `id`. Always.
|
|
88
|
+
switch (data.decision) {
|
|
89
|
+
case 'match':
|
|
90
|
+
// One candidate, close, and clear of the next.
|
|
91
|
+
add(data.id!);
|
|
92
|
+
break;
|
|
93
|
+
case 'ambiguous':
|
|
94
|
+
// Two printings share this illustration. `data.id` is null on purpose.
|
|
95
|
+
showPicker(data.candidates);
|
|
96
|
+
break;
|
|
97
|
+
case 'no_match':
|
|
98
|
+
askForABetterPhoto();
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Reprints and regional twins share their artwork, so artwork alone cannot name a printing — not here
|
|
103
|
+
and not anywhere. The endpoint returns candidates with a `distance` (0–512, lower is closer; real
|
|
104
|
+
matches land well under 150) and refuses to pick when two are within a few bits of each other.
|
|
105
|
+
Passing `set` or `region` when your workflow knows them is what resolves the tie.
|
|
106
|
+
|
|
107
|
+
It costs 25 credits a call against 1 for a lookup: it is the whole image index answering, not a row
|
|
108
|
+
being read. Do not put it in a loop.
|
|
109
|
+
|
|
110
|
+
### Errors you can branch on
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { NotFoundError, RateLimitedError, QuotaExceededError } from '@pokemontcgapi/sdk';
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await client.cards.get('nope-1');
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error instanceof NotFoundError) { /* ... */ }
|
|
119
|
+
if (error instanceof RateLimitedError) { /* error.retryAfter */ }
|
|
120
|
+
if (error instanceof QuotaExceededError) { /* retrying will never help */ }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Every error carries `code`, `status`, `details` and `requestId` — quote the request id in a support
|
|
125
|
+
message, it is the only thing that can be looked up. Retries use exponential backoff with full
|
|
126
|
+
jitter on 429, 5xx and network failures, honour `Retry-After`, and never retry a quota exhaustion.
|
|
127
|
+
|
|
128
|
+
## What this API does not have
|
|
129
|
+
|
|
130
|
+
Stated up front so you find out here rather than three days into an integration:
|
|
131
|
+
|
|
132
|
+
- **No Korean cards.** Zero `KR` sets, zero `ko` translations. Both are modelled in the schema and
|
|
133
|
+
carry no data.
|
|
134
|
+
- **No card game text.** `attacks`, `abilities`, `weaknesses`, `resistances`, `subtypes`,
|
|
135
|
+
`retreat_cost`, `rules`, `flavor_text` and `legalities` are empty for every card; `types` and
|
|
136
|
+
`national_pokedex_numbers` are populated only on part of the Scarlet & Violet era. The types in
|
|
137
|
+
this package say so on each field. If you are building a deck checker or a rules engine, this is
|
|
138
|
+
not the data source you need.
|
|
139
|
+
|
|
140
|
+
What it does have: the printing itself — set, number, rarity, region, release date, illustrator,
|
|
141
|
+
image, marketplace ids, six-language names — and prices.
|
|
142
|
+
|
|
143
|
+
## Prices
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const card = await client.cards.get('base1-4', { include: ['prices'] });
|
|
147
|
+
for (const price of card.prices ?? []) {
|
|
148
|
+
console.log(price.source, price.basis, price.price, price.currency, price.as_of, price.sample_n);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
There is no printing filter: first edition, holofoil and graded rows come back together, so read
|
|
153
|
+
`printing`, `condition` and `grading` per row. `basis` separates `GUIDE` (published upstream) from
|
|
154
|
+
`DERIVED` (computed by us). `PTCG_INDEX` is a composite index in EUR carrying `sample_n`, and it is
|
|
155
|
+
also on every card row as `index_eur`, so a list already has a comparable number without a second
|
|
156
|
+
request per card.
|
|
157
|
+
|
|
158
|
+
## Also available
|
|
159
|
+
|
|
160
|
+
- **MCP server** for agents: [`@pokemontcgapi/mcp`](https://www.npmjs.com/package/@pokemontcgapi/mcp) — [source](https://github.com/pokemontcgapi/mcp-server)
|
|
161
|
+
- **Docs**: <https://pokemontcgapi.com/docs>
|
|
162
|
+
- **Coverage, measured live**: <https://pokemontcgapi.com/coverage>
|
|
163
|
+
|
|
164
|
+
## Build from source
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm ci
|
|
168
|
+
npm run typecheck
|
|
169
|
+
npm run build
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Node >= 20. No test suite lives here yet: what CI enforces is that the package
|
|
173
|
+
typechecks and builds on both Node 20 and Node 22, and that `npm pack` produces
|
|
174
|
+
the file list the registry is meant to receive.
|
|
175
|
+
|
|
176
|
+
This package is developed inside the private monorepo that runs
|
|
177
|
+
[pokemontcgapi.com](https://pokemontcgapi.com) and mirrored here on each release,
|
|
178
|
+
so a merged pull request travels back by hand rather than by merge button. That
|
|
179
|
+
is not a reason to send patches elsewhere — open the issue or the PR here, it is
|
|
180
|
+
the address that gets read.
|
|
181
|
+
|
|
182
|
+
## Licence
|
|
183
|
+
|
|
184
|
+
MIT. Data served by the API carries per-source redistribution terms — see
|
|
185
|
+
<https://pokemontcgapi.com/legal/attribution>.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Collection } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Il trasporto.
|
|
4
|
+
*
|
|
5
|
+
* Nessuna dipendenza a runtime: `fetch` globale, che c'e' su Node 20+, Bun,
|
|
6
|
+
* Deno, i Workers e i browser. Un client HTTP portato dentro il pacchetto
|
|
7
|
+
* costerebbe piu' della funzione che sostituisce, e ogni sua CVE diventerebbe
|
|
8
|
+
* nostra.
|
|
9
|
+
*/
|
|
10
|
+
export interface ClientOptions {
|
|
11
|
+
/** Se assente si legge `PTCG_API_KEY` dall'ambiente, dove esiste. */
|
|
12
|
+
readonly apiKey?: string;
|
|
13
|
+
readonly baseUrl?: string;
|
|
14
|
+
/** Millisecondi per singolo tentativo, non per l'operazione intera. */
|
|
15
|
+
readonly timeout?: number;
|
|
16
|
+
/** Tentativi RIPETUTI, oltre al primo. 0 disattiva. */
|
|
17
|
+
readonly maxRetries?: number;
|
|
18
|
+
/** Iniettabile per i test e per gli ambienti che avvolgono fetch. */
|
|
19
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
20
|
+
/**
|
|
21
|
+
* Ricorda gli ETag e rispedisce `If-None-Match`.
|
|
22
|
+
*
|
|
23
|
+
* Vale la pena accenderlo: un 304 non ha corpo e non consuma quota, quindi
|
|
24
|
+
* un mirror che risincronizza spesso paga solo le pagine cambiate. La cache
|
|
25
|
+
* e' in memoria e per-istanza: non sopravvive al processo, di proposito —
|
|
26
|
+
* una cache su disco dentro un SDK e' una sorgente di bug che il chiamante
|
|
27
|
+
* non puo' ispezionare.
|
|
28
|
+
*/
|
|
29
|
+
readonly cache?: 'none' | 'etag';
|
|
30
|
+
readonly userAgent?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function serializeParams(params: Record<string, unknown> | undefined): URLSearchParams;
|
|
33
|
+
export declare class HttpClient {
|
|
34
|
+
readonly baseUrl: string;
|
|
35
|
+
private readonly apiKey;
|
|
36
|
+
private readonly timeout;
|
|
37
|
+
private readonly maxRetries;
|
|
38
|
+
private readonly doFetch;
|
|
39
|
+
private readonly userAgent;
|
|
40
|
+
private readonly etags;
|
|
41
|
+
constructor(options?: ClientOptions);
|
|
42
|
+
/** URL assoluto da un path applicativo piu' i parametri. */
|
|
43
|
+
url(path: string, params?: Record<string, unknown>): string;
|
|
44
|
+
get<T>(path: string, params?: Record<string, unknown>): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* POST con un corpo.
|
|
47
|
+
*
|
|
48
|
+
* Non passa dalla cache ETag e non viene mai ritentato su un errore di rete:
|
|
49
|
+
* il ritentativo automatico e' sicuro solo su richieste idempotenti, e una
|
|
50
|
+
* POST che potrebbe essere arrivata a destinazione non lo e'. Un 429 con
|
|
51
|
+
* `Retry-After` resta ritentabile perche' li' sappiamo che non e' stata
|
|
52
|
+
* eseguita.
|
|
53
|
+
*/
|
|
54
|
+
post<T>(path: string, body: BodyInit, contentType?: string, params?: Record<string, unknown>): Promise<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Segue un URL gia' costruito dall'API (`links.next`).
|
|
57
|
+
*
|
|
58
|
+
* Esiste come metodo pubblico perche' il cursore porta la firma
|
|
59
|
+
* dell'ordinamento: ricostruire l'URL a mano e rimetterci dentro il cursore
|
|
60
|
+
* e' esattamente cio' che l'API rifiuta con `INVALID_CURSOR`.
|
|
61
|
+
*/
|
|
62
|
+
follow<T>(absoluteUrl: string): Promise<T>;
|
|
63
|
+
private request;
|
|
64
|
+
private attempt;
|
|
65
|
+
/**
|
|
66
|
+
* Un 5xx puo' arrivare da un proxy davanti all'API, quindi in HTML: se il
|
|
67
|
+
* corpo non e' il nostro envelope si costruisce un errore comunque, invece di
|
|
68
|
+
* far esplodere il parser e nascondere lo status vero.
|
|
69
|
+
*/
|
|
70
|
+
private readErrorBody;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Una pagina che e' anche un iteratore.
|
|
74
|
+
*
|
|
75
|
+
* `for await (const card of client.cards.search(...))` cammina l'intera
|
|
76
|
+
* collezione seguendo `links.next`, senza che il chiamante veda mai un cursore.
|
|
77
|
+
* E' il motivo per cui vale la pena usare l'SDK invece di `fetch`: la paginazione
|
|
78
|
+
* a cursore e' corretta ma noiosa, ed e' il punto in cui le integrazioni scritte
|
|
79
|
+
* a mano perdono righe.
|
|
80
|
+
*/
|
|
81
|
+
export declare class Page<T> implements AsyncIterable<T> {
|
|
82
|
+
readonly data: readonly T[];
|
|
83
|
+
readonly meta: Collection<T>['meta'];
|
|
84
|
+
private readonly nextUrl;
|
|
85
|
+
private readonly http;
|
|
86
|
+
constructor(http: HttpClient, body: Collection<T>);
|
|
87
|
+
get hasMore(): boolean;
|
|
88
|
+
nextPage(): Promise<Page<T> | null>;
|
|
89
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
90
|
+
/**
|
|
91
|
+
* Materializza in un array. `max` e' OBBLIGATORIO: il catalogo ha oltre
|
|
92
|
+
* 52.000 carte, e un `.toArray()` senza tetto e' il modo piu' rapido di
|
|
93
|
+
* riempire la memoria di un processo per sbaglio.
|
|
94
|
+
*/
|
|
95
|
+
toArray({ max }: {
|
|
96
|
+
max: number;
|
|
97
|
+
}): Promise<T[]>;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;GAOG;AAEH,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAuCD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,eAAe,CAgB5F;AAED,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsD;gBAEhE,OAAO,GAAE,aAAkB;IAUvC,4DAA4D;IAC5D,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IAKrD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxE;;;;;;;;OAQG;IACG,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAI/G;;;;;;OAMG;IACG,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;YAIlC,OAAO;YAiBP,OAAO;IA0DrB;;;;OAIG;YACW,aAAa;CAY5B;AAED;;;;;;;;GAQG;AACH,qBAAa,IAAI,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;gBAEtB,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;IAOjD,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAMlC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC;IAQjD;;;;OAIG;IACG,OAAO,CAAC,EAAE,GAAG,EAAE,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;CAQtD"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { ApiConnectionError, ApiTimeoutError, PokemonTcgApiError, QuotaExceededError, RateLimitedError, ServerError, toApiError, } from './errors.js';
|
|
2
|
+
const DEFAULT_BASE_URL = 'https://api.pokemontcgapi.com';
|
|
3
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
4
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
5
|
+
/** Errore di rete o 5xx/429: e' l'unico insieme su cui riprovare ha senso. */
|
|
6
|
+
function isRetryable(error) {
|
|
7
|
+
if (error instanceof QuotaExceededError)
|
|
8
|
+
return false;
|
|
9
|
+
if (error instanceof RateLimitedError)
|
|
10
|
+
return true;
|
|
11
|
+
if (error instanceof ServerError)
|
|
12
|
+
return true;
|
|
13
|
+
if (error instanceof ApiConnectionError)
|
|
14
|
+
return true;
|
|
15
|
+
if (error instanceof PokemonTcgApiError)
|
|
16
|
+
return error.status === 408;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Backoff esponenziale con jitter pieno.
|
|
21
|
+
*
|
|
22
|
+
* Il jitter non e' un dettaglio: senza, mille client che prendono lo stesso 429
|
|
23
|
+
* riprovano tutti nello stesso millisecondo e ricostruiscono la coda che
|
|
24
|
+
* stavano cercando di far smaltire.
|
|
25
|
+
*/
|
|
26
|
+
function backoffMs(attempt, retryAfter) {
|
|
27
|
+
if (retryAfter !== undefined)
|
|
28
|
+
return Math.min(retryAfter * 1000, 60_000);
|
|
29
|
+
const ceiling = Math.min(500 * 2 ** attempt, 8_000);
|
|
30
|
+
return Math.random() * ceiling;
|
|
31
|
+
}
|
|
32
|
+
function sleep(ms) {
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
function readEnv(name) {
|
|
36
|
+
// `process` non esiste nei browser ne' nei Workers: si guarda senza assumerlo.
|
|
37
|
+
const proc = globalThis.process;
|
|
38
|
+
return proc?.env?.[name];
|
|
39
|
+
}
|
|
40
|
+
export function serializeParams(params) {
|
|
41
|
+
const search = new URLSearchParams();
|
|
42
|
+
if (!params)
|
|
43
|
+
return search;
|
|
44
|
+
for (const [key, value] of Object.entries(params)) {
|
|
45
|
+
if (value === undefined || value === null)
|
|
46
|
+
continue;
|
|
47
|
+
// Gli array (select, include, ids) viaggiano come lista separata da virgole,
|
|
48
|
+
// che e' la forma che l'API accetta — non come chiavi ripetute.
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
if (value.length === 0)
|
|
51
|
+
continue;
|
|
52
|
+
search.set(key, value.join(','));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
search.set(key, String(value));
|
|
56
|
+
}
|
|
57
|
+
return search;
|
|
58
|
+
}
|
|
59
|
+
export class HttpClient {
|
|
60
|
+
baseUrl;
|
|
61
|
+
apiKey;
|
|
62
|
+
timeout;
|
|
63
|
+
maxRetries;
|
|
64
|
+
doFetch;
|
|
65
|
+
userAgent;
|
|
66
|
+
etags;
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
69
|
+
this.apiKey = options.apiKey ?? readEnv('PTCG_API_KEY');
|
|
70
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
71
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
72
|
+
this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
73
|
+
this.userAgent = options.userAgent ?? '@pokemontcgapi/sdk';
|
|
74
|
+
this.etags = options.cache === 'etag' ? new Map() : null;
|
|
75
|
+
}
|
|
76
|
+
/** URL assoluto da un path applicativo piu' i parametri. */
|
|
77
|
+
url(path, params) {
|
|
78
|
+
const search = serializeParams(params).toString();
|
|
79
|
+
return `${this.baseUrl}${path}${search === '' ? '' : `?${search}`}`;
|
|
80
|
+
}
|
|
81
|
+
async get(path, params) {
|
|
82
|
+
return this.request(this.url(path, params));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* POST con un corpo.
|
|
86
|
+
*
|
|
87
|
+
* Non passa dalla cache ETag e non viene mai ritentato su un errore di rete:
|
|
88
|
+
* il ritentativo automatico e' sicuro solo su richieste idempotenti, e una
|
|
89
|
+
* POST che potrebbe essere arrivata a destinazione non lo e'. Un 429 con
|
|
90
|
+
* `Retry-After` resta ritentabile perche' li' sappiamo che non e' stata
|
|
91
|
+
* eseguita.
|
|
92
|
+
*/
|
|
93
|
+
async post(path, body, contentType, params) {
|
|
94
|
+
return this.attempt(this.url(path, params), { method: 'POST', body, contentType });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Segue un URL gia' costruito dall'API (`links.next`).
|
|
98
|
+
*
|
|
99
|
+
* Esiste come metodo pubblico perche' il cursore porta la firma
|
|
100
|
+
* dell'ordinamento: ricostruire l'URL a mano e rimetterci dentro il cursore
|
|
101
|
+
* e' esattamente cio' che l'API rifiuta con `INVALID_CURSOR`.
|
|
102
|
+
*/
|
|
103
|
+
async follow(absoluteUrl) {
|
|
104
|
+
return this.request(absoluteUrl);
|
|
105
|
+
}
|
|
106
|
+
async request(url) {
|
|
107
|
+
let lastError;
|
|
108
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
109
|
+
try {
|
|
110
|
+
return await this.attempt(url);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
lastError = error;
|
|
114
|
+
if (attempt === this.maxRetries || !isRetryable(error))
|
|
115
|
+
throw error;
|
|
116
|
+
const retryAfter = error instanceof RateLimitedError ? error.retryAfter : undefined;
|
|
117
|
+
await sleep(backoffMs(attempt, retryAfter));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw lastError;
|
|
121
|
+
}
|
|
122
|
+
async attempt(url, write) {
|
|
123
|
+
const headers = {
|
|
124
|
+
accept: 'application/json',
|
|
125
|
+
'user-agent': this.userAgent,
|
|
126
|
+
};
|
|
127
|
+
if (this.apiKey !== undefined)
|
|
128
|
+
headers['x-api-key'] = this.apiKey;
|
|
129
|
+
// `contentType` si imposta solo se lo si conosce: su un FormData va lasciato
|
|
130
|
+
// scrivere a fetch, che ci mette dentro il `boundary`. Scriverlo a mano
|
|
131
|
+
// produce un multipart che il server non riesce a separare, e l'errore che
|
|
132
|
+
// ne esce parla di un campo mancante invece che di un'intestazione sbagliata.
|
|
133
|
+
if (write?.contentType !== undefined)
|
|
134
|
+
headers['content-type'] = write.contentType;
|
|
135
|
+
// Nessuna cache condizionale sulle scritture: l'ETag e' l'impronta di un
|
|
136
|
+
// corpo, e su una risposta che dipende da cio' che hai appena caricato non
|
|
137
|
+
// significherebbe niente.
|
|
138
|
+
const cached = write === undefined ? this.etags?.get(url) : undefined;
|
|
139
|
+
if (cached !== undefined)
|
|
140
|
+
headers['if-none-match'] = cached.etag;
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
143
|
+
let response;
|
|
144
|
+
try {
|
|
145
|
+
response = await this.doFetch(url, {
|
|
146
|
+
method: write?.method ?? 'GET',
|
|
147
|
+
headers,
|
|
148
|
+
...(write === undefined ? {} : { body: write.body }),
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (controller.signal.aborted)
|
|
154
|
+
throw new ApiTimeoutError(this.timeout, error);
|
|
155
|
+
throw new ApiConnectionError(`Request to ${url} failed`, error);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
// 304: il corpo e' vuoto per definizione, la risposta e' quella in cache.
|
|
161
|
+
if (response.status === 304 && cached !== undefined)
|
|
162
|
+
return cached.body;
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const retryAfterHeader = response.headers.get('retry-after');
|
|
165
|
+
const retryAfter = retryAfterHeader === null ? undefined : Number.parseInt(retryAfterHeader, 10);
|
|
166
|
+
const body = await this.readErrorBody(response);
|
|
167
|
+
throw toApiError(response.status, body, Number.isFinite(retryAfter) ? retryAfter : undefined);
|
|
168
|
+
}
|
|
169
|
+
const body = (await response.json());
|
|
170
|
+
const etag = response.headers.get('etag');
|
|
171
|
+
if (write === undefined && this.etags !== null && etag !== null)
|
|
172
|
+
this.etags.set(url, { etag, body });
|
|
173
|
+
return body;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Un 5xx puo' arrivare da un proxy davanti all'API, quindi in HTML: se il
|
|
177
|
+
* corpo non e' il nostro envelope si costruisce un errore comunque, invece di
|
|
178
|
+
* far esplodere il parser e nascondere lo status vero.
|
|
179
|
+
*/
|
|
180
|
+
async readErrorBody(response) {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = (await response.json());
|
|
183
|
+
if (parsed.error !== undefined && typeof parsed.error.code === 'string')
|
|
184
|
+
return parsed.error;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
/* cade sotto */
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
code: `HTTP_${response.status}`,
|
|
191
|
+
message: response.statusText === '' ? `HTTP ${response.status}` : response.statusText,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Una pagina che e' anche un iteratore.
|
|
197
|
+
*
|
|
198
|
+
* `for await (const card of client.cards.search(...))` cammina l'intera
|
|
199
|
+
* collezione seguendo `links.next`, senza che il chiamante veda mai un cursore.
|
|
200
|
+
* E' il motivo per cui vale la pena usare l'SDK invece di `fetch`: la paginazione
|
|
201
|
+
* a cursore e' corretta ma noiosa, ed e' il punto in cui le integrazioni scritte
|
|
202
|
+
* a mano perdono righe.
|
|
203
|
+
*/
|
|
204
|
+
export class Page {
|
|
205
|
+
data;
|
|
206
|
+
meta;
|
|
207
|
+
nextUrl;
|
|
208
|
+
http;
|
|
209
|
+
constructor(http, body) {
|
|
210
|
+
this.http = http;
|
|
211
|
+
this.data = body.data;
|
|
212
|
+
this.meta = body.meta;
|
|
213
|
+
this.nextUrl = body.links?.next;
|
|
214
|
+
}
|
|
215
|
+
get hasMore() {
|
|
216
|
+
return this.nextUrl !== undefined;
|
|
217
|
+
}
|
|
218
|
+
async nextPage() {
|
|
219
|
+
if (this.nextUrl === undefined)
|
|
220
|
+
return null;
|
|
221
|
+
const body = await this.http.follow(this.nextUrl);
|
|
222
|
+
return new Page(this.http, body);
|
|
223
|
+
}
|
|
224
|
+
async *[Symbol.asyncIterator]() {
|
|
225
|
+
let page = this;
|
|
226
|
+
while (page !== null) {
|
|
227
|
+
for (const item of page.data)
|
|
228
|
+
yield item;
|
|
229
|
+
page = await page.nextPage();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Materializza in un array. `max` e' OBBLIGATORIO: il catalogo ha oltre
|
|
234
|
+
* 52.000 carte, e un `.toArray()` senza tetto e' il modo piu' rapido di
|
|
235
|
+
* riempire la memoria di un processo per sbaglio.
|
|
236
|
+
*/
|
|
237
|
+
async toArray({ max }) {
|
|
238
|
+
const out = [];
|
|
239
|
+
for await (const item of this) {
|
|
240
|
+
out.push(item);
|
|
241
|
+
if (out.length >= max)
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gerarchia degli errori.
|
|
3
|
+
*
|
|
4
|
+
* Sottoclassi e non un solo tipo con un campo `code`, per una ragione pratica:
|
|
5
|
+
* chi integra scrive `catch (e) { if (e instanceof RateLimited) ... }`, e con un
|
|
6
|
+
* tipo solo dovrebbe confrontare stringhe — cioe' riscrivere a mano la
|
|
7
|
+
* tassonomia che noi gia' conosciamo, sbagliando i nomi.
|
|
8
|
+
*
|
|
9
|
+
* `QuotaExceeded` e' separato da `RateLimited` di proposito: sembrano lo stesso
|
|
10
|
+
* errore (entrambi 429) ma si trattano in modo opposto. Un rate limit passa
|
|
11
|
+
* aspettando; una quota mensile finita non passa mai, e riprovare e' solo un
|
|
12
|
+
* modo di consumare tempo. Il retry automatico di questo client riprova il
|
|
13
|
+
* primo e non riprova mai il secondo.
|
|
14
|
+
*/
|
|
15
|
+
export interface ApiErrorBody {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly details?: Record<string, unknown>;
|
|
19
|
+
readonly request_id?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class PokemonTcgApiError extends Error {
|
|
22
|
+
/** Codice stabile della tassonomia, es. `CARD_NOT_FOUND`. */
|
|
23
|
+
readonly code: string;
|
|
24
|
+
readonly status: number;
|
|
25
|
+
/**
|
|
26
|
+
* Sempre valorizzato quando la risposta e' passata dall'API: e' l'unica cosa
|
|
27
|
+
* che il supporto puo' cercare nei log. Va incluso in ogni bug report.
|
|
28
|
+
*/
|
|
29
|
+
readonly requestId: string | undefined;
|
|
30
|
+
readonly details: Record<string, unknown> | undefined;
|
|
31
|
+
constructor(status: number, body: ApiErrorBody);
|
|
32
|
+
}
|
|
33
|
+
/** 401 — chiave assente, malformata o revocata. */
|
|
34
|
+
export declare class AuthenticationError extends PokemonTcgApiError {
|
|
35
|
+
}
|
|
36
|
+
/** 403 — la chiave e' valida ma non puo' fare questa cosa. */
|
|
37
|
+
export declare class PermissionDeniedError extends PokemonTcgApiError {
|
|
38
|
+
}
|
|
39
|
+
/** 402 / UPGRADE_REQUIRED — serve un piano superiore. */
|
|
40
|
+
export declare class UpgradeRequiredError extends PokemonTcgApiError {
|
|
41
|
+
/** Finestra concessa dal piano corrente, quando l'API la dichiara. */
|
|
42
|
+
get permittedWindow(): unknown;
|
|
43
|
+
}
|
|
44
|
+
/** 404 — la risorsa non esiste. Non e' un errore di rete: non si riprova. */
|
|
45
|
+
export declare class NotFoundError extends PokemonTcgApiError {
|
|
46
|
+
}
|
|
47
|
+
/** 400 / 422 — la richiesta e' sbagliata. `field` dice quale parametro. */
|
|
48
|
+
export declare class InvalidRequestError extends PokemonTcgApiError {
|
|
49
|
+
get field(): string | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** 429 con Retry-After: passa aspettando. */
|
|
52
|
+
export declare class RateLimitedError extends PokemonTcgApiError {
|
|
53
|
+
/** Secondi da aspettare, dall'header `Retry-After`, se c'era. */
|
|
54
|
+
readonly retryAfter: number | undefined;
|
|
55
|
+
constructor(status: number, body: ApiErrorBody, retryAfter?: number);
|
|
56
|
+
}
|
|
57
|
+
/** 429 per quota di periodo esaurita: NON passa aspettando, e non si riprova. */
|
|
58
|
+
export declare class QuotaExceededError extends PokemonTcgApiError {
|
|
59
|
+
}
|
|
60
|
+
/** 5xx. */
|
|
61
|
+
export declare class ServerError extends PokemonTcgApiError {
|
|
62
|
+
}
|
|
63
|
+
/** La richiesta non e' mai arrivata: DNS, TLS, socket. */
|
|
64
|
+
export declare class ApiConnectionError extends Error {
|
|
65
|
+
/** `override` perche' Error dichiara gia' `cause` da ES2022. */
|
|
66
|
+
readonly cause: unknown;
|
|
67
|
+
constructor(message: string, cause: unknown);
|
|
68
|
+
}
|
|
69
|
+
/** La richiesta e' stata abbandonata da noi dopo `timeout`. */
|
|
70
|
+
export declare class ApiTimeoutError extends ApiConnectionError {
|
|
71
|
+
constructor(timeoutMs: number, cause: unknown);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Dal corpo dell'errore alla classe giusta.
|
|
75
|
+
*
|
|
76
|
+
* Si guarda PRIMA il `code` e poi lo status: lo status dice la famiglia, il
|
|
77
|
+
* code dice il caso, e i due casi che contano davvero (limite contro quota)
|
|
78
|
+
* condividono lo stesso status.
|
|
79
|
+
*/
|
|
80
|
+
export declare function toApiError(status: number, body: ApiErrorBody, retryAfter?: number): PokemonTcgApiError;
|
|
81
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;gBAE1C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY;CAQ/C;AAED,mDAAmD;AACnD,qBAAa,mBAAoB,SAAQ,kBAAkB;CAAG;AAE9D,8DAA8D;AAC9D,qBAAa,qBAAsB,SAAQ,kBAAkB;CAAG;AAEhE,yDAAyD;AACzD,qBAAa,oBAAqB,SAAQ,kBAAkB;IAC1D,sEAAsE;IACtE,IAAI,eAAe,IAAI,OAAO,CAE7B;CACF;AAED,6EAA6E;AAC7E,qBAAa,aAAc,SAAQ,kBAAkB;CAAG;AAExD,2EAA2E;AAC3E,qBAAa,mBAAoB,SAAQ,kBAAkB;IACzD,IAAI,KAAK,IAAI,MAAM,GAAG,SAAS,CAG9B;CACF;AAED,6CAA6C;AAC7C,qBAAa,gBAAiB,SAAQ,kBAAkB;IACtD,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;gBAE5B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,MAAM;CAIpE;AAED,iFAAiF;AACjF,qBAAa,kBAAmB,SAAQ,kBAAkB;CAAG;AAE7D,WAAW;AACX,qBAAa,WAAY,SAAQ,kBAAkB;CAAG;AAEtD,0DAA0D;AAC1D,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,gEAAgE;IAChE,SAAkB,KAAK,EAAE,OAAO,CAAC;gBACrB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAK5C;AAED,+DAA+D;AAC/D,qBAAa,eAAgB,SAAQ,kBAAkB;gBACzC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAI9C;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAatG"}
|