mtgjson-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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2018-Present Zachary Halpern
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,463 @@
1
+ # mtgjson-sdk
2
+
3
+ A high-performance, DuckDB-backed TypeScript query client for [MTGJSON](https://mtgjson.com).
4
+
5
+ Unlike traditional SDKs that rely on rate-limited REST APIs, `mtgjson-sdk` implements a local data warehouse architecture. It synchronizes optimized Parquet data from the MTGJSON CDN to your local machine, utilizing DuckDB to execute complex analytics, fuzzy searches, and booster simulations with sub-millisecond latency.
6
+
7
+ ## Key Features
8
+
9
+ * **Vectorized Execution**: Powered by DuckDB for high-speed OLAP queries on the full MTG dataset.
10
+ * **Offline-First**: Data is cached locally, allowing for full functionality without an active internet connection.
11
+ * **Fuzzy Search**: Built-in Jaro-Winkler similarity matching to handle typos and approximate name lookups.
12
+ * **Fully Async**: Native async/await API with `Symbol.asyncDispose` for automatic resource cleanup.
13
+ * **Fully Typed**: Complete TypeScript type definitions for all query results and parameters.
14
+ * **Booster Simulation**: Accurate pack opening logic using official MTGJSON weights and sheet configurations.
15
+
16
+ ## Install
17
+
18
+ TODO: NPM STUFF
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { MtgjsonSDK } from "mtgjson-sdk";
24
+
25
+ const sdk = await MtgjsonSDK.create();
26
+
27
+ // Search for cards
28
+ const bolts = await sdk.cards.getByName("Lightning Bolt");
29
+ console.log(`Found ${bolts.length} printings of Lightning Bolt`);
30
+
31
+ // Get a specific set
32
+ const mh3 = await sdk.sets.get("MH3");
33
+ if (mh3) {
34
+ console.log(`${mh3.name} -- ${mh3.totalSetSize} cards`);
35
+ }
36
+
37
+ // Check format legality
38
+ const isLegal = await sdk.legalities.isLegal(bolts[0].uuid, "modern");
39
+ console.log(`Modern legal: ${isLegal}`);
40
+
41
+ // Find the cheapest printing
42
+ const cheapest = await sdk.prices.cheapestPrinting("Lightning Bolt");
43
+ if (cheapest) {
44
+ console.log(`Cheapest: $${cheapest.price} (${cheapest.setCode})`);
45
+ }
46
+
47
+ // Raw SQL for anything else
48
+ const rows = await sdk.sql("SELECT name, manaValue FROM cards WHERE manaValue = $1 LIMIT 5", [0]);
49
+
50
+ await sdk.close();
51
+ ```
52
+
53
+ ## Architecture
54
+
55
+ By using DuckDB, the SDK leverages columnar storage and vectorized execution, making it significantly faster than SQLite or standard JSON parsing for MTG's relational dataset.
56
+
57
+ 1. **Synchronization**: On first use, the SDK lazily downloads Parquet and JSON files from the MTGJSON CDN to a platform-specific cache directory (`~/.cache/mtgjson-sdk` on Linux, `~/Library/Caches/mtgjson-sdk` on macOS, `AppData/Local/mtgjson-sdk` on Windows).
58
+ 2. **Virtual Schema**: DuckDB views are registered on-demand. Accessing `sdk.cards` registers the card view; accessing `sdk.prices` registers price data. You only pay the memory cost for the data you query.
59
+ 3. **Dynamic Adaptation**: The SDK introspects Parquet metadata to automatically handle schema changes, plural-column array conversion, and format legality unpivoting.
60
+ 4. **Materialization**: Queries return fully-typed TypeScript interfaces for individual record ergonomics, or raw `Record<string, unknown>` objects for flexible consumption.
61
+
62
+ ## Use Cases
63
+
64
+ ### Price Analytics
65
+
66
+ ```typescript
67
+ const sdk = await MtgjsonSDK.create();
68
+
69
+ // Find the cheapest printing of any card
70
+ const cheapest = await sdk.prices.cheapestPrinting("Ragavan, Nimble Pilferer");
71
+
72
+ // Price trend over time
73
+ if (cheapest) {
74
+ const trend = await sdk.prices.priceTrend(cheapest.uuid, {
75
+ provider: "tcgplayer",
76
+ finish: "normal",
77
+ });
78
+ console.log(`Range: $${trend.min_price} - $${trend.max_price}`);
79
+ console.log(`Average: $${trend.avg_price} over ${trend.data_points} data points`);
80
+
81
+ // Full price history with date range
82
+ const history = await sdk.prices.history(cheapest.uuid, {
83
+ provider: "tcgplayer",
84
+ dateFrom: "2024-01-01",
85
+ dateTo: "2024-12-31",
86
+ });
87
+
88
+ // Most expensive printings across the entire dataset
89
+ const priciest = await sdk.prices.mostExpensivePrintings({ limit: 10 });
90
+ }
91
+
92
+ await sdk.close();
93
+ ```
94
+
95
+ ### Advanced Card Search
96
+
97
+ The `search()` method supports ~20 composable filters that can be combined freely:
98
+
99
+ ```typescript
100
+ const sdk = await MtgjsonSDK.create();
101
+
102
+ // Complex filters: Modern-legal red creatures with CMC <= 2
103
+ const aggroCreatures = await sdk.cards.search({
104
+ colors: ["R"],
105
+ types: "Creature",
106
+ manaValueLte: 2.0,
107
+ legalIn: "modern",
108
+ limit: 50,
109
+ });
110
+
111
+ // Typo-tolerant fuzzy search (Jaro-Winkler similarity)
112
+ const results = await sdk.cards.search({
113
+ fuzzyName: "Ligtning Bolt", // still finds it!
114
+ });
115
+
116
+ // Rules text search using regular expressions
117
+ const burn = await sdk.cards.search({
118
+ textRegex: "deals? \\d+ damage to any target",
119
+ });
120
+
121
+ // Search by keyword ability across formats
122
+ const flyers = await sdk.cards.search({
123
+ keyword: "Flying",
124
+ colors: ["W", "U"],
125
+ legalIn: "standard",
126
+ });
127
+
128
+ // Find cards by foreign-language name
129
+ const blitz = await sdk.cards.search({
130
+ localizedName: "Blitzschlag", // German for Lightning Bolt
131
+ });
132
+
133
+ await sdk.close();
134
+ ```
135
+
136
+ <details>
137
+ <summary>All <code>search()</code> parameters</summary>
138
+
139
+ | Parameter | Type | Description |
140
+ |---|---|---|
141
+ | `name` | `string` | Name pattern (`%` = wildcard) |
142
+ | `fuzzyName` | `string` | Typo-tolerant Jaro-Winkler match |
143
+ | `localizedName` | `string` | Foreign-language name search |
144
+ | `colors` | `string[]` | Cards containing these colors |
145
+ | `colorIdentity` | `string[]` | Color identity filter |
146
+ | `legalIn` | `string` | Format legality |
147
+ | `rarity` | `string` | Rarity filter |
148
+ | `manaValue` | `number` | Exact mana value |
149
+ | `manaValueLte` | `number` | Mana value upper bound |
150
+ | `manaValueGte` | `number` | Mana value lower bound |
151
+ | `text` | `string` | Rules text substring |
152
+ | `textRegex` | `string` | Rules text regex |
153
+ | `types` | `string` | Type line search |
154
+ | `artist` | `string` | Artist name |
155
+ | `keyword` | `string` | Keyword ability |
156
+ | `isPromo` | `boolean` | Promo status |
157
+ | `availability` | `string` | `"paper"` or `"mtgo"` |
158
+ | `language` | `string` | Language filter |
159
+ | `layout` | `string` | Card layout |
160
+ | `setCode` | `string` | Set code |
161
+ | `setType` | `string` | Set type (joins sets table) |
162
+ | `power` | `string` | Power filter |
163
+ | `toughness` | `string` | Toughness filter |
164
+ | `limit` / `offset` | `number` | Pagination |
165
+
166
+ </details>
167
+
168
+ ### Collection & Cross-Reference
169
+
170
+ ```typescript
171
+ const sdk = await MtgjsonSDK.create();
172
+
173
+ // Cross-reference by any external ID system
174
+ const cards = await sdk.identifiers.findByScryfallId("f7a21fe4-...");
175
+ const tcgCards = await sdk.identifiers.findByTcgplayerId("12345");
176
+ const mtgoCards = await sdk.identifiers.findByMtgoId("67890");
177
+
178
+ // Get all external identifiers for a card
179
+ const allIds = await sdk.identifiers.getIdentifiers("card-uuid-here");
180
+ // -> Scryfall, TCGPlayer, MTGO, Arena, Cardmarket, Card Kingdom, Cardsphere, ...
181
+
182
+ // TCGPlayer SKU variants (foil, etched, etc.)
183
+ const skus = await sdk.skus.get("card-uuid-here");
184
+
185
+ // Export to a standalone DuckDB file for offline analysis
186
+ await sdk.exportDb("my_collection.duckdb");
187
+ // Now query with: duckdb my_collection.duckdb "SELECT * FROM cards LIMIT 5"
188
+
189
+ await sdk.close();
190
+ ```
191
+
192
+ ### Booster Simulation
193
+
194
+ ```typescript
195
+ const sdk = await MtgjsonSDK.create();
196
+
197
+ // See available booster types for a set
198
+ const types = await sdk.booster.availableTypes("MH3"); // ["draft", "collector", ...]
199
+
200
+ // Open a single draft pack
201
+ const pack = await sdk.booster.openPack("MH3", "draft");
202
+ for (const card of pack) {
203
+ console.log(` ${card.name} (${card.rarity})`);
204
+ }
205
+
206
+ // Open an entire box
207
+ const box = await sdk.booster.openBox("MH3", "draft", 36);
208
+ const totalCards = box.reduce((sum, p) => sum + p.length, 0);
209
+ console.log(`Opened ${box.length} packs, ${totalCards} total cards`);
210
+
211
+ await sdk.close();
212
+ ```
213
+
214
+ ## API Reference
215
+
216
+ ### Core Data
217
+
218
+ ```typescript
219
+ // Cards
220
+ await sdk.cards.getByUuid("uuid") // single card lookup
221
+ await sdk.cards.getByUuids(["uuid1", "uuid2"]) // batch lookup
222
+ await sdk.cards.getByName("Lightning Bolt") // all printings of a name
223
+ await sdk.cards.search({...}) // composable filters (see above)
224
+ await sdk.cards.getPrintings("Lightning Bolt") // all printings across sets
225
+ await sdk.cards.getAtomic("Lightning Bolt") // oracle data (no printing info)
226
+ await sdk.cards.findByScryfallId("...") // cross-reference shortcut
227
+ await sdk.cards.random(5) // random cards
228
+ await sdk.cards.count() // total (or filtered with kwargs)
229
+
230
+ // Tokens
231
+ await sdk.tokens.getByUuid("uuid")
232
+ await sdk.tokens.getByName("Soldier")
233
+ await sdk.tokens.search({ name: "%Token", setCode: "MH3", colors: ["W"] })
234
+ await sdk.tokens.forSet("MH3")
235
+ await sdk.tokens.count()
236
+
237
+ // Sets
238
+ await sdk.sets.get("MH3")
239
+ await sdk.sets.list({ setType: "expansion" })
240
+ await sdk.sets.search({ name: "Horizons", releaseYear: 2024 })
241
+ await sdk.sets.getFinancialSummary("MH3", { provider: "tcgplayer" })
242
+ await sdk.sets.count()
243
+ ```
244
+
245
+ ### Playability
246
+
247
+ ```typescript
248
+ // Legalities
249
+ await sdk.legalities.formatsForCard("uuid") // -> { modern: "Legal", ... }
250
+ await sdk.legalities.legalIn("modern") // all modern-legal cards
251
+ await sdk.legalities.isLegal("uuid", "modern") // -> boolean
252
+ await sdk.legalities.bannedIn("modern") // also: restrictedIn, suspendedIn
253
+
254
+ // Decks & Sealed Products
255
+ await sdk.decks.list({ setCode: "MH3" })
256
+ await sdk.decks.search({ name: "Eldrazi" })
257
+ await sdk.decks.count()
258
+ await sdk.sealed.list({ setCode: "MH3" })
259
+ await sdk.sealed.get("uuid")
260
+ ```
261
+
262
+ ### Market & Identifiers
263
+
264
+ ```typescript
265
+ // Prices
266
+ await sdk.prices.get("uuid") // full nested price data
267
+ await sdk.prices.today("uuid", { provider: "tcgplayer", finish: "foil" })
268
+ await sdk.prices.history("uuid", { provider: "tcgplayer", dateFrom: "2024-01-01" })
269
+ await sdk.prices.priceTrend("uuid") // min/max/avg statistics
270
+ await sdk.prices.cheapestPrinting("Lightning Bolt")
271
+ await sdk.prices.mostExpensivePrintings({ limit: 10 })
272
+
273
+ // Identifiers (supports all major external ID systems)
274
+ await sdk.identifiers.findByScryfallId("...")
275
+ await sdk.identifiers.findByTcgplayerId("...")
276
+ await sdk.identifiers.findByMtgoId("...")
277
+ await sdk.identifiers.findByMtgArenaId("...")
278
+ await sdk.identifiers.findByMultiverseId("...")
279
+ await sdk.identifiers.findByMcmId("...")
280
+ await sdk.identifiers.findByCardKingdomId("...")
281
+ await sdk.identifiers.findBy("scryfallId", "...") // generic lookup
282
+ await sdk.identifiers.getIdentifiers("uuid") // all IDs for a card
283
+
284
+ // SKUs
285
+ await sdk.skus.get("uuid")
286
+ await sdk.skus.findBySkuId(123456)
287
+ await sdk.skus.findByProductId(789)
288
+ ```
289
+
290
+ ### Booster & Enums
291
+
292
+ ```typescript
293
+ await sdk.booster.availableTypes("MH3")
294
+ await sdk.booster.openPack("MH3", "draft")
295
+ await sdk.booster.openBox("MH3", "draft", 36)
296
+ await sdk.booster.sheetContents("MH3", "draft", "common")
297
+
298
+ await sdk.enums.keywords()
299
+ await sdk.enums.cardTypes()
300
+ await sdk.enums.enumValues()
301
+ ```
302
+
303
+ ### System
304
+
305
+ ```typescript
306
+ await sdk.meta // version and build date
307
+ sdk.views // registered view names
308
+ await sdk.refresh() // check CDN for new data -> boolean
309
+ await sdk.exportDb("output.duckdb") // export to persistent DuckDB file
310
+ await sdk.sql(query, params) // raw parameterized SQL
311
+ await sdk.close() // release resources
312
+ ```
313
+
314
+ ## Performance and Memory
315
+
316
+ When querying large datasets (thousands of cards), use raw SQL to avoid materializing large arrays of typed objects in memory.
317
+
318
+ ```typescript
319
+ // Use raw SQL for bulk analysis
320
+ const stats = await sdk.sql(`
321
+ SELECT setCode, COUNT(*) as card_count, AVG(manaValue) as avg_cmc
322
+ FROM cards
323
+ GROUP BY setCode
324
+ ORDER BY card_count DESC
325
+ LIMIT 10
326
+ `);
327
+ ```
328
+
329
+ ## Advanced Usage
330
+
331
+ ### Async Factory Pattern
332
+
333
+ The SDK uses an async factory since DuckDB initialization is asynchronous:
334
+
335
+ ```typescript
336
+ import { MtgjsonSDK } from "mtgjson-sdk";
337
+
338
+ const sdk = await MtgjsonSDK.create({
339
+ cacheDir: "/data/mtgjson-cache",
340
+ offline: false,
341
+ timeout: 300_000,
342
+ onProgress: (filename, downloaded, total) => {
343
+ const pct = total ? ((downloaded / total) * 100).toFixed(1) : "?";
344
+ process.stdout.write(`\r${filename}: ${pct}%`);
345
+ },
346
+ });
347
+ ```
348
+
349
+ ### Automatic Resource Cleanup
350
+
351
+ The SDK supports `Symbol.asyncDispose` for automatic cleanup with `await using`:
352
+
353
+ ```typescript
354
+ {
355
+ await using sdk = await MtgjsonSDK.create();
356
+
357
+ const cards = await sdk.cards.search({ name: "Lightning%" });
358
+ console.log(cards.length);
359
+
360
+ // sdk.close() is called automatically when the block exits
361
+ }
362
+ ```
363
+
364
+ ### Auto-Refresh for Long-Running Services
365
+
366
+ ```typescript
367
+ // In a scheduled task or health check:
368
+ const refreshed = await sdk.refresh();
369
+ if (refreshed) {
370
+ console.log("New MTGJSON data detected -- cache refreshed");
371
+ }
372
+ ```
373
+
374
+ ### Raw SQL
375
+
376
+ All user input goes through DuckDB parameter binding (`$1`, `$2`, ...):
377
+
378
+ ```typescript
379
+ const sdk = await MtgjsonSDK.create();
380
+
381
+ // Ensure views are registered before querying
382
+ await sdk.cards.count();
383
+
384
+ // Parameterized queries
385
+ const rows = await sdk.sql(
386
+ "SELECT name, setCode, rarity FROM cards WHERE manaValue <= $1 AND rarity = $2",
387
+ [2, "mythic"]
388
+ );
389
+ ```
390
+
391
+ ### Web API Example
392
+
393
+ ```typescript
394
+ import { MtgjsonSDK } from "mtgjson-sdk";
395
+ import express from "express";
396
+
397
+ const app = express();
398
+ const sdk = await MtgjsonSDK.create();
399
+
400
+ app.get("/card/:name", async (req, res) => {
401
+ const cards = await sdk.cards.getByName(req.params.name);
402
+ res.json(cards);
403
+ });
404
+
405
+ app.get("/health", async (_req, res) => {
406
+ const refreshed = await sdk.refresh();
407
+ res.json({ refreshed, views: sdk.views });
408
+ });
409
+
410
+ process.on("SIGTERM", async () => {
411
+ await sdk.close();
412
+ process.exit(0);
413
+ });
414
+
415
+ app.listen(3000, () => console.log("Listening on :3000"));
416
+ ```
417
+
418
+ ## Examples
419
+
420
+ ### Next.js Card Search (`examples/next-search`)
421
+
422
+ A full-stack web application built with Next.js 15 and Tailwind CSS that demonstrates the SDK in a server-side rendered context. Features a card search interface with fuzzy matching, filters (color, rarity, type, set, format legality), card detail pages, and responsive image grids powered by Scryfall.
423
+
424
+ **SDK features demonstrated:**
425
+
426
+ | Feature | SDK Method |
427
+ |---------|-----------|
428
+ | Fuzzy card search with filters | `sdk.cards.search({ fuzzyName, colors, rarity, types, setCode, legalIn })` |
429
+ | Total result count with pagination | `sdk.cards.count()` |
430
+ | Random cards on the home page | `sdk.cards.random()` |
431
+ | Card detail lookup | `sdk.cards.getByUuid()` |
432
+ | All printings across sets | `sdk.cards.getPrintings()` |
433
+ | Cross-system identifier lookup | `sdk.identifiers.getIdentifiers()` |
434
+ | Format legality table | `sdk.legalities.formatsForCard()` |
435
+ | Retail price data by provider | `sdk.prices.today()` |
436
+ | Set list for autocomplete filter | `sdk.sets.list()` |
437
+ | Data version attribution | `sdk.meta` |
438
+
439
+ ```bash
440
+ cd examples/next-search
441
+ bun install
442
+ bun run dev
443
+ # Open http://localhost:3000
444
+ ```
445
+
446
+ > **Note:** The SDK must be built first (`bun run build` in the repo root). First page load downloads parquet data from the MTGJSON CDN (~30s cold start), subsequent loads use the local cache.
447
+
448
+ ## Development
449
+
450
+ ```bash
451
+ git clone https://github.com/the-muppet2/mtgjson-sdk-typescript.git
452
+ cd mtgjson-sdk-typescript
453
+ bun install
454
+ bun run build
455
+ bun run typecheck
456
+ bun test
457
+ bun run lint
458
+ bun run format
459
+ ```
460
+
461
+ ## License
462
+
463
+ MIT