scryfall-api 4.0.4 → 5.0.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.
@@ -0,0 +1,401 @@
1
+ // src/error/invalid.error.ts
2
+ var InvalidScryfallArgumentError = class extends Error {};
3
+
4
+ // src/error/scryfall.error.ts
5
+ var ScryfallError = class extends Error {
6
+ code;
7
+ status;
8
+ type;
9
+ warnings;
10
+ constructor(code, details, status, type, warnings) {
11
+ super(details);
12
+ this.code = code;
13
+ this.status = status;
14
+ this.type = type;
15
+ this.warnings = warnings;
16
+ }
17
+ };
18
+
19
+ // src/error/unknown.error.ts
20
+ var UnknownScryfallError = class extends Error {};
21
+
22
+ // src/fetcher/debounce.fetcher.ts
23
+ const defaultNow = () => Date.now();
24
+ async function sleep(milliseconds) {
25
+ await new Promise((resolve) => {
26
+ setTimeout(resolve, milliseconds);
27
+ });
28
+ }
29
+ function createDebounceFetcher(fetcher, { maxRequestsPerSecond, now, sleep: sleepFunction }) {
30
+ const interval = 1e3 / maxRequestsPerSecond;
31
+ const clock = now ?? defaultNow;
32
+ const wait = sleepFunction ?? sleep;
33
+ let lastRequestAt = -Infinity;
34
+ return async function debounceFetcher(...arguments_) {
35
+ for (;;) {
36
+ const currentTime = clock();
37
+ const timeUntilNextRequest = lastRequestAt + interval - currentTime;
38
+ if (timeUntilNextRequest <= 0) {
39
+ lastRequestAt = currentTime;
40
+ break;
41
+ }
42
+ await wait(timeUntilNextRequest);
43
+ }
44
+ return fetcher(...arguments_);
45
+ };
46
+ }
47
+
48
+ // src/fetcher/retry.fetcher.ts
49
+ const defaultCanRetry = (object) => {
50
+ const status = "status" in object ? Number(object.status) : void 0;
51
+ if (status && status >= 500) return true;
52
+ const code = "code" in object && typeof object.code === "string" ? object.code : void 0;
53
+ return code !== "not_found" && code !== "bad_request";
54
+ };
55
+ function createRetryFetcher(fetcher = fetch, { canRetry = defaultCanRetry, maxAttempts = 3 } = {}) {
56
+ let retries = 0;
57
+ return async function retryFetcher(...arguments_) {
58
+ retries++;
59
+ const response = await fetcher(...arguments_);
60
+ if (response.ok) return response;
61
+ const object = await response.json();
62
+ const code = "code" in object && typeof object.code === "string" ? object.code : void 0;
63
+ const status = "status" in object ? Number(object.status) : void 0;
64
+ const details = "details" in object && typeof object.details === "string" ? object.details : void 0;
65
+ const type = "type" in object && typeof object.type === "string" ? object.type : void 0;
66
+ const warnings = "warnings" in object && Array.isArray(object.warnings) ? object.warnings : void 0;
67
+ if (retries >= maxAttempts) {
68
+ if (code && details && status) throw new ScryfallError(code, details, status, type, warnings);
69
+ throw new UnknownScryfallError(`Request failed with status ${response.status.toFixed(0)}`);
70
+ }
71
+ if (canRetry(object)) return retryFetcher(...arguments_);
72
+ if (code && details && status) throw new ScryfallError(code, details, status, type, warnings);
73
+ throw new UnknownScryfallError(`Request failed with status ${response.status.toFixed(0)}`);
74
+ };
75
+ }
76
+
77
+ // src/fetcher/fetcher.ts
78
+ const scryfallEndpoint = "https://api.scryfall.com";
79
+ const defaultRequestsPerSecond = 10;
80
+ const DATE_REGEXP = /^\d{4}-\d{2}-\d{2}?$/;
81
+ const dateParser = (_key, value) => {
82
+ if (typeof value === "string" && DATE_REGEXP.test(value)) return new Date(value);
83
+ return value;
84
+ };
85
+ const globallyLimitedFetch = createDebounceFetcher(fetch, { maxRequestsPerSecond: defaultRequestsPerSecond });
86
+ function createFetcher({ fetch: fetchFunction, maxRequestsPerSecond = defaultRequestsPerSecond, now, sleep } = {}) {
87
+ const globalFetch = fetchFunction ? createDebounceFetcher(fetchFunction, {
88
+ maxRequestsPerSecond: defaultRequestsPerSecond,
89
+ now,
90
+ sleep
91
+ }) : globallyLimitedFetch;
92
+ const retryingFetch = createRetryFetcher(globalFetch);
93
+ const simpleFetcher = maxRequestsPerSecond === defaultRequestsPerSecond ? retryingFetch : createDebounceFetcher(retryingFetch, {
94
+ maxRequestsPerSecond,
95
+ now,
96
+ sleep
97
+ });
98
+ return async function fetcher(apiPath, parameters, body) {
99
+ let path = "";
100
+ if (typeof apiPath === "number" || typeof apiPath === "string") path = apiPath;
101
+ else if (apiPath) path = apiPath.join("/");
102
+ const url = new URL(`${scryfallEndpoint}/${String(path)}`);
103
+ if (parameters) {
104
+ for (const [key, value] of Object.entries(parameters)) if (value !== void 0) url.searchParams.append(key, value.toString());
105
+ }
106
+ try {
107
+ const init = { method: body ? "POST" : "GET" };
108
+ if (body) {
109
+ init.body = JSON.stringify(body);
110
+ init.headers = { "Content-Type": "application/json" };
111
+ }
112
+ const content = await (await simpleFetcher(url, init)).text();
113
+ return JSON.parse(content, dateParser);
114
+ } catch (error) {
115
+ if (error instanceof ScryfallError) {
116
+ if (error.warnings) console.warn(error.warnings);
117
+ if (error.code === "not_found" || error.code === "bad_request") return;
118
+ }
119
+ throw error;
120
+ }
121
+ };
122
+ }
123
+ const fetcher = createFetcher();
124
+
125
+ // src/fetcher/page.result.ts
126
+ var MagicPageResult = class {
127
+ apiPath;
128
+ query;
129
+ fetcher;
130
+ #count = 0;
131
+ #hasMore = true;
132
+ constructor(apiPath, query, fetcher$1 = fetcher) {
133
+ this.apiPath = apiPath;
134
+ this.query = query;
135
+ this.fetcher = fetcher$1;
136
+ }
137
+ get count() {
138
+ return this.#count;
139
+ }
140
+ get hasMore() {
141
+ return this.#hasMore;
142
+ }
143
+ async all() {
144
+ const r = [];
145
+ while (this.#hasMore) {
146
+ const result = await this.next();
147
+ r.push(...result);
148
+ }
149
+ return r;
150
+ }
151
+ async get(limit) {
152
+ const r = [];
153
+ while (this.#hasMore) {
154
+ const result = await this.next();
155
+ r.push(...result);
156
+ if (r.length === limit) return r;
157
+ if (r.length > limit) return r.slice(0, limit);
158
+ }
159
+ return r;
160
+ }
161
+ async next() {
162
+ const results = await this.fetcher(this.apiPath, this.query);
163
+ this.#hasMore = results?.has_more ?? false;
164
+ this.#count = Number.parseInt(results?.total_cards?.toFixed(0) ?? "0", 10);
165
+ if (this.#hasMore) this.setNextPage();
166
+ return results?.data ?? [];
167
+ }
168
+ async page(page) {
169
+ this.query.page = page;
170
+ return this.next();
171
+ }
172
+ setNextPage() {
173
+ this.query.page += 1;
174
+ }
175
+ };
176
+
177
+ // src/api/card/card.api.ts
178
+ const cardsPerSecond = 2;
179
+ const collectionFetcher = createFetcher({ maxRequestsPerSecond: cardsPerSecond });
180
+ const namedFetcher = createFetcher({ maxRequestsPerSecond: cardsPerSecond });
181
+ const randomFetcher = createFetcher({ maxRequestsPerSecond: cardsPerSecond });
182
+ const searchFetcher = createFetcher({ maxRequestsPerSecond: cardsPerSecond });
183
+ var CardApi = class {
184
+ async autoCompleteName(name) {
185
+ return (await fetcher("cards/autocomplete", { q: name }))?.data ?? [];
186
+ }
187
+ async byArenaId(id) {
188
+ return fetcher(["cards/arena", id]);
189
+ }
190
+ async byId(id) {
191
+ return fetcher(["cards", id]);
192
+ }
193
+ async byMtgoId(id) {
194
+ return fetcher(["cards/mtgo", id]);
195
+ }
196
+ async byMultiverseId(id) {
197
+ return fetcher(["cards/multiverse", id]);
198
+ }
199
+ async byName(name, set, isFuzzySearch = false) {
200
+ let isF = isFuzzySearch;
201
+ let s = set;
202
+ if (typeof set === "boolean") {
203
+ isF = set;
204
+ s = void 0;
205
+ }
206
+ return namedFetcher("cards/named", {
207
+ [isF ? "fuzzy" : "exact"]: name,
208
+ set: s
209
+ });
210
+ }
211
+ async bySet(set, collectorNumber, lang) {
212
+ const path = [
213
+ "cards",
214
+ set,
215
+ collectorNumber
216
+ ];
217
+ if (lang) path.push(lang);
218
+ return fetcher(path);
219
+ }
220
+ async byTcgPlayerId(id) {
221
+ return fetcher(["cards/tcgplayer", id]);
222
+ }
223
+ async collection(...identifiers) {
224
+ const cards = [];
225
+ for (let index = 0; index < identifiers.length; index += 75) {
226
+ const collectionSection = { identifiers: identifiers.slice(index, index + 75) };
227
+ const result = await collectionFetcher("cards/collection", void 0, collectionSection);
228
+ cards.push(...result?.data ?? []);
229
+ }
230
+ return cards;
231
+ }
232
+ async random() {
233
+ return randomFetcher("cards/random");
234
+ }
235
+ search(query, options) {
236
+ return new MagicPageResult("cards/search", {
237
+ q: query,
238
+ ...typeof options === "number" ? { page: options } : {
239
+ ...options,
240
+ page: options?.page ?? 1
241
+ }
242
+ }, searchFetcher);
243
+ }
244
+ };
245
+ const cards = new CardApi();
246
+
247
+ // src/api/card-identifier/card-identifier.api.ts
248
+ var CardIdentifierApi = class {
249
+ static byId(id) {
250
+ return { id };
251
+ }
252
+ static byIllustrationId(id) {
253
+ return { illustration_id: id };
254
+ }
255
+ static byMtgoId(id) {
256
+ return { mtgo_id: id };
257
+ }
258
+ static byMultiverseId(id) {
259
+ return { multiverse_id: id };
260
+ }
261
+ static byName(name, set) {
262
+ if (set) return {
263
+ name,
264
+ set
265
+ };
266
+ return { name };
267
+ }
268
+ static byOracleId(id) {
269
+ return { oracle_id: id };
270
+ }
271
+ static bySet(set, collectorNumber) {
272
+ return {
273
+ collector_number: String(collectorNumber),
274
+ set
275
+ };
276
+ }
277
+ };
278
+
279
+ // src/api/catalog/catalog.api.ts
280
+ var CatalogApi = class {
281
+ async artifactTypes() {
282
+ return (await fetcher("catalog/artifact-types"))?.data ?? [];
283
+ }
284
+ async artistNames() {
285
+ return (await fetcher("catalog/artist-names"))?.data ?? [];
286
+ }
287
+ async cardNames() {
288
+ return (await fetcher("catalog/card-names"))?.data ?? [];
289
+ }
290
+ async creatureTypes() {
291
+ return (await fetcher("catalog/creature-types"))?.data ?? [];
292
+ }
293
+ async enchantmentTypes() {
294
+ return (await fetcher("catalog/enchantment-types"))?.data ?? [];
295
+ }
296
+ async landTypes() {
297
+ return (await fetcher("catalog/land-types"))?.data ?? [];
298
+ }
299
+ async loyalties() {
300
+ return (await fetcher("catalog/loyalties"))?.data ?? [];
301
+ }
302
+ async planeswalkerTypes() {
303
+ return (await fetcher("catalog/planeswalker-types"))?.data ?? [];
304
+ }
305
+ async powers() {
306
+ return (await fetcher("catalog/powers"))?.data ?? [];
307
+ }
308
+ async spellTypes() {
309
+ return (await fetcher("catalog/spell-types"))?.data ?? [];
310
+ }
311
+ async toughnesses() {
312
+ return (await fetcher("catalog/toughnesses"))?.data ?? [];
313
+ }
314
+ async watermarks() {
315
+ return (await fetcher("catalog/watermarks"))?.data ?? [];
316
+ }
317
+ async wordBank() {
318
+ return (await fetcher("catalog/word-bank"))?.data ?? [];
319
+ }
320
+ };
321
+ const catalog = new CatalogApi();
322
+
323
+ // src/api/ruling/ruling.api.ts
324
+ var RulingApi = class {
325
+ async byArenaId(id) {
326
+ return (await fetcher([
327
+ "cards/arena",
328
+ id,
329
+ "rulings"
330
+ ]))?.data ?? [];
331
+ }
332
+ async byId(id) {
333
+ return (await fetcher([
334
+ "cards",
335
+ id,
336
+ "rulings"
337
+ ]))?.data ?? [];
338
+ }
339
+ async byMtgoId(id) {
340
+ return (await fetcher([
341
+ "cards/mtgo",
342
+ id,
343
+ "rulings"
344
+ ]))?.data ?? [];
345
+ }
346
+ async byMultiverseId(id) {
347
+ return (await fetcher([
348
+ "cards/multiverse",
349
+ id,
350
+ "rulings"
351
+ ]))?.data ?? [];
352
+ }
353
+ async bySet(set, collectorNumber) {
354
+ return (await fetcher([
355
+ "cards",
356
+ set,
357
+ String(collectorNumber),
358
+ "rulings"
359
+ ]))?.data ?? [];
360
+ }
361
+ };
362
+ const rulings = new RulingApi();
363
+
364
+ // src/api/set/set.api.ts
365
+ var SetApi = class {
366
+ async all() {
367
+ return (await fetcher("sets"))?.data ?? [];
368
+ }
369
+ async byCode(code) {
370
+ return await fetcher(["sets", code]);
371
+ }
372
+ async byId(id) {
373
+ return fetcher(["sets", id]);
374
+ }
375
+ async byTcgPlayerId(id) {
376
+ return fetcher(["sets/tcgplayer", id]);
377
+ }
378
+ };
379
+ const set = new SetApi();
380
+
381
+ // src/api/symbology/symbology.api.ts
382
+ var SymbologyApi = class {
383
+ async all() {
384
+ return (await fetcher("symbology"))?.data ?? [];
385
+ }
386
+ async parseMana(shorthand) {
387
+ if (!shorthand || typeof shorthand !== "string") throw new InvalidScryfallArgumentError("shorthand must be a string");
388
+ return await fetcher("symbology/parse-mana", { cost: shorthand }) ?? {
389
+ cmc: 0,
390
+ colorless: false,
391
+ colors: [],
392
+ cost: "",
393
+ monocolored: false,
394
+ multicolored: false,
395
+ object: "mana_cost"
396
+ };
397
+ }
398
+ };
399
+ const symbology = new SymbologyApi();
400
+
401
+ export { CardIdentifierApi as CardIdentifierBuilder, cards as Cards, catalog as Catalogs, InvalidScryfallArgumentError, MagicPageResult, rulings as Rulings, ScryfallError, set as Sets, symbology as SymbologyApi, UnknownScryfallError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scryfall-api",
3
- "version": "4.0.4",
3
+ "version": "5.0.0",
4
4
  "description": "A Javascript library for https://scryfall.com/docs/api written in Typescript.",
5
5
  "keywords": [
6
6
  "magic",
@@ -28,37 +28,35 @@
28
28
  "files": [
29
29
  "dist"
30
30
  ],
31
- "main": "./dist/scryfall-api.js",
32
- "module": "./dist/scryfall-api.js",
33
- "types": "./dist/scryfall-api.d.ts",
31
+ "main": "./dist/scryfall-api.mjs",
32
+ "types": "./dist/scryfall-api.d.mts",
34
33
  "exports": {
35
34
  ".": {
36
- "import": {
37
- "types": "./dist/scryfall-api.d.ts",
38
- "default": "./dist/scryfall-api.js"
39
- },
40
- "require": {
41
- "types": "./dist/scryfall-api.d.cts",
42
- "default": "./dist/scryfall-api.cjs"
43
- },
44
- "default": "./dist/scryfall-api.js"
35
+ "import": "./dist/scryfall-api.d.mts"
45
36
  }
46
37
  },
47
38
  "scripts": {
48
- "build": "tsup",
49
- "lint": "eslint .",
50
- "lint:fix": "eslint --fix .",
51
- "prepublishOnly": "tsup",
39
+ "build": "tsdown",
40
+ "commitlint": "commitlint --edit",
41
+ "lint": "eslint --cache .",
42
+ "lint:ci": "eslint --cache --cache-strategy content .",
43
+ "lint:fix": "eslint --cache --fix .",
44
+ "prepare": "husky",
45
+ "prepublishOnly": "tsdown",
52
46
  "test": "bun test --timeout 50000",
53
47
  "typecheck": "tsc --noEmit"
54
48
  },
55
49
  "devDependencies": {
56
- "@changesets/cli": "2.29.7",
57
- "@hexadrop/eslint-config": "0.1.9",
50
+ "@changesets/cli": "3.0.3",
51
+ "@commitlint/cli": "21.2.4",
52
+ "@commitlint/config-conventional": "21.2.4",
53
+ "@hexadrop/eslint-config": "1.3.1",
58
54
  "@hexadrop/tsconfig": "0.0.5",
59
- "@types/bun": "1.2.23",
60
- "eslint": "9.37.0",
61
- "tsup": "8.5.0",
62
- "typescript": "5.9.3"
55
+ "@types/bun": "1.4.4",
56
+ "eslint": "10.13.0",
57
+ "husky": "9.1.7",
58
+ "lint-staged": "17.6.0",
59
+ "tsdown": "0.23.4",
60
+ "typescript": "6.0.3"
63
61
  }
64
62
  }