@carddb/client 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/README.md +33 -0
- package/dist/index.cjs +1249 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +780 -0
- package/dist/index.d.ts +780 -0
- package/dist/index.js +1219 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@carddb/core');
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var DEFAULT_ENDPOINT = "https://carddb.xtda.org/query";
|
|
7
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
8
|
+
var DEFAULT_OPEN_TIMEOUT = 1e4;
|
|
9
|
+
var DEFAULT_CACHE_TTL = 300;
|
|
10
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
11
|
+
var DEFAULT_MAX_NETWORK_RETRIES = 2;
|
|
12
|
+
var DEFAULT_RETRY_BASE_DELAY = 1e3;
|
|
13
|
+
var DEFAULT_RETRY_MAX_DELAY = 3e4;
|
|
14
|
+
var Configuration = class _Configuration {
|
|
15
|
+
apiKey;
|
|
16
|
+
endpoint;
|
|
17
|
+
timeout;
|
|
18
|
+
openTimeout;
|
|
19
|
+
defaultPublisher;
|
|
20
|
+
defaultGame;
|
|
21
|
+
allowedPublishers;
|
|
22
|
+
allowedGames;
|
|
23
|
+
logger;
|
|
24
|
+
logLevel;
|
|
25
|
+
retryOnRateLimit;
|
|
26
|
+
maxRetries;
|
|
27
|
+
retryOnNetworkError;
|
|
28
|
+
maxNetworkRetries;
|
|
29
|
+
retryBaseDelay;
|
|
30
|
+
retryMaxDelay;
|
|
31
|
+
cache;
|
|
32
|
+
cacheTtl;
|
|
33
|
+
cacheTtls;
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
this.apiKey = config.apiKey;
|
|
36
|
+
this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
|
|
37
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
38
|
+
this.openTimeout = config.openTimeout ?? DEFAULT_OPEN_TIMEOUT;
|
|
39
|
+
this.defaultPublisher = config.defaultPublisher;
|
|
40
|
+
this.defaultGame = config.defaultGame;
|
|
41
|
+
this.allowedPublishers = config.allowedPublishers;
|
|
42
|
+
this.allowedGames = config.allowedGames;
|
|
43
|
+
this.logger = config.logger;
|
|
44
|
+
this.logLevel = config.logLevel ?? "info";
|
|
45
|
+
this.retryOnRateLimit = config.retryOnRateLimit ?? false;
|
|
46
|
+
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
47
|
+
this.retryOnNetworkError = config.retryOnNetworkError ?? true;
|
|
48
|
+
this.maxNetworkRetries = config.maxNetworkRetries ?? DEFAULT_MAX_NETWORK_RETRIES;
|
|
49
|
+
this.retryBaseDelay = config.retryBaseDelay ?? DEFAULT_RETRY_BASE_DELAY;
|
|
50
|
+
this.retryMaxDelay = config.retryMaxDelay ?? DEFAULT_RETRY_MAX_DELAY;
|
|
51
|
+
this.cache = config.cache;
|
|
52
|
+
this.cacheTtl = config.cacheTtl ?? DEFAULT_CACHE_TTL;
|
|
53
|
+
this.cacheTtls = config.cacheTtls ?? {};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Calculate delay for exponential backoff
|
|
57
|
+
* @param attempt - The attempt number (0-indexed)
|
|
58
|
+
* @returns Delay in milliseconds with jitter
|
|
59
|
+
*/
|
|
60
|
+
calculateRetryDelay(attempt) {
|
|
61
|
+
const exponentialDelay = this.retryBaseDelay * Math.pow(2, attempt);
|
|
62
|
+
const cappedDelay = Math.min(exponentialDelay, this.retryMaxDelay);
|
|
63
|
+
const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
|
|
64
|
+
return Math.round(cappedDelay + jitter);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the cache TTL for a specific resource type
|
|
68
|
+
*/
|
|
69
|
+
cacheTtlFor(resource) {
|
|
70
|
+
return this.cacheTtls[resource] ?? this.cacheTtl;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve publisher slug, using default if not provided
|
|
74
|
+
*/
|
|
75
|
+
resolvePublisher(publisherSlug) {
|
|
76
|
+
return publisherSlug ?? this.defaultPublisher;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve game key, using default if not provided
|
|
80
|
+
*/
|
|
81
|
+
resolveGame(gameKey) {
|
|
82
|
+
return gameKey ?? this.defaultGame;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate that a publisher is allowed by the configuration
|
|
86
|
+
*/
|
|
87
|
+
validatePublisher(publisherSlug) {
|
|
88
|
+
if (!this.allowedPublishers) return;
|
|
89
|
+
if (this.allowedPublishers.includes(publisherSlug)) return;
|
|
90
|
+
throw new core.RestrictedError(
|
|
91
|
+
`Publisher '${publisherSlug}' is not in the allowed publishers list`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate that a game is allowed by the configuration
|
|
96
|
+
*/
|
|
97
|
+
validateGame(publisherSlug, gameKey) {
|
|
98
|
+
if (!this.allowedGames) return;
|
|
99
|
+
const allowedForPublisher = this.allowedGames[publisherSlug];
|
|
100
|
+
if (!allowedForPublisher) return;
|
|
101
|
+
if (allowedForPublisher.includes(gameKey)) return;
|
|
102
|
+
throw new core.RestrictedError(
|
|
103
|
+
`Game '${gameKey}' is not allowed for publisher '${publisherSlug}'`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validate both publisher and game access
|
|
108
|
+
*/
|
|
109
|
+
validateAccess(publisherSlug, gameKey) {
|
|
110
|
+
if (publisherSlug) {
|
|
111
|
+
this.validatePublisher(publisherSlug);
|
|
112
|
+
if (gameKey) {
|
|
113
|
+
this.validateGame(publisherSlug, gameKey);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if a log level should be logged
|
|
119
|
+
*/
|
|
120
|
+
shouldLog(level) {
|
|
121
|
+
const levels = {
|
|
122
|
+
debug: 0,
|
|
123
|
+
info: 1,
|
|
124
|
+
warn: 2,
|
|
125
|
+
error: 3
|
|
126
|
+
};
|
|
127
|
+
return levels[level] >= levels[this.logLevel];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Create a copy of this configuration with overrides
|
|
131
|
+
*/
|
|
132
|
+
merge(overrides) {
|
|
133
|
+
return new _Configuration({
|
|
134
|
+
apiKey: overrides.apiKey ?? this.apiKey,
|
|
135
|
+
endpoint: overrides.endpoint ?? this.endpoint,
|
|
136
|
+
timeout: overrides.timeout ?? this.timeout,
|
|
137
|
+
openTimeout: overrides.openTimeout ?? this.openTimeout,
|
|
138
|
+
defaultPublisher: overrides.defaultPublisher ?? this.defaultPublisher,
|
|
139
|
+
defaultGame: overrides.defaultGame ?? this.defaultGame,
|
|
140
|
+
allowedPublishers: overrides.allowedPublishers ?? this.allowedPublishers,
|
|
141
|
+
allowedGames: overrides.allowedGames ?? this.allowedGames,
|
|
142
|
+
logger: overrides.logger ?? this.logger,
|
|
143
|
+
logLevel: overrides.logLevel ?? this.logLevel,
|
|
144
|
+
retryOnRateLimit: overrides.retryOnRateLimit ?? this.retryOnRateLimit,
|
|
145
|
+
maxRetries: overrides.maxRetries ?? this.maxRetries,
|
|
146
|
+
retryOnNetworkError: overrides.retryOnNetworkError ?? this.retryOnNetworkError,
|
|
147
|
+
maxNetworkRetries: overrides.maxNetworkRetries ?? this.maxNetworkRetries,
|
|
148
|
+
retryBaseDelay: overrides.retryBaseDelay ?? this.retryBaseDelay,
|
|
149
|
+
retryMaxDelay: overrides.retryMaxDelay ?? this.retryMaxDelay,
|
|
150
|
+
cache: overrides.cache ?? this.cache,
|
|
151
|
+
cacheTtl: overrides.cacheTtl ?? this.cacheTtl,
|
|
152
|
+
cacheTtls: overrides.cacheTtls ?? this.cacheTtls
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/user-agent.ts
|
|
158
|
+
var SDK_VERSION = "0.1.0";
|
|
159
|
+
function detectRuntime() {
|
|
160
|
+
if (typeof globalThis !== "undefined" && "Bun" in globalThis) {
|
|
161
|
+
const bunVersion = globalThis.Bun && typeof globalThis.Bun === "object" && globalThis.Bun.version;
|
|
162
|
+
return {
|
|
163
|
+
name: "bun",
|
|
164
|
+
version: typeof bunVersion === "string" ? bunVersion : "unknown"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (typeof globalThis !== "undefined" && "Deno" in globalThis) {
|
|
168
|
+
const denoVersion = globalThis.Deno && typeof globalThis.Deno === "object" && globalThis.Deno.version && typeof globalThis.Deno.version === "object" && globalThis.Deno.version.deno;
|
|
169
|
+
return {
|
|
170
|
+
name: "deno",
|
|
171
|
+
version: typeof denoVersion === "string" ? denoVersion : "unknown"
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
175
|
+
return {
|
|
176
|
+
name: "node",
|
|
177
|
+
version: process.versions.node
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) {
|
|
181
|
+
return detectBrowser(navigator.userAgent);
|
|
182
|
+
}
|
|
183
|
+
return { name: "unknown", version: "unknown" };
|
|
184
|
+
}
|
|
185
|
+
function detectBrowser(userAgent) {
|
|
186
|
+
const edgeMatch = userAgent.match(/Edg\/(\d+(?:\.\d+)*)/);
|
|
187
|
+
if (edgeMatch) {
|
|
188
|
+
return { name: "edge", version: edgeMatch[1] };
|
|
189
|
+
}
|
|
190
|
+
const chromeMatch = userAgent.match(/Chrome\/(\d+(?:\.\d+)*)/);
|
|
191
|
+
if (chromeMatch && !userAgent.includes("Edg/")) {
|
|
192
|
+
return { name: "chrome", version: chromeMatch[1] };
|
|
193
|
+
}
|
|
194
|
+
const firefoxMatch = userAgent.match(/Firefox\/(\d+(?:\.\d+)*)/);
|
|
195
|
+
if (firefoxMatch) {
|
|
196
|
+
return { name: "firefox", version: firefoxMatch[1] };
|
|
197
|
+
}
|
|
198
|
+
const safariMatch = userAgent.match(/Version\/(\d+(?:\.\d+)*).*Safari/);
|
|
199
|
+
if (safariMatch) {
|
|
200
|
+
return { name: "safari", version: safariMatch[1] };
|
|
201
|
+
}
|
|
202
|
+
return { name: "browser", version: "unknown" };
|
|
203
|
+
}
|
|
204
|
+
function getUserAgent() {
|
|
205
|
+
const runtime = detectRuntime();
|
|
206
|
+
return `CardDB-SDK/js/${SDK_VERSION} (${runtime.name}/${runtime.version})`;
|
|
207
|
+
}
|
|
208
|
+
function getSDKVersion() {
|
|
209
|
+
return SDK_VERSION;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/connection.ts
|
|
213
|
+
var lastRateLimitInfo = null;
|
|
214
|
+
function getRateLimitInfo() {
|
|
215
|
+
return lastRateLimitInfo;
|
|
216
|
+
}
|
|
217
|
+
var Connection = class {
|
|
218
|
+
config;
|
|
219
|
+
constructor(config) {
|
|
220
|
+
this.config = config;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Execute a GraphQL query
|
|
224
|
+
*/
|
|
225
|
+
async execute(query, variables = {}) {
|
|
226
|
+
const operationName = this.extractOperationName(query);
|
|
227
|
+
let rateLimitRetries = 0;
|
|
228
|
+
let networkRetries = 0;
|
|
229
|
+
while (true) {
|
|
230
|
+
try {
|
|
231
|
+
const startTime = Date.now();
|
|
232
|
+
this.logRequest(operationName, variables);
|
|
233
|
+
const response = await this.fetch(query, variables);
|
|
234
|
+
const duration = Date.now() - startTime;
|
|
235
|
+
const result = await this.handleResponse(response);
|
|
236
|
+
this.logResponse(operationName, duration);
|
|
237
|
+
return result;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error instanceof core.RateLimitError) {
|
|
240
|
+
if (this.config.retryOnRateLimit && rateLimitRetries < this.config.maxRetries) {
|
|
241
|
+
rateLimitRetries++;
|
|
242
|
+
const sleepTime = error.retryAfter ?? 60;
|
|
243
|
+
this.logRateLimitRetry(operationName, rateLimitRetries, sleepTime);
|
|
244
|
+
await this.sleep(sleepTime * 1e3);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (this.isRetryableNetworkError(error)) {
|
|
249
|
+
if (this.config.retryOnNetworkError && networkRetries < this.config.maxNetworkRetries) {
|
|
250
|
+
const delay = this.config.calculateRetryDelay(networkRetries);
|
|
251
|
+
this.logNetworkRetry(operationName, networkRetries + 1, delay, error);
|
|
252
|
+
networkRetries++;
|
|
253
|
+
await this.sleep(delay);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (error instanceof core.ServerError) {
|
|
258
|
+
if (this.config.retryOnNetworkError && networkRetries < this.config.maxNetworkRetries) {
|
|
259
|
+
const delay = this.config.calculateRetryDelay(networkRetries);
|
|
260
|
+
this.logNetworkRetry(operationName, networkRetries + 1, delay, error);
|
|
261
|
+
networkRetries++;
|
|
262
|
+
await this.sleep(delay);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check if an error is a retryable network error
|
|
272
|
+
*/
|
|
273
|
+
isRetryableNetworkError(error) {
|
|
274
|
+
return error instanceof core.ConnectionError || error instanceof core.TimeoutError;
|
|
275
|
+
}
|
|
276
|
+
async fetch(query, variables) {
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
279
|
+
try {
|
|
280
|
+
const response = await fetch(this.config.endpoint, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/json",
|
|
284
|
+
Accept: "application/json",
|
|
285
|
+
"User-Agent": getUserAgent(),
|
|
286
|
+
...this.config.apiKey ? { "X-API-Key": this.config.apiKey } : {}
|
|
287
|
+
},
|
|
288
|
+
body: JSON.stringify({ query, variables }),
|
|
289
|
+
signal: controller.signal
|
|
290
|
+
});
|
|
291
|
+
return response;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error instanceof Error) {
|
|
294
|
+
if (error.name === "AbortError") {
|
|
295
|
+
this.logError("Request timed out");
|
|
296
|
+
throw new core.TimeoutError("Request timed out");
|
|
297
|
+
}
|
|
298
|
+
this.logError(`Connection failed: ${error.message}`);
|
|
299
|
+
throw new core.ConnectionError(`Connection failed: ${error.message}`);
|
|
300
|
+
}
|
|
301
|
+
throw new core.ConnectionError("Connection failed");
|
|
302
|
+
} finally {
|
|
303
|
+
clearTimeout(timeoutId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async handleResponse(response) {
|
|
307
|
+
this.extractRateLimitInfo(response);
|
|
308
|
+
switch (response.status) {
|
|
309
|
+
case 200:
|
|
310
|
+
return this.handleSuccessResponse(response);
|
|
311
|
+
case 401:
|
|
312
|
+
throw new core.AuthenticationError("Invalid or missing API key");
|
|
313
|
+
case 429:
|
|
314
|
+
return this.handleRateLimitResponse(response);
|
|
315
|
+
default:
|
|
316
|
+
if (response.status >= 400 && response.status < 500) {
|
|
317
|
+
return this.handleClientError(response);
|
|
318
|
+
}
|
|
319
|
+
if (response.status >= 500) {
|
|
320
|
+
const body = await this.parseBody(response);
|
|
321
|
+
throw new core.ServerError(`Server error (${response.status})`, {
|
|
322
|
+
status: response.status,
|
|
323
|
+
response: body
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
throw new core.CardDBError(`Unexpected response status: ${response.status}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async handleSuccessResponse(response) {
|
|
330
|
+
const body = await this.parseBody(response);
|
|
331
|
+
if (body.errors && body.errors.length > 0) {
|
|
332
|
+
this.handleGraphQLErrors(body.errors, body);
|
|
333
|
+
}
|
|
334
|
+
return body.data ?? {};
|
|
335
|
+
}
|
|
336
|
+
handleGraphQLErrors(errors, body) {
|
|
337
|
+
const firstError = errors[0];
|
|
338
|
+
const message = firstError.message || "GraphQL error";
|
|
339
|
+
const code = firstError.extensions?.["code"];
|
|
340
|
+
switch (code) {
|
|
341
|
+
case "UNAUTHENTICATED":
|
|
342
|
+
throw new core.AuthenticationError(message, body);
|
|
343
|
+
case "RATE_LIMITED":
|
|
344
|
+
throw new core.RateLimitError(message, { response: body });
|
|
345
|
+
case "NOT_FOUND":
|
|
346
|
+
throw new core.CardDBError(message, body);
|
|
347
|
+
case "VALIDATION_ERROR":
|
|
348
|
+
case "BAD_USER_INPUT":
|
|
349
|
+
throw new core.ValidationError(message, { errors, response: body });
|
|
350
|
+
default:
|
|
351
|
+
throw new core.GraphQLError(message, { errors, response: body });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async handleRateLimitResponse(response) {
|
|
355
|
+
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "60", 10);
|
|
356
|
+
const limit = parseInt(response.headers.get("X-RateLimit-Limit") ?? "", 10) || null;
|
|
357
|
+
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") ?? "", 10) || null;
|
|
358
|
+
const reset = parseInt(response.headers.get("X-RateLimit-Reset") ?? "", 10) || null;
|
|
359
|
+
const resetAt = reset ? new Date(reset * 1e3) : null;
|
|
360
|
+
const body = await this.parseBody(response);
|
|
361
|
+
throw new core.RateLimitError("Rate limit exceeded", {
|
|
362
|
+
retryAfter,
|
|
363
|
+
limit,
|
|
364
|
+
remaining,
|
|
365
|
+
resetAt,
|
|
366
|
+
response: body
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async handleClientError(response) {
|
|
370
|
+
const body = await this.parseBody(response);
|
|
371
|
+
const message = body.errors?.[0]?.message ?? `Client error (${response.status})`;
|
|
372
|
+
throw new core.ValidationError(message, { response: body });
|
|
373
|
+
}
|
|
374
|
+
async parseBody(response) {
|
|
375
|
+
try {
|
|
376
|
+
return await response.json();
|
|
377
|
+
} catch {
|
|
378
|
+
return { raw: await response.text() };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
extractRateLimitInfo(response) {
|
|
382
|
+
const limit = parseInt(response.headers.get("X-RateLimit-Limit") ?? "", 10) || null;
|
|
383
|
+
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") ?? "", 10) || null;
|
|
384
|
+
const reset = parseInt(response.headers.get("X-RateLimit-Reset") ?? "", 10) || null;
|
|
385
|
+
lastRateLimitInfo = { limit, remaining, reset };
|
|
386
|
+
}
|
|
387
|
+
extractOperationName(query) {
|
|
388
|
+
const match = query.match(/(?:query|mutation)\s+(\w+)/);
|
|
389
|
+
return match ? match[1] : "Unknown";
|
|
390
|
+
}
|
|
391
|
+
sleep(ms) {
|
|
392
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
393
|
+
}
|
|
394
|
+
// Logging helpers
|
|
395
|
+
logRequest(operationName, variables) {
|
|
396
|
+
if (!this.config.logger || !this.config.shouldLog("debug")) return;
|
|
397
|
+
this.config.logger.debug(`[CardDB] Executing ${operationName}`, { variables });
|
|
398
|
+
}
|
|
399
|
+
logResponse(operationName, duration) {
|
|
400
|
+
if (!this.config.logger || !this.config.shouldLog("info")) return;
|
|
401
|
+
this.config.logger.info(`[CardDB] ${operationName} completed in ${duration}ms`);
|
|
402
|
+
}
|
|
403
|
+
logRateLimitRetry(operationName, retry, sleepTime) {
|
|
404
|
+
if (!this.config.logger || !this.config.shouldLog("warn")) return;
|
|
405
|
+
this.config.logger.warn(
|
|
406
|
+
`[CardDB] Rate limited on ${operationName}, retry ${retry}/${this.config.maxRetries} after ${sleepTime}s`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
logNetworkRetry(operationName, retry, delay, error) {
|
|
410
|
+
if (!this.config.logger || !this.config.shouldLog("warn")) return;
|
|
411
|
+
const errorType = error instanceof Error ? error.name : "Unknown";
|
|
412
|
+
this.config.logger.warn(
|
|
413
|
+
`[CardDB] ${errorType} on ${operationName}, retry ${retry}/${this.config.maxNetworkRetries} after ${delay}ms`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
logError(message) {
|
|
417
|
+
if (!this.config.logger || !this.config.shouldLog("error")) return;
|
|
418
|
+
this.config.logger.error(`[CardDB] ${message}`);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// src/cache.ts
|
|
423
|
+
function cacheKey(resource, method, params) {
|
|
424
|
+
const sortedParams = Object.entries(params).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join("&");
|
|
425
|
+
return `carddb:${resource}:${method}:${sortedParams}`;
|
|
426
|
+
}
|
|
427
|
+
async function readCache(cache, key) {
|
|
428
|
+
const result = cache.get(key);
|
|
429
|
+
if (result instanceof Promise) {
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
async function writeCache(cache, key, value, ttl) {
|
|
435
|
+
const result = cache.set(key, value, ttl);
|
|
436
|
+
if (result instanceof Promise) {
|
|
437
|
+
await result;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/resources/base.ts
|
|
442
|
+
var BaseResource = class {
|
|
443
|
+
connection;
|
|
444
|
+
config;
|
|
445
|
+
constructor(connection, config) {
|
|
446
|
+
this.connection = connection;
|
|
447
|
+
this.config = config;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Resolve publisher slug, using default if not provided
|
|
451
|
+
*/
|
|
452
|
+
resolvePublisher(publisherSlug) {
|
|
453
|
+
const resolved = this.config.resolvePublisher(publisherSlug);
|
|
454
|
+
if (!resolved) {
|
|
455
|
+
throw new Error("publisher_slug is required (no default configured)");
|
|
456
|
+
}
|
|
457
|
+
return resolved;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Resolve game key, using default if not provided
|
|
461
|
+
*/
|
|
462
|
+
resolveGame(gameKey) {
|
|
463
|
+
const resolved = this.config.resolveGame(gameKey);
|
|
464
|
+
if (!resolved) {
|
|
465
|
+
throw new Error("game_key is required (no default configured)");
|
|
466
|
+
}
|
|
467
|
+
return resolved;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Validate access to publisher/game
|
|
471
|
+
*/
|
|
472
|
+
validateAccess(publisherSlug, gameKey) {
|
|
473
|
+
this.config.validateAccess(publisherSlug, gameKey);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Execute a query with optional caching
|
|
477
|
+
*/
|
|
478
|
+
async withCache(key, resource, options, fn) {
|
|
479
|
+
const useCache = options.cache ?? !!this.config.cache;
|
|
480
|
+
const cache = this.config.cache;
|
|
481
|
+
if (!useCache || !cache) {
|
|
482
|
+
return fn();
|
|
483
|
+
}
|
|
484
|
+
const cached = await readCache(cache, key);
|
|
485
|
+
if (cached !== null) {
|
|
486
|
+
return cached;
|
|
487
|
+
}
|
|
488
|
+
const result = await fn();
|
|
489
|
+
if (result !== null && result !== void 0) {
|
|
490
|
+
const ttl = this.config.cacheTtlFor(resource);
|
|
491
|
+
await writeCache(cache, key, result, ttl);
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Generate a cache key
|
|
497
|
+
*/
|
|
498
|
+
cacheKey(resource, method, params) {
|
|
499
|
+
return cacheKey(resource, method, params);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// src/resources/publishers.ts
|
|
504
|
+
var PublishersResource = class extends BaseResource {
|
|
505
|
+
constructor(connection, config) {
|
|
506
|
+
super(connection, config);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Search for publishers
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* const publishers = await client.publishers.search({ search: 'pokemon' })
|
|
513
|
+
* for await (const publisher of publishers) {
|
|
514
|
+
* console.log(publisher.name)
|
|
515
|
+
* }
|
|
516
|
+
*/
|
|
517
|
+
async search(options = {}) {
|
|
518
|
+
const { query, variables } = core.QueryBuilder.searchPublishers({
|
|
519
|
+
search: options.search,
|
|
520
|
+
first: options.first,
|
|
521
|
+
after: options.after
|
|
522
|
+
});
|
|
523
|
+
const key = this.cacheKey("publishers", "search", variables);
|
|
524
|
+
const data = await this.withCache(key, "publishers", options, async () => {
|
|
525
|
+
const result = await this.connection.execute(query, variables);
|
|
526
|
+
return result["searchPublishers"];
|
|
527
|
+
});
|
|
528
|
+
return new core.Collection(data, {
|
|
529
|
+
nextPageLoader: async (cursor) => {
|
|
530
|
+
return this.search({ ...options, after: cursor });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Get a single publisher by slug or ID
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* const publisher = await client.publishers.get('pokemon')
|
|
539
|
+
* const publisher = await client.publishers.get('550e8400-e29b-41d4-a716-446655440000')
|
|
540
|
+
*/
|
|
541
|
+
async get(slugOrId, options = {}) {
|
|
542
|
+
const isUUID = this.isUUID(slugOrId);
|
|
543
|
+
const key = this.cacheKey("publishers", "get", { [isUUID ? "id" : "slug"]: slugOrId });
|
|
544
|
+
return this.withCache(key, "publishers", options, async () => {
|
|
545
|
+
if (isUUID) {
|
|
546
|
+
const { query } = core.QueryBuilder.fetchPublisherById();
|
|
547
|
+
const result = await this.connection.execute(query, { id: slugOrId });
|
|
548
|
+
return result["fetchPublisher"] ?? null;
|
|
549
|
+
} else {
|
|
550
|
+
const { query } = core.QueryBuilder.fetchPublisherBySlug();
|
|
551
|
+
const result = await this.connection.execute(query, { slug: slugOrId });
|
|
552
|
+
return result["fetchPublisher"] ?? null;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Get multiple publishers by slug
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* const publishers = await client.publishers.getMany(['pokemon', 'wizards'])
|
|
561
|
+
*/
|
|
562
|
+
async getMany(slugs, options = {}) {
|
|
563
|
+
if (slugs.length === 0) return [];
|
|
564
|
+
const key = this.cacheKey("publishers", "getMany", { slugs });
|
|
565
|
+
return this.withCache(key, "publishers", options, async () => {
|
|
566
|
+
const { query } = core.QueryBuilder.fetchPublishers();
|
|
567
|
+
const result = await this.connection.execute(query, { slugs });
|
|
568
|
+
return result["fetchPublishers"] ?? [];
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Check if a string is a valid UUID
|
|
573
|
+
*/
|
|
574
|
+
isUUID(value) {
|
|
575
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
576
|
+
return uuidRegex.test(value);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
var GamesResource = class extends BaseResource {
|
|
580
|
+
constructor(connection, config) {
|
|
581
|
+
super(connection, config);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Search for games
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* // Search all games
|
|
588
|
+
* const games = await client.games.search({ search: 'trading card' })
|
|
589
|
+
*
|
|
590
|
+
* // Search games from a specific publisher
|
|
591
|
+
* const games = await client.games.search({ publisherSlug: 'pokemon' })
|
|
592
|
+
*/
|
|
593
|
+
async search(options = {}) {
|
|
594
|
+
const publisherSlug = this.config.resolvePublisher(options.publisherSlug);
|
|
595
|
+
if (publisherSlug) {
|
|
596
|
+
this.validateAccess(publisherSlug);
|
|
597
|
+
}
|
|
598
|
+
const { query, variables } = core.QueryBuilder.searchGames({
|
|
599
|
+
publisherSlug,
|
|
600
|
+
search: options.search,
|
|
601
|
+
first: options.first,
|
|
602
|
+
after: options.after
|
|
603
|
+
});
|
|
604
|
+
const key = this.cacheKey("games", "search", variables);
|
|
605
|
+
const data = await this.withCache(key, "games", options, async () => {
|
|
606
|
+
const result = await this.connection.execute(query, variables);
|
|
607
|
+
return result["searchGames"];
|
|
608
|
+
});
|
|
609
|
+
return new core.Collection(data, {
|
|
610
|
+
nextPageLoader: async (cursor) => {
|
|
611
|
+
return this.search({ ...options, after: cursor });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async get(idOrPublisherSlug, gameKeyOrOptions, options) {
|
|
616
|
+
if (typeof gameKeyOrOptions === "string") {
|
|
617
|
+
const publisherSlug = idOrPublisherSlug;
|
|
618
|
+
const gameKey = gameKeyOrOptions;
|
|
619
|
+
const opts = options ?? {};
|
|
620
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
621
|
+
const key = this.cacheKey("games", "getByKeys", { publisherSlug, gameKey });
|
|
622
|
+
return this.withCache(key, "games", opts, async () => {
|
|
623
|
+
const { query } = core.QueryBuilder.fetchGameByKeys();
|
|
624
|
+
const result = await this.connection.execute(query, { publisherSlug, gameKey });
|
|
625
|
+
return result["fetchGame"] ?? null;
|
|
626
|
+
});
|
|
627
|
+
} else {
|
|
628
|
+
const id = idOrPublisherSlug;
|
|
629
|
+
const opts = gameKeyOrOptions ?? {};
|
|
630
|
+
const key = this.cacheKey("games", "get", { id });
|
|
631
|
+
return this.withCache(key, "games", opts, async () => {
|
|
632
|
+
const { query } = core.QueryBuilder.fetchGameById();
|
|
633
|
+
const result = await this.connection.execute(query, { id });
|
|
634
|
+
return result["fetchGame"] ?? null;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Get multiple games by ID
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* const games = await client.games.getMany(['id1', 'id2', 'id3'])
|
|
643
|
+
*/
|
|
644
|
+
async getMany(ids, options = {}) {
|
|
645
|
+
if (ids.length === 0) return [];
|
|
646
|
+
const key = this.cacheKey("games", "getMany", { ids });
|
|
647
|
+
return this.withCache(key, "games", options, async () => {
|
|
648
|
+
const { query } = core.QueryBuilder.fetchGames();
|
|
649
|
+
const result = await this.connection.execute(query, { ids });
|
|
650
|
+
return result["fetchGames"] ?? [];
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
var DatasetsResource = class extends BaseResource {
|
|
655
|
+
constructor(connection, config) {
|
|
656
|
+
super(connection, config);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Search for datasets
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* // Search all datasets
|
|
663
|
+
* const datasets = await client.datasets.search({ search: 'cards' })
|
|
664
|
+
*
|
|
665
|
+
* // Search datasets from a specific publisher and game
|
|
666
|
+
* const datasets = await client.datasets.search({
|
|
667
|
+
* publisherSlug: 'pokemon',
|
|
668
|
+
* gameKey: 'tcg'
|
|
669
|
+
* })
|
|
670
|
+
*/
|
|
671
|
+
async search(options = {}) {
|
|
672
|
+
const publisherSlug = this.config.resolvePublisher(options.publisherSlug);
|
|
673
|
+
const gameKey = this.config.resolveGame(options.gameKey);
|
|
674
|
+
if (publisherSlug) {
|
|
675
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
676
|
+
}
|
|
677
|
+
const { query, variables } = core.QueryBuilder.searchDatasets({
|
|
678
|
+
publisherSlug,
|
|
679
|
+
gameKey,
|
|
680
|
+
search: options.search,
|
|
681
|
+
purpose: options.purpose,
|
|
682
|
+
first: options.first,
|
|
683
|
+
after: options.after
|
|
684
|
+
});
|
|
685
|
+
const key = this.cacheKey("datasets", "search", variables);
|
|
686
|
+
const data = await this.withCache(key, "datasets", options, async () => {
|
|
687
|
+
const result = await this.connection.execute(query, variables);
|
|
688
|
+
return result["searchDatasets"];
|
|
689
|
+
});
|
|
690
|
+
return new core.Collection(data, {
|
|
691
|
+
nextPageLoader: async (cursor) => {
|
|
692
|
+
return this.search({ ...options, after: cursor });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
async get(idOrPublisherSlug, gameKeyOrOptions, datasetKey, options) {
|
|
697
|
+
if (typeof gameKeyOrOptions === "string" && typeof datasetKey === "string") {
|
|
698
|
+
const publisherSlug = idOrPublisherSlug;
|
|
699
|
+
const gameKey = gameKeyOrOptions;
|
|
700
|
+
const opts = options ?? {};
|
|
701
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
702
|
+
const key = this.cacheKey("datasets", "getByKeys", { publisherSlug, gameKey, datasetKey });
|
|
703
|
+
return this.withCache(key, "datasets", opts, async () => {
|
|
704
|
+
const { query } = core.QueryBuilder.fetchDatasetByKeys();
|
|
705
|
+
const result = await this.connection.execute(query, { publisherSlug, gameKey, datasetKey });
|
|
706
|
+
return result["fetchDataset"] ?? null;
|
|
707
|
+
});
|
|
708
|
+
} else {
|
|
709
|
+
const id = idOrPublisherSlug;
|
|
710
|
+
const opts = gameKeyOrOptions ?? {};
|
|
711
|
+
const key = this.cacheKey("datasets", "get", { id });
|
|
712
|
+
return this.withCache(key, "datasets", opts, async () => {
|
|
713
|
+
const { query } = core.QueryBuilder.fetchDatasetById();
|
|
714
|
+
const result = await this.connection.execute(query, { id });
|
|
715
|
+
return result["fetchDataset"] ?? null;
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Get multiple datasets by ID
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* const datasets = await client.datasets.getMany(['id1', 'id2', 'id3'])
|
|
724
|
+
*/
|
|
725
|
+
async getMany(ids, options = {}) {
|
|
726
|
+
if (ids.length === 0) return [];
|
|
727
|
+
const key = this.cacheKey("datasets", "getMany", { ids });
|
|
728
|
+
return this.withCache(key, "datasets", options, async () => {
|
|
729
|
+
const { query } = core.QueryBuilder.fetchDatasets();
|
|
730
|
+
const result = await this.connection.execute(query, { ids });
|
|
731
|
+
return result["fetchDatasets"] ?? [];
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
var DecksResource = class extends BaseResource {
|
|
736
|
+
constructor(connection, config) {
|
|
737
|
+
super(connection, config);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Fetch a hosted deck by CardDB UUID.
|
|
741
|
+
*/
|
|
742
|
+
async fetch(id, options = {}) {
|
|
743
|
+
const key = this.cacheKey("decks", "fetch", { id });
|
|
744
|
+
return this.withCache(key, "decks", options, async () => {
|
|
745
|
+
const { query } = core.QueryBuilder.fetchDeckById();
|
|
746
|
+
const result = await this.connection.execute(query, { id });
|
|
747
|
+
return result["fetchDeck"] ?? null;
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Fetch a hosted deck by external reference for the current API application.
|
|
752
|
+
*/
|
|
753
|
+
async fetchByExternalRef(externalRef, options = {}) {
|
|
754
|
+
const key = this.cacheKey("decks", "fetchByExternalRef", { externalRef });
|
|
755
|
+
return this.withCache(key, "decks", options, async () => {
|
|
756
|
+
const { query } = core.QueryBuilder.fetchDeckByExternalRef();
|
|
757
|
+
const result = await this.connection.execute(query, { externalRef });
|
|
758
|
+
return result["fetchDeckByExternalRef"] ?? null;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* List decks owned by the current account or API application.
|
|
763
|
+
*/
|
|
764
|
+
async listMine(options = {}) {
|
|
765
|
+
const { query, variables } = core.QueryBuilder.listMyDecks({
|
|
766
|
+
first: options.first,
|
|
767
|
+
after: options.after
|
|
768
|
+
});
|
|
769
|
+
const key = this.cacheKey("decks", "listMine", variables);
|
|
770
|
+
const data = await this.withCache(key, "decks", options, async () => {
|
|
771
|
+
const result = await this.connection.execute(query, variables);
|
|
772
|
+
return result["myDecks"];
|
|
773
|
+
});
|
|
774
|
+
return new core.Collection(data, {
|
|
775
|
+
nextPageLoader: async (cursor) => this.listMine({ ...options, after: cursor })
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Create a hosted deck.
|
|
780
|
+
*/
|
|
781
|
+
async create(input) {
|
|
782
|
+
const { query } = core.QueryBuilder.createDeck();
|
|
783
|
+
const variables = core.QueryBuilder.deckCreateVariables(input);
|
|
784
|
+
const result = await this.connection.execute(query, variables);
|
|
785
|
+
return result["deckCreate"];
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Update a hosted deck.
|
|
789
|
+
*/
|
|
790
|
+
async update(id, input) {
|
|
791
|
+
const { query } = core.QueryBuilder.updateDeck();
|
|
792
|
+
const variables = core.QueryBuilder.deckUpdateVariables(id, input);
|
|
793
|
+
const result = await this.connection.execute(query, variables);
|
|
794
|
+
return result["deckUpdate"];
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Delete a hosted deck.
|
|
798
|
+
*/
|
|
799
|
+
async delete(id) {
|
|
800
|
+
const { query } = core.QueryBuilder.deleteDeck();
|
|
801
|
+
const result = await this.connection.execute(query, { id });
|
|
802
|
+
return Boolean(result["deckDelete"]);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Hydrate third-party-owned deck entries without storing them in CardDB.
|
|
806
|
+
*/
|
|
807
|
+
async hydrateEntries(options) {
|
|
808
|
+
if (options.entries.length === 0) return [];
|
|
809
|
+
const publisherSlug = this.resolvePublisher(options.publisherSlug);
|
|
810
|
+
const gameKey = this.resolveGame(options.gameKey);
|
|
811
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
812
|
+
const identifiers = options.entries.map((entry) => entry.identifier);
|
|
813
|
+
const key = this.cacheKey("decks", "hydrateEntries", {
|
|
814
|
+
publisherSlug,
|
|
815
|
+
gameKey,
|
|
816
|
+
datasetKey: options.datasetKey,
|
|
817
|
+
identifiers
|
|
818
|
+
});
|
|
819
|
+
const records = await this.withCache(key, "decks", options, async () => {
|
|
820
|
+
const { query } = core.QueryBuilder.fetchRecordsByIdentifier();
|
|
821
|
+
const result = await this.connection.execute(query, {
|
|
822
|
+
publisherSlug,
|
|
823
|
+
gameKey,
|
|
824
|
+
datasetKey: options.datasetKey,
|
|
825
|
+
identifiers
|
|
826
|
+
});
|
|
827
|
+
return result["fetchRecordsByIdentifier"];
|
|
828
|
+
});
|
|
829
|
+
const recordsByIdentifier = recordsByDeckIdentifier(records, identifiers, options.identifierField);
|
|
830
|
+
return options.entries.map((entry) => ({
|
|
831
|
+
...entry,
|
|
832
|
+
record: recordsByIdentifier.get(entry.identifier) ?? null
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
function recordsByDeckIdentifier(records, identifiers, identifierField) {
|
|
837
|
+
const remaining = new Set(identifiers);
|
|
838
|
+
const recordsByIdentifier = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const record of records) {
|
|
840
|
+
const identifier = identifierField ? record.data[identifierField] : Object.values(record.data).find((value) => remaining.has(String(value)));
|
|
841
|
+
if (identifier === void 0 || identifier === null) continue;
|
|
842
|
+
const key = String(identifier);
|
|
843
|
+
if (!remaining.has(key)) continue;
|
|
844
|
+
recordsByIdentifier.set(key, record);
|
|
845
|
+
remaining.delete(key);
|
|
846
|
+
}
|
|
847
|
+
return recordsByIdentifier;
|
|
848
|
+
}
|
|
849
|
+
var RecordsResource = class extends BaseResource {
|
|
850
|
+
constructor(connection, config) {
|
|
851
|
+
super(connection, config);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Search for records in a dataset with filtering, search, and pagination
|
|
855
|
+
*
|
|
856
|
+
* @example Basic search
|
|
857
|
+
* const records = await client.records.search({
|
|
858
|
+
* publisherSlug: 'pokemon',
|
|
859
|
+
* gameKey: 'tcg',
|
|
860
|
+
* datasetKey: 'cards',
|
|
861
|
+
* })
|
|
862
|
+
*
|
|
863
|
+
* @example With defaults configured
|
|
864
|
+
* // If client has defaultPublisher: 'pokemon', defaultGame: 'tcg'
|
|
865
|
+
* const records = await client.records.search({ datasetKey: 'cards' })
|
|
866
|
+
*
|
|
867
|
+
* @example With filtering (object literal)
|
|
868
|
+
* const records = await client.records.search({
|
|
869
|
+
* datasetKey: 'cards',
|
|
870
|
+
* filter: {
|
|
871
|
+
* hp: { gte: 100 },
|
|
872
|
+
* types: { contains: 'Fire' }
|
|
873
|
+
* }
|
|
874
|
+
* })
|
|
875
|
+
*
|
|
876
|
+
* @example With filtering (builder function)
|
|
877
|
+
* const records = await client.records.search({
|
|
878
|
+
* datasetKey: 'cards',
|
|
879
|
+
* filter: f => f
|
|
880
|
+
* .where('hp', gte(100))
|
|
881
|
+
* .where('types', contains('Fire'))
|
|
882
|
+
* .any(or => or
|
|
883
|
+
* .where('rarity', 'Rare')
|
|
884
|
+
* .where('rarity', 'Rare Holo')
|
|
885
|
+
* )
|
|
886
|
+
* })
|
|
887
|
+
*
|
|
888
|
+
* @example With link resolution
|
|
889
|
+
* const records = await client.records.search({
|
|
890
|
+
* datasetKey: 'cards',
|
|
891
|
+
* resolveLinks: ['set_id'],
|
|
892
|
+
* filter: f => f.whereLink('set_id', { code: 'BASE' })
|
|
893
|
+
* })
|
|
894
|
+
*
|
|
895
|
+
* @example Iterate through all results
|
|
896
|
+
* const records = await client.records.search({ datasetKey: 'cards' })
|
|
897
|
+
* for await (const record of records) {
|
|
898
|
+
* console.log(record.data.name)
|
|
899
|
+
* }
|
|
900
|
+
*/
|
|
901
|
+
async search(options) {
|
|
902
|
+
const publisherSlug = this.resolvePublisher(options.publisherSlug);
|
|
903
|
+
const gameKey = this.resolveGame(options.gameKey);
|
|
904
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
905
|
+
const filter = core.resolveFilter(options.filter);
|
|
906
|
+
const { query, variables } = core.QueryBuilder.searchRecords({
|
|
907
|
+
publisherSlug,
|
|
908
|
+
gameKey,
|
|
909
|
+
datasetKey: options.datasetKey,
|
|
910
|
+
filter,
|
|
911
|
+
search: options.search,
|
|
912
|
+
orderBy: options.orderBy,
|
|
913
|
+
resolveLinks: options.resolveLinks,
|
|
914
|
+
first: options.first,
|
|
915
|
+
after: options.after,
|
|
916
|
+
validateSchema: options.validateSchema
|
|
917
|
+
});
|
|
918
|
+
const key = this.cacheKey("records", "search", variables);
|
|
919
|
+
const data = await this.withCache(key, "records", options, async () => {
|
|
920
|
+
const result = await this.connection.execute(query, variables);
|
|
921
|
+
return result["searchRecords"];
|
|
922
|
+
});
|
|
923
|
+
return new core.Collection(data, {
|
|
924
|
+
nextPageLoader: async (cursor) => {
|
|
925
|
+
return this.search({ ...options, after: cursor });
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Get a single record by ID
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* const record = await client.records.get('550e8400-e29b-41d4-a716-446655440000')
|
|
934
|
+
*/
|
|
935
|
+
async get(id, options = {}) {
|
|
936
|
+
const key = this.cacheKey("records", "get", { id });
|
|
937
|
+
return this.withCache(key, "records", options, async () => {
|
|
938
|
+
const { query } = core.QueryBuilder.fetchRecordById();
|
|
939
|
+
const result = await this.connection.execute(query, { id });
|
|
940
|
+
return result["fetchRecord"] ?? null;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Get a record by its identifier field value
|
|
945
|
+
*
|
|
946
|
+
* The identifier field is defined in the dataset schema (the field marked as isIdentifier)
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* const record = await client.records.getByIdentifier({
|
|
950
|
+
* publisherSlug: 'pokemon',
|
|
951
|
+
* gameKey: 'tcg',
|
|
952
|
+
* datasetKey: 'cards',
|
|
953
|
+
* identifier: 'base1-4'
|
|
954
|
+
* })
|
|
955
|
+
*
|
|
956
|
+
* @example With defaults configured
|
|
957
|
+
* const record = await client.records.getByIdentifier({
|
|
958
|
+
* datasetKey: 'cards',
|
|
959
|
+
* identifier: 'base1-4'
|
|
960
|
+
* })
|
|
961
|
+
*/
|
|
962
|
+
async getByIdentifier(options) {
|
|
963
|
+
const publisherSlug = this.resolvePublisher(options.publisherSlug);
|
|
964
|
+
const gameKey = this.resolveGame(options.gameKey);
|
|
965
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
966
|
+
const key = this.cacheKey("records", "getByIdentifier", {
|
|
967
|
+
publisherSlug,
|
|
968
|
+
gameKey,
|
|
969
|
+
datasetKey: options.datasetKey,
|
|
970
|
+
identifier: options.identifier
|
|
971
|
+
});
|
|
972
|
+
return this.withCache(key, "records", options, async () => {
|
|
973
|
+
const { query } = core.QueryBuilder.fetchRecordByIdentifier();
|
|
974
|
+
const result = await this.connection.execute(query, {
|
|
975
|
+
publisherSlug,
|
|
976
|
+
gameKey,
|
|
977
|
+
datasetKey: options.datasetKey,
|
|
978
|
+
identifier: options.identifier
|
|
979
|
+
});
|
|
980
|
+
return result["fetchRecordByIdentifier"] ?? null;
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Get multiple records by their identifier field values
|
|
985
|
+
*
|
|
986
|
+
* Missing identifiers are omitted from the result.
|
|
987
|
+
*
|
|
988
|
+
* @example
|
|
989
|
+
* const records = await client.records.getManyByIdentifier({
|
|
990
|
+
* publisherSlug: 'pokemon',
|
|
991
|
+
* gameKey: 'tcg',
|
|
992
|
+
* datasetKey: 'cards',
|
|
993
|
+
* identifiers: ['base1-4', 'base1-5']
|
|
994
|
+
* })
|
|
995
|
+
*
|
|
996
|
+
* @example With defaults configured
|
|
997
|
+
* const records = await client.records.getManyByIdentifier({
|
|
998
|
+
* datasetKey: 'cards',
|
|
999
|
+
* identifiers: ['base1-4', 'base1-5']
|
|
1000
|
+
* })
|
|
1001
|
+
*/
|
|
1002
|
+
async getManyByIdentifier(options) {
|
|
1003
|
+
if (options.identifiers.length === 0) return [];
|
|
1004
|
+
const publisherSlug = this.resolvePublisher(options.publisherSlug);
|
|
1005
|
+
const gameKey = this.resolveGame(options.gameKey);
|
|
1006
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
1007
|
+
const key = this.cacheKey("records", "getManyByIdentifier", {
|
|
1008
|
+
publisherSlug,
|
|
1009
|
+
gameKey,
|
|
1010
|
+
datasetKey: options.datasetKey,
|
|
1011
|
+
identifiers: options.identifiers
|
|
1012
|
+
});
|
|
1013
|
+
return this.withCache(key, "records", options, async () => {
|
|
1014
|
+
const { query } = core.QueryBuilder.fetchRecordsByIdentifier();
|
|
1015
|
+
const result = await this.connection.execute(query, {
|
|
1016
|
+
publisherSlug,
|
|
1017
|
+
gameKey,
|
|
1018
|
+
datasetKey: options.datasetKey,
|
|
1019
|
+
identifiers: options.identifiers
|
|
1020
|
+
});
|
|
1021
|
+
return result["fetchRecordsByIdentifier"] ?? [];
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Get multiple records by ID
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* const records = await client.records.getMany(['id1', 'id2', 'id3'])
|
|
1029
|
+
*/
|
|
1030
|
+
async getMany(ids, options = {}) {
|
|
1031
|
+
if (ids.length === 0) return [];
|
|
1032
|
+
const key = this.cacheKey("records", "getMany", { ids });
|
|
1033
|
+
return this.withCache(key, "records", options, async () => {
|
|
1034
|
+
const { query } = core.QueryBuilder.fetchRecords();
|
|
1035
|
+
const result = await this.connection.execute(query, { ids });
|
|
1036
|
+
return result["fetchRecords"] ?? [];
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
var RulesResource = class extends BaseResource {
|
|
1041
|
+
constructor(connection, config) {
|
|
1042
|
+
super(connection, config);
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* List rule-related datasets for a game.
|
|
1046
|
+
*
|
|
1047
|
+
* @example
|
|
1048
|
+
* const datasets = await client.rules.datasets({ publisherSlug: 'wotc', gameKey: 'magic' })
|
|
1049
|
+
*/
|
|
1050
|
+
async datasets(options = {}) {
|
|
1051
|
+
const publisherSlug = this.resolvePublisher(options.publisherSlug);
|
|
1052
|
+
const gameKey = this.resolveGame(options.gameKey);
|
|
1053
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
1054
|
+
const { query, variables } = core.QueryBuilder.searchDatasets({
|
|
1055
|
+
publisherSlug,
|
|
1056
|
+
gameKey,
|
|
1057
|
+
search: options.search,
|
|
1058
|
+
purpose: "RULES",
|
|
1059
|
+
first: options.first,
|
|
1060
|
+
after: options.after
|
|
1061
|
+
});
|
|
1062
|
+
const key = this.cacheKey("rules", "datasets", variables);
|
|
1063
|
+
const data = await this.withCache(key, "datasets", options, async () => {
|
|
1064
|
+
const result = await this.connection.execute(query, variables);
|
|
1065
|
+
return result["searchDatasets"];
|
|
1066
|
+
});
|
|
1067
|
+
return new core.Collection(data, {
|
|
1068
|
+
nextPageLoader: async (cursor) => this.datasets({ ...options, after: cursor })
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* List rules from the game's rules dataset.
|
|
1073
|
+
*
|
|
1074
|
+
* @example
|
|
1075
|
+
* const rules = await client.rules.list({ first: 100 })
|
|
1076
|
+
*/
|
|
1077
|
+
async list(options = {}) {
|
|
1078
|
+
return this.searchRuleRecords("rules", options);
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* List deck/game formats from the game's formats dataset.
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* const formats = await client.rules.formats({ first: 100 })
|
|
1085
|
+
* const names = formats.items.map(format => format.data.name)
|
|
1086
|
+
*/
|
|
1087
|
+
async formats(options = {}) {
|
|
1088
|
+
return this.searchRuleRecords(options.datasetKey ?? "formats", options);
|
|
1089
|
+
}
|
|
1090
|
+
async searchRuleRecords(defaultDatasetKey, options) {
|
|
1091
|
+
const publisherSlug = this.resolvePublisher(options.publisherSlug);
|
|
1092
|
+
const gameKey = this.resolveGame(options.gameKey);
|
|
1093
|
+
const datasetKey = options.datasetKey ?? defaultDatasetKey;
|
|
1094
|
+
this.validateAccess(publisherSlug, gameKey);
|
|
1095
|
+
const filter = core.resolveFilter(options.filter);
|
|
1096
|
+
const { query, variables } = core.QueryBuilder.searchRecords({
|
|
1097
|
+
publisherSlug,
|
|
1098
|
+
gameKey,
|
|
1099
|
+
datasetKey,
|
|
1100
|
+
filter,
|
|
1101
|
+
search: options.search,
|
|
1102
|
+
orderBy: options.orderBy,
|
|
1103
|
+
resolveLinks: options.resolveLinks,
|
|
1104
|
+
first: options.first,
|
|
1105
|
+
after: options.after,
|
|
1106
|
+
validateSchema: options.validateSchema
|
|
1107
|
+
});
|
|
1108
|
+
const key = this.cacheKey("rules", "records", variables);
|
|
1109
|
+
const data = await this.withCache(key, "records", options, async () => {
|
|
1110
|
+
const result = await this.connection.execute(query, variables);
|
|
1111
|
+
return result["searchRecords"];
|
|
1112
|
+
});
|
|
1113
|
+
return new core.Collection(data, {
|
|
1114
|
+
nextPageLoader: async (cursor) => this.searchRuleRecords(defaultDatasetKey, { ...options, after: cursor })
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
// src/client.ts
|
|
1120
|
+
var CardDBClient = class _CardDBClient {
|
|
1121
|
+
config;
|
|
1122
|
+
connection;
|
|
1123
|
+
/** Publishers resource for searching and fetching publishers */
|
|
1124
|
+
publishers;
|
|
1125
|
+
/** Games resource for searching and fetching games */
|
|
1126
|
+
games;
|
|
1127
|
+
/** Datasets resource for searching and fetching datasets */
|
|
1128
|
+
datasets;
|
|
1129
|
+
/** Records resource for searching and fetching records */
|
|
1130
|
+
records;
|
|
1131
|
+
/** Decks resource for hosted decks and external deck hydration */
|
|
1132
|
+
decks;
|
|
1133
|
+
/** Rules resource for game rules and deck formats */
|
|
1134
|
+
rules;
|
|
1135
|
+
/**
|
|
1136
|
+
* Create a new CardDB client
|
|
1137
|
+
*
|
|
1138
|
+
* @param config - Client configuration options
|
|
1139
|
+
*/
|
|
1140
|
+
constructor(config = {}) {
|
|
1141
|
+
this.config = new Configuration(config);
|
|
1142
|
+
this.connection = new Connection(this.config);
|
|
1143
|
+
this.publishers = new PublishersResource(this.connection, this.config);
|
|
1144
|
+
this.games = new GamesResource(this.connection, this.config);
|
|
1145
|
+
this.datasets = new DatasetsResource(this.connection, this.config);
|
|
1146
|
+
this.records = new RecordsResource(this.connection, this.config);
|
|
1147
|
+
this.decks = new DecksResource(this.connection, this.config);
|
|
1148
|
+
this.rules = new RulesResource(this.connection, this.config);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Get information about the current API key
|
|
1152
|
+
*
|
|
1153
|
+
* Returns null if no API key is configured or the key is invalid.
|
|
1154
|
+
*
|
|
1155
|
+
* @example
|
|
1156
|
+
* const info = await client.me()
|
|
1157
|
+
* if (info) {
|
|
1158
|
+
* console.log(`Logged in as: ${info.account.displayName}`)
|
|
1159
|
+
* console.log(`Application: ${info.application.name}`)
|
|
1160
|
+
* }
|
|
1161
|
+
*/
|
|
1162
|
+
async me() {
|
|
1163
|
+
const { query } = core.QueryBuilder.fetchMe();
|
|
1164
|
+
const result = await this.connection.execute(query);
|
|
1165
|
+
return result["fetchMe"] ?? null;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Get rate limit information from the last request
|
|
1169
|
+
*
|
|
1170
|
+
* @example
|
|
1171
|
+
* await client.publishers.search()
|
|
1172
|
+
* const rateLimit = client.getRateLimitInfo()
|
|
1173
|
+
* console.log(`Remaining: ${rateLimit?.remaining}/${rateLimit?.limit}`)
|
|
1174
|
+
*/
|
|
1175
|
+
getRateLimitInfo() {
|
|
1176
|
+
return getRateLimitInfo();
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Create a new client with configuration overrides
|
|
1180
|
+
*
|
|
1181
|
+
* Useful for creating scoped clients with different defaults.
|
|
1182
|
+
*
|
|
1183
|
+
* @example
|
|
1184
|
+
* const pokemonClient = client.withConfig({
|
|
1185
|
+
* defaultPublisher: 'pokemon',
|
|
1186
|
+
* defaultGame: 'tcg'
|
|
1187
|
+
* })
|
|
1188
|
+
*
|
|
1189
|
+
* // Now all queries use these defaults
|
|
1190
|
+
* const cards = await pokemonClient.records.search({ datasetKey: 'cards' })
|
|
1191
|
+
*/
|
|
1192
|
+
withConfig(overrides) {
|
|
1193
|
+
const mergedConfig = this.config.merge(overrides);
|
|
1194
|
+
return new _CardDBClient({
|
|
1195
|
+
apiKey: mergedConfig.apiKey,
|
|
1196
|
+
endpoint: mergedConfig.endpoint,
|
|
1197
|
+
timeout: mergedConfig.timeout,
|
|
1198
|
+
openTimeout: mergedConfig.openTimeout,
|
|
1199
|
+
defaultPublisher: mergedConfig.defaultPublisher,
|
|
1200
|
+
defaultGame: mergedConfig.defaultGame,
|
|
1201
|
+
allowedPublishers: mergedConfig.allowedPublishers,
|
|
1202
|
+
allowedGames: mergedConfig.allowedGames,
|
|
1203
|
+
logger: mergedConfig.logger,
|
|
1204
|
+
logLevel: mergedConfig.logLevel,
|
|
1205
|
+
retryOnRateLimit: mergedConfig.retryOnRateLimit,
|
|
1206
|
+
maxRetries: mergedConfig.maxRetries,
|
|
1207
|
+
retryOnNetworkError: mergedConfig.retryOnNetworkError,
|
|
1208
|
+
maxNetworkRetries: mergedConfig.maxNetworkRetries,
|
|
1209
|
+
retryBaseDelay: mergedConfig.retryBaseDelay,
|
|
1210
|
+
retryMaxDelay: mergedConfig.retryMaxDelay,
|
|
1211
|
+
cache: mergedConfig.cache,
|
|
1212
|
+
cacheTtl: mergedConfig.cacheTtl,
|
|
1213
|
+
cacheTtls: mergedConfig.cacheTtls
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
exports.BaseResource = BaseResource;
|
|
1219
|
+
exports.CardDBClient = CardDBClient;
|
|
1220
|
+
exports.Configuration = Configuration;
|
|
1221
|
+
exports.Connection = Connection;
|
|
1222
|
+
exports.DEFAULT_CACHE_TTL = DEFAULT_CACHE_TTL;
|
|
1223
|
+
exports.DEFAULT_ENDPOINT = DEFAULT_ENDPOINT;
|
|
1224
|
+
exports.DEFAULT_MAX_NETWORK_RETRIES = DEFAULT_MAX_NETWORK_RETRIES;
|
|
1225
|
+
exports.DEFAULT_MAX_RETRIES = DEFAULT_MAX_RETRIES;
|
|
1226
|
+
exports.DEFAULT_OPEN_TIMEOUT = DEFAULT_OPEN_TIMEOUT;
|
|
1227
|
+
exports.DEFAULT_RETRY_BASE_DELAY = DEFAULT_RETRY_BASE_DELAY;
|
|
1228
|
+
exports.DEFAULT_RETRY_MAX_DELAY = DEFAULT_RETRY_MAX_DELAY;
|
|
1229
|
+
exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
|
1230
|
+
exports.DatasetsResource = DatasetsResource;
|
|
1231
|
+
exports.DecksResource = DecksResource;
|
|
1232
|
+
exports.GamesResource = GamesResource;
|
|
1233
|
+
exports.PublishersResource = PublishersResource;
|
|
1234
|
+
exports.RecordsResource = RecordsResource;
|
|
1235
|
+
exports.RulesResource = RulesResource;
|
|
1236
|
+
exports.cacheKey = cacheKey;
|
|
1237
|
+
exports.getRateLimitInfo = getRateLimitInfo;
|
|
1238
|
+
exports.getSDKVersion = getSDKVersion;
|
|
1239
|
+
exports.getUserAgent = getUserAgent;
|
|
1240
|
+
exports.readCache = readCache;
|
|
1241
|
+
exports.writeCache = writeCache;
|
|
1242
|
+
Object.keys(core).forEach(function (k) {
|
|
1243
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
1244
|
+
enumerable: true,
|
|
1245
|
+
get: function () { return core[k]; }
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
//# sourceMappingURL=index.cjs.map
|
|
1249
|
+
//# sourceMappingURL=index.cjs.map
|