@carddex/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/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/index.cjs +444 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +583 -0
- package/dist/index.d.ts +583 -3
- package/dist/index.js +420 -1
- package/dist/index.js.map +1 -1
- package/package.json +48 -11
- package/dist/client.d.ts +0 -85
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -119
- package/dist/client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types.d.ts +0 -161
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michał Furgalski
|
|
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,155 @@
|
|
|
1
|
+
# @carddex/sdk
|
|
2
|
+
|
|
3
|
+
Official TypeScript/JavaScript client for the [CardDex](https://carddex.dev) Pokémon TCG API — cards, sets, print variants, sealed products, and price history from Cardmarket (EUR) and TCGplayer (USD).
|
|
4
|
+
|
|
5
|
+
- Zero runtime dependencies — uses global `fetch`, so it runs unmodified in Node 18+, browsers, Cloudflare Workers, Deno and Bun.
|
|
6
|
+
- Fully typed, ESM + CJS builds, with a shipped `.d.ts`.
|
|
7
|
+
- No codegen step for consumers: types are generated at build time from the CardDex OpenAPI spec and committed.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @carddex/sdk
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @carddex/sdk
|
|
15
|
+
# or
|
|
16
|
+
yarn add @carddex/sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { CardDex } from '@carddex/sdk';
|
|
23
|
+
|
|
24
|
+
const client = new CardDex({ apiKey: process.env.CARDDEX_API_KEY });
|
|
25
|
+
|
|
26
|
+
const { data: cards } = await client.cards.list({ name: 'Charizard', sort: 'release_date', order: 'desc' });
|
|
27
|
+
|
|
28
|
+
const card = await client.cards.get('sv06-001', { include: ['prices', 'images'] });
|
|
29
|
+
|
|
30
|
+
const prices = await client.cards.prices('sv06-001');
|
|
31
|
+
|
|
32
|
+
const history = await client.cards.priceHistory('sv06-001', { days: 180, source: ['cardmarket', 'tcgplayer'] });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
An API key is optional. Omit it to call the API anonymously. The SDK sends the key in the `X-API-Key` header.
|
|
36
|
+
|
|
37
|
+
You can also sign in with an email link at [carddex.dev/account](https://carddex.dev/account) to see your keys, create up to 2 active keys, and rotate or revoke them.
|
|
38
|
+
### Get a free API key
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { CardDex } from '@carddex/sdk';
|
|
42
|
+
|
|
43
|
+
const client = new CardDex();
|
|
44
|
+
const { api_key } = await client.auth.register('you@example.com', 'My App');
|
|
45
|
+
// Save api_key now — it is shown only once. If you lose it, sign in at https://carddex.dev/account to create a new one or rotate/revoke keys.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Calling from a browser
|
|
49
|
+
|
|
50
|
+
Public read endpoints accept requests from any origin. **Never ship a real API key in public frontend code** — anyone who opens dev tools can read it, and it would be shared by every visitor to your site. Instead:
|
|
51
|
+
|
|
52
|
+
- Call anonymously from the browser. CardDex allows this at a lower but real rate limit (30 req/min, 5,000 req/day per visitor IP) specifically so a frontend can call it directly, with no key:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const client = new CardDex(); // no apiKey — anonymous browser mode
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- Or proxy through your own backend and attach the key there, server-side.
|
|
59
|
+
|
|
60
|
+
If you construct `new CardDex({ apiKey })` in what looks like a browser environment, the SDK logs a one-time `console.warn` as a safety net — it does not block the request.
|
|
61
|
+
|
|
62
|
+
## Pagination
|
|
63
|
+
|
|
64
|
+
Every list endpoint returns `{ data, meta: { total, page, pageSize, totalPages } }`. Each resource that lists something also exposes an `*All` method that returns an async iterator, fetching pages lazily as you consume them:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
for await (const card of client.cards.listAll({ set: 'sv06' })) {
|
|
68
|
+
console.log(card.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for await (const product of client.sealed.listAll({ set_id: 'sv06' })) { ... }
|
|
72
|
+
|
|
73
|
+
for await (const card of client.sets.cardsAll('sv06')) { ... }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
You can also use the lower-level `paginate()` helper directly against any page-shaped fetcher:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { paginate } from '@carddex/sdk';
|
|
80
|
+
|
|
81
|
+
for await (const card of paginate((page) => client.cards.list({ set: 'sv06', page }))) { ... }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Errors
|
|
85
|
+
|
|
86
|
+
Every non-2xx response throws a `CardDexError`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { CardDex, CardDexError } from '@carddex/sdk';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await client.cards.get('does-not-exist');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof CardDexError) {
|
|
95
|
+
err.status; // HTTP status, e.g. 404
|
|
96
|
+
err.code; // the API's error.code (a number for a real API response; 'NETWORK_ERROR' for a transport failure)
|
|
97
|
+
err.message; // the API's error.message
|
|
98
|
+
err.retryAfter; // seconds to wait, parsed from Retry-After on a 429 — null otherwise
|
|
99
|
+
err.rateLimit; // { limit, dailyLimit, dailyRemaining } parsed from response headers, or null
|
|
100
|
+
err.isRateLimited; // true for a 429 (either the per-minute or the daily cap)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Rate limits & retries
|
|
106
|
+
|
|
107
|
+
Every response carries `X-RateLimit-Limit` (per minute), `X-RateLimit-Daily-Limit` and `X-RateLimit-Daily-Remaining` (per UTC day, approximate). A 429 carries `Retry-After` in seconds: `60` for the per-minute limit, or the seconds until 00:00 UTC for the daily one.
|
|
108
|
+
|
|
109
|
+
Automatic retry on 429 is **off by default** — the SDK never silently blocks your code. Turn it on when you want it:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const client = new CardDex({ retry: true }); // up to 2 retries, never waiting more than 60s
|
|
113
|
+
|
|
114
|
+
const client2 = new CardDex({ retry: { maxRetries: 3, maxWaitSeconds: 30 } });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A `Retry-After` longer than `maxWaitSeconds` is never honoured — the call fails immediately instead of sleeping. This is deliberate: the daily-quota 429's `Retry-After` can be most of a day, and a small SDK default should never block a request for hours. The per-minute 429's `Retry-After: 60` fits comfortably under the default 60s cap, so that case retries; a daily-cap 429 does not, by design.
|
|
118
|
+
|
|
119
|
+
## Migrating from pokemontcg.io
|
|
120
|
+
|
|
121
|
+
The pokemontcg.io v2 compatibility layer is live: an app built on pokemontcg.io v2 can keep its code and stored ids and change its base URL to `https://api.carddex.dev/compat/pokemontcg/v2`. The guide at [carddex.dev/migrate/pokemontcg](https://carddex.dev/migrate/pokemontcg) covers what the layer supports, which official pokemontcg.io SDKs work with it, and the fields that differ.
|
|
122
|
+
|
|
123
|
+
This SDK talks to the native `/v1` API, which uses CardDex ids (`sv02-062`) but accepts pokemontcg.io ids as well: `client.cards.get('sv2-62')` returns the same card. The API also returns a `ptcgio_id` field on every card and accepts a `ptcgio_id` filter on the card list; this SDK's types don't include either yet.
|
|
124
|
+
|
|
125
|
+
## API surface
|
|
126
|
+
|
|
127
|
+
| Resource | Methods |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `client.cards` | `list`, `listAll`, `get`, `random`, `prices`, `priceHistory` |
|
|
130
|
+
| `client.sets` | `list`, `get`, `cards`, `cardsAll`, `neighbors`, `sealed` |
|
|
131
|
+
| `client.sealed` | `list`, `listAll`, `get`, `prices` |
|
|
132
|
+
| `client.prices` | `bulk`, `top`, `trends`, `bargains`, `history` |
|
|
133
|
+
| `client.auth` | `register`, `usage` |
|
|
134
|
+
| `client` | `usage()` (shorthand for `client.auth.usage()`), `health()` |
|
|
135
|
+
|
|
136
|
+
Full parameter and response types are exported from the package root — see `src/params.ts` and `src/types.ts`, or your editor's autocomplete.
|
|
137
|
+
|
|
138
|
+
## Development (this monorepo)
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pnpm install
|
|
142
|
+
pnpm --filter @carddex/sdk generate # refresh src/generated/openapi.d.ts from the live OpenAPI spec
|
|
143
|
+
pnpm --filter @carddex/sdk build
|
|
144
|
+
pnpm --filter @carddex/sdk test
|
|
145
|
+
pnpm --filter @carddex/sdk typecheck
|
|
146
|
+
pnpm --filter @carddex/sdk lint
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`openapi.json` in this package's root is a committed snapshot of `https://api.carddex.dev/openapi.json`. `pnpm generate` refreshes it and regenerates `src/generated/openapi.d.ts` from it; if the fetch fails (offline, no network egress in CI), it regenerates from whatever snapshot is already committed instead of failing outright.
|
|
150
|
+
|
|
151
|
+
The OpenAPI spec now documents response body schemas too (`components.schemas`, validated against live API responses by `carddex-api`'s `tests/integration/openapi-contract.test.ts`), so `src/generated/openapi.d.ts` types response bodies precisely rather than as `unknown`. The hand-written interfaces in `src/types.ts` predate that and can stay — they're what this SDK's public API is built on — but new code can also reach into `components["schemas"]` from the generated file directly.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CardDex: () => CardDex,
|
|
24
|
+
CardDexError: () => CardDexError,
|
|
25
|
+
paginate: () => paginate
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/errors.ts
|
|
30
|
+
function parseRateLimitHeaders(headers) {
|
|
31
|
+
const num = (name) => {
|
|
32
|
+
const raw = headers.get(name);
|
|
33
|
+
if (raw == null) return null;
|
|
34
|
+
const n = Number(raw);
|
|
35
|
+
return Number.isFinite(n) ? n : null;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
limit: num("X-RateLimit-Limit"),
|
|
39
|
+
dailyLimit: num("X-RateLimit-Daily-Limit"),
|
|
40
|
+
dailyRemaining: num("X-RateLimit-Daily-Remaining")
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
var CardDexError = class _CardDexError extends Error {
|
|
44
|
+
/** HTTP status code. `0` for a request that never got a response (network failure, abort). */
|
|
45
|
+
status;
|
|
46
|
+
/**
|
|
47
|
+
* The API's own `error.code`. For a response the API actually produced,
|
|
48
|
+
* this is the same number as `status` (see the note on `ApiErrorBody` in
|
|
49
|
+
* `types.ts`) — kept as its own field because a transport-level failure
|
|
50
|
+
* (no response at all) sets it to a string like `NETWORK_ERROR` instead.
|
|
51
|
+
*/
|
|
52
|
+
code;
|
|
53
|
+
/** Seconds to wait before retrying, parsed from `Retry-After` on a 429. `null` otherwise. */
|
|
54
|
+
retryAfter;
|
|
55
|
+
/** Rate-limit headers on the response, when there was one. */
|
|
56
|
+
rateLimit;
|
|
57
|
+
constructor(message, options) {
|
|
58
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
59
|
+
this.name = "CardDexError";
|
|
60
|
+
this.status = options.status;
|
|
61
|
+
this.code = options.code;
|
|
62
|
+
this.retryAfter = options.retryAfter ?? null;
|
|
63
|
+
this.rateLimit = options.rateLimit ?? null;
|
|
64
|
+
Object.setPrototypeOf(this, _CardDexError.prototype);
|
|
65
|
+
}
|
|
66
|
+
/** Whether this is a 429 (either the per-minute or the daily cap). */
|
|
67
|
+
get isRateLimited() {
|
|
68
|
+
return this.status === 429;
|
|
69
|
+
}
|
|
70
|
+
static async fromResponse(response) {
|
|
71
|
+
const rateLimit = parseRateLimitHeaders(response.headers);
|
|
72
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
73
|
+
const retryAfter = retryAfterHeader != null ? Number(retryAfterHeader) : null;
|
|
74
|
+
let message = `CardDex API error (${response.status})`;
|
|
75
|
+
let code = response.status;
|
|
76
|
+
try {
|
|
77
|
+
const body = await response.clone().json();
|
|
78
|
+
if (body?.error) {
|
|
79
|
+
if (typeof body.error.message === "string") message = body.error.message;
|
|
80
|
+
if (typeof body.error.code === "number" || typeof body.error.code === "string") {
|
|
81
|
+
code = body.error.code;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
return new _CardDexError(message, {
|
|
87
|
+
status: response.status,
|
|
88
|
+
code,
|
|
89
|
+
retryAfter: Number.isFinite(retryAfter) ? retryAfter : null,
|
|
90
|
+
rateLimit
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
static network(cause) {
|
|
94
|
+
const message = cause instanceof Error ? cause.message : "Network request failed";
|
|
95
|
+
return new _CardDexError(message, { status: 0, code: "NETWORK_ERROR", cause });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/http.ts
|
|
100
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
101
|
+
var DEFAULT_MAX_WAIT_SECONDS = 60;
|
|
102
|
+
function buildQueryString(params) {
|
|
103
|
+
if (!params) return "";
|
|
104
|
+
const search = new URLSearchParams();
|
|
105
|
+
for (const [key, value] of Object.entries(params)) {
|
|
106
|
+
if (value === void 0 || value === null) continue;
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
if (value.length === 0) continue;
|
|
109
|
+
search.set(key, value.join(","));
|
|
110
|
+
} else {
|
|
111
|
+
search.set(key, String(value));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const qs = search.toString();
|
|
115
|
+
return qs ? `?${qs}` : "";
|
|
116
|
+
}
|
|
117
|
+
function sleep(ms) {
|
|
118
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
119
|
+
}
|
|
120
|
+
var HttpClient = class {
|
|
121
|
+
baseUrl;
|
|
122
|
+
apiKey;
|
|
123
|
+
fetchImpl;
|
|
124
|
+
retryConfig;
|
|
125
|
+
constructor(options) {
|
|
126
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
127
|
+
this.apiKey = options.apiKey;
|
|
128
|
+
this.fetchImpl = options.fetch;
|
|
129
|
+
if (options.retry === true) {
|
|
130
|
+
this.retryConfig = {
|
|
131
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
132
|
+
maxWaitSeconds: DEFAULT_MAX_WAIT_SECONDS
|
|
133
|
+
};
|
|
134
|
+
} else if (options.retry && typeof options.retry === "object") {
|
|
135
|
+
this.retryConfig = {
|
|
136
|
+
maxRetries: options.retry.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
137
|
+
maxWaitSeconds: options.retry.maxWaitSeconds ?? DEFAULT_MAX_WAIT_SECONDS
|
|
138
|
+
};
|
|
139
|
+
} else {
|
|
140
|
+
this.retryConfig = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async get(path, query, signal) {
|
|
144
|
+
return this.request("GET", path, { query, signal });
|
|
145
|
+
}
|
|
146
|
+
async post(path, body, signal) {
|
|
147
|
+
return this.request("POST", path, { body, signal });
|
|
148
|
+
}
|
|
149
|
+
async request(method, path, opts) {
|
|
150
|
+
const url = `${this.baseUrl}${path}${buildQueryString(opts.query)}`;
|
|
151
|
+
const headers = { Accept: "application/json" };
|
|
152
|
+
if (this.apiKey) headers["X-API-Key"] = this.apiKey;
|
|
153
|
+
if (opts.body !== void 0) headers["Content-Type"] = "application/json";
|
|
154
|
+
let attempt = 0;
|
|
155
|
+
for (; ; ) {
|
|
156
|
+
let response;
|
|
157
|
+
try {
|
|
158
|
+
response = await this.fetchImpl(url, {
|
|
159
|
+
method,
|
|
160
|
+
headers,
|
|
161
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
162
|
+
signal: opts.signal
|
|
163
|
+
});
|
|
164
|
+
} catch (cause) {
|
|
165
|
+
throw CardDexError.network(cause);
|
|
166
|
+
}
|
|
167
|
+
if (response.status === 429 && this.retryConfig && attempt < this.retryConfig.maxRetries) {
|
|
168
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
169
|
+
const retryAfterSeconds = retryAfterHeader != null ? Number(retryAfterHeader) : Number.NaN;
|
|
170
|
+
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds <= this.retryConfig.maxWaitSeconds) {
|
|
171
|
+
attempt++;
|
|
172
|
+
await sleep(retryAfterSeconds * 1e3);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
throw await CardDexError.fromResponse(response);
|
|
178
|
+
}
|
|
179
|
+
if (response.status === 204) return void 0;
|
|
180
|
+
return await response.json();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/resources/auth.ts
|
|
186
|
+
var AuthResource = class {
|
|
187
|
+
constructor(http) {
|
|
188
|
+
this.http = http;
|
|
189
|
+
}
|
|
190
|
+
http;
|
|
191
|
+
/**
|
|
192
|
+
* `POST /v1/auth/register` — claim a free API key. The key is returned
|
|
193
|
+
* exactly once, so store it immediately. A lost key can be replaced by signing
|
|
194
|
+
* in at https://carddex.dev/account (create, rotate or revoke keys there).
|
|
195
|
+
*/
|
|
196
|
+
async register(email, name) {
|
|
197
|
+
const res = await this.http.post("/v1/auth/register", {
|
|
198
|
+
email,
|
|
199
|
+
name
|
|
200
|
+
});
|
|
201
|
+
return res.data;
|
|
202
|
+
}
|
|
203
|
+
/** `GET /v1/auth/usage` — usage stats for the API key this client was constructed with. Requires an `apiKey`. */
|
|
204
|
+
async usage() {
|
|
205
|
+
const res = await this.http.get("/v1/auth/usage");
|
|
206
|
+
return res.data;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/pagination.ts
|
|
211
|
+
async function* paginate(fetchPage, startPage = 1) {
|
|
212
|
+
let page = startPage;
|
|
213
|
+
for (; ; ) {
|
|
214
|
+
const response = await fetchPage(page);
|
|
215
|
+
if (response.data.length === 0) return;
|
|
216
|
+
yield* response.data;
|
|
217
|
+
if (page >= response.meta.totalPages) return;
|
|
218
|
+
page++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/resources/cards.ts
|
|
223
|
+
var CardsResource = class {
|
|
224
|
+
constructor(http) {
|
|
225
|
+
this.http = http;
|
|
226
|
+
}
|
|
227
|
+
http;
|
|
228
|
+
/** `GET /v1/cards` — list, search, filter and sort cards. */
|
|
229
|
+
list(params) {
|
|
230
|
+
return this.http.get("/v1/cards", params);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Iterates every card matching `params` across all pages, fetching pages
|
|
234
|
+
* lazily as you consume them.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* for await (const card of client.cards.listAll({ set: 'sv06' })) { ... }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
listAll(params) {
|
|
242
|
+
return paginate((page) => this.list({ ...params, page }));
|
|
243
|
+
}
|
|
244
|
+
/** `GET /v1/cards/random` */
|
|
245
|
+
async random() {
|
|
246
|
+
const res = await this.http.get("/v1/cards/random");
|
|
247
|
+
return res.data;
|
|
248
|
+
}
|
|
249
|
+
/** `GET /v1/cards/{id}` */
|
|
250
|
+
async get(id, params) {
|
|
251
|
+
const res = await this.http.get(
|
|
252
|
+
`/v1/cards/${encodeURIComponent(id)}`,
|
|
253
|
+
params
|
|
254
|
+
);
|
|
255
|
+
return res.data;
|
|
256
|
+
}
|
|
257
|
+
/** `GET /v1/cards/{id}/prices` — the latest price row(s) for a card. */
|
|
258
|
+
async prices(id, params) {
|
|
259
|
+
const res = await this.http.get(
|
|
260
|
+
`/v1/cards/${encodeURIComponent(id)}/prices`,
|
|
261
|
+
params
|
|
262
|
+
);
|
|
263
|
+
return res.data;
|
|
264
|
+
}
|
|
265
|
+
/** `GET /v1/cards/{id}/prices/history` — one row per day per price guide, oldest first. */
|
|
266
|
+
async priceHistory(id, params) {
|
|
267
|
+
const res = await this.http.get(
|
|
268
|
+
`/v1/cards/${encodeURIComponent(id)}/prices/history`,
|
|
269
|
+
params
|
|
270
|
+
);
|
|
271
|
+
return res.data;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/resources/prices.ts
|
|
276
|
+
var PricesResource = class {
|
|
277
|
+
constructor(http) {
|
|
278
|
+
this.http = http;
|
|
279
|
+
}
|
|
280
|
+
http;
|
|
281
|
+
/**
|
|
282
|
+
* `GET /v1/prices` — latest price rows for up to 100 card ids (and/or up
|
|
283
|
+
* to 100 print/variant ids) in one request. Keyed by the id you asked for;
|
|
284
|
+
* an id with no price data maps to `[]`.
|
|
285
|
+
*/
|
|
286
|
+
async bulk(params) {
|
|
287
|
+
const res = await this.http.get(
|
|
288
|
+
"/v1/prices",
|
|
289
|
+
params
|
|
290
|
+
);
|
|
291
|
+
return res.data;
|
|
292
|
+
}
|
|
293
|
+
/** `GET /v1/prices/top` — highest-valued cards. */
|
|
294
|
+
async top(params) {
|
|
295
|
+
const res = await this.http.get("/v1/prices/top", params);
|
|
296
|
+
return res.data;
|
|
297
|
+
}
|
|
298
|
+
/** `GET /v1/prices/trends` — biggest movers against the card's own rolling average. */
|
|
299
|
+
async trends(params) {
|
|
300
|
+
const res = await this.http.get("/v1/prices/trends", params);
|
|
301
|
+
return res.data;
|
|
302
|
+
}
|
|
303
|
+
/** `GET /v1/prices/bargains` — cards trading below a reference price, best discount first. */
|
|
304
|
+
async bargains(params) {
|
|
305
|
+
const res = await this.http.get(
|
|
306
|
+
"/v1/prices/bargains",
|
|
307
|
+
params
|
|
308
|
+
);
|
|
309
|
+
return res.data;
|
|
310
|
+
}
|
|
311
|
+
/** `GET /v1/prices/{cardId}/history` — one row per day per guide, oldest first. */
|
|
312
|
+
async history(cardId, params) {
|
|
313
|
+
const res = await this.http.get(
|
|
314
|
+
`/v1/prices/${encodeURIComponent(cardId)}/history`,
|
|
315
|
+
params
|
|
316
|
+
);
|
|
317
|
+
return res.data;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/resources/sealed.ts
|
|
322
|
+
var SealedResource = class {
|
|
323
|
+
constructor(http) {
|
|
324
|
+
this.http = http;
|
|
325
|
+
}
|
|
326
|
+
http;
|
|
327
|
+
/** `GET /v1/sealed` — list sealed products with their latest price. */
|
|
328
|
+
list(params) {
|
|
329
|
+
return this.http.get("/v1/sealed", params);
|
|
330
|
+
}
|
|
331
|
+
/** Iterates every sealed product matching `params` across all pages. */
|
|
332
|
+
listAll(params) {
|
|
333
|
+
return paginate((page) => this.list({ ...params, page }));
|
|
334
|
+
}
|
|
335
|
+
/** `GET /v1/sealed/{id}` */
|
|
336
|
+
async get(id, params) {
|
|
337
|
+
const res = await this.http.get(
|
|
338
|
+
`/v1/sealed/${encodeURIComponent(id)}`,
|
|
339
|
+
params
|
|
340
|
+
);
|
|
341
|
+
return res.data;
|
|
342
|
+
}
|
|
343
|
+
/** `GET /v1/sealed/{id}/prices` — one row per synced day, oldest first. */
|
|
344
|
+
async prices(id, params) {
|
|
345
|
+
const res = await this.http.get(
|
|
346
|
+
`/v1/sealed/${encodeURIComponent(id)}/prices`,
|
|
347
|
+
params
|
|
348
|
+
);
|
|
349
|
+
return res.data;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/resources/sets.ts
|
|
354
|
+
var SetsResource = class {
|
|
355
|
+
constructor(http) {
|
|
356
|
+
this.http = http;
|
|
357
|
+
}
|
|
358
|
+
http;
|
|
359
|
+
/** `GET /v1/sets` — every set. */
|
|
360
|
+
async list() {
|
|
361
|
+
const res = await this.http.get("/v1/sets");
|
|
362
|
+
return res.data;
|
|
363
|
+
}
|
|
364
|
+
/** `GET /v1/sets/{id}` */
|
|
365
|
+
async get(id) {
|
|
366
|
+
const res = await this.http.get(
|
|
367
|
+
`/v1/sets/${encodeURIComponent(id)}`
|
|
368
|
+
);
|
|
369
|
+
return res.data;
|
|
370
|
+
}
|
|
371
|
+
/** `GET /v1/sets/{id}/cards` */
|
|
372
|
+
cards(id, params) {
|
|
373
|
+
return this.http.get(
|
|
374
|
+
`/v1/sets/${encodeURIComponent(id)}/cards`,
|
|
375
|
+
params
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
/** Iterates every card in a set across all pages. */
|
|
379
|
+
cardsAll(id, params) {
|
|
380
|
+
return paginate((page) => this.cards(id, { ...params, page }));
|
|
381
|
+
}
|
|
382
|
+
/** `GET /v1/sets/{id}/cards/{cardId}/neighbors` — the 2 previous / 2 next card ids. */
|
|
383
|
+
async neighbors(setId, cardId) {
|
|
384
|
+
const res = await this.http.get(
|
|
385
|
+
`/v1/sets/${encodeURIComponent(setId)}/cards/${encodeURIComponent(cardId)}/neighbors`
|
|
386
|
+
);
|
|
387
|
+
return res.data;
|
|
388
|
+
}
|
|
389
|
+
/** `GET /v1/sets/{id}/sealed` — sealed products attached to a set, each with its latest price. */
|
|
390
|
+
async sealed(id, params) {
|
|
391
|
+
const res = await this.http.get(
|
|
392
|
+
`/v1/sets/${encodeURIComponent(id)}/sealed`,
|
|
393
|
+
params
|
|
394
|
+
);
|
|
395
|
+
return res.data;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/client.ts
|
|
400
|
+
var DEFAULT_BASE_URL = "https://api.carddex.dev";
|
|
401
|
+
function isBrowserLike() {
|
|
402
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
403
|
+
}
|
|
404
|
+
var CardDex = class {
|
|
405
|
+
cards;
|
|
406
|
+
sets;
|
|
407
|
+
sealed;
|
|
408
|
+
prices;
|
|
409
|
+
auth;
|
|
410
|
+
http;
|
|
411
|
+
constructor(options = {}) {
|
|
412
|
+
if (options.apiKey && isBrowserLike()) {
|
|
413
|
+
console.warn(
|
|
414
|
+
"[@carddex/sdk] An apiKey was passed in what looks like a browser environment. API keys shipped in frontend code are visible to every visitor. Omit apiKey for CardDex's anonymous browser mode, or proxy authenticated requests through your own backend."
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
418
|
+
if (!fetchImpl) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"@carddex/sdk requires a global fetch implementation. On Node < 18, pass one via `fetch` in the options."
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
this.http = new HttpClient({
|
|
424
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
425
|
+
apiKey: options.apiKey,
|
|
426
|
+
fetch: fetchImpl.bind(globalThis),
|
|
427
|
+
retry: options.retry
|
|
428
|
+
});
|
|
429
|
+
this.cards = new CardsResource(this.http);
|
|
430
|
+
this.sets = new SetsResource(this.http);
|
|
431
|
+
this.sealed = new SealedResource(this.http);
|
|
432
|
+
this.prices = new PricesResource(this.http);
|
|
433
|
+
this.auth = new AuthResource(this.http);
|
|
434
|
+
}
|
|
435
|
+
/** `GET /v1/auth/usage` — shorthand for `client.auth.usage()`. Requires an `apiKey`. */
|
|
436
|
+
usage() {
|
|
437
|
+
return this.auth.usage();
|
|
438
|
+
}
|
|
439
|
+
/** `GET /v1/health` — API health, price freshness, and cron status. */
|
|
440
|
+
async health() {
|
|
441
|
+
return this.http.get("/v1/health");
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
//# sourceMappingURL=index.cjs.map
|