@buun_group/gunspec-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/dist/index.cjs ADDED
@@ -0,0 +1,2342 @@
1
+ 'use strict';
2
+
3
+ // src/core/auth.ts
4
+ function readEnvKey() {
5
+ try {
6
+ if (typeof process !== "undefined" && process.env) {
7
+ return process.env.GUNSPEC_API_KEY;
8
+ }
9
+ } catch {
10
+ }
11
+ try {
12
+ const g = globalThis;
13
+ if (typeof g.Deno !== "undefined" && g.Deno.env) {
14
+ return g.Deno.env.get("GUNSPEC_API_KEY");
15
+ }
16
+ } catch {
17
+ }
18
+ return void 0;
19
+ }
20
+ function resolveApiKey(config) {
21
+ if (config.apiKey !== void 0 && config.apiKey !== "") {
22
+ return config.apiKey;
23
+ }
24
+ return readEnvKey();
25
+ }
26
+ function buildAuthHeaders(apiKey) {
27
+ const headers = {};
28
+ if (apiKey !== void 0 && apiKey !== "") {
29
+ headers["X-API-Key"] = apiKey;
30
+ }
31
+ return headers;
32
+ }
33
+
34
+ // src/core/errors.ts
35
+ var GunSpecError = class extends Error {
36
+ name = "GunSpecError";
37
+ constructor(message) {
38
+ super(message);
39
+ Object.setPrototypeOf(this, new.target.prototype);
40
+ }
41
+ };
42
+ var APIError = class extends GunSpecError {
43
+ name = "APIError";
44
+ /** HTTP status code returned by the API. */
45
+ status;
46
+ /** Machine-readable error code from the response body (e.g. `"NOT_FOUND"`). */
47
+ code;
48
+ /** The `X-Request-Id` header value, useful for support requests. */
49
+ requestId;
50
+ /** Raw response headers for further inspection. */
51
+ headers;
52
+ constructor(status, code, message, requestId, headers) {
53
+ super(message);
54
+ this.status = status;
55
+ this.code = code;
56
+ this.requestId = requestId;
57
+ this.headers = headers;
58
+ }
59
+ };
60
+ var AuthenticationError = class extends APIError {
61
+ name = "AuthenticationError";
62
+ constructor(code, message, requestId, headers) {
63
+ super(401, code, message, requestId, headers);
64
+ }
65
+ };
66
+ var PermissionError = class extends APIError {
67
+ name = "PermissionError";
68
+ constructor(code, message, requestId, headers) {
69
+ super(403, code, message, requestId, headers);
70
+ }
71
+ };
72
+ var NotFoundError = class extends APIError {
73
+ name = "NotFoundError";
74
+ constructor(code, message, requestId, headers) {
75
+ super(404, code, message, requestId, headers);
76
+ }
77
+ };
78
+ var BadRequestError = class extends APIError {
79
+ name = "BadRequestError";
80
+ constructor(code, message, requestId, headers) {
81
+ super(400, code, message, requestId, headers);
82
+ }
83
+ };
84
+ var RateLimitError = class extends APIError {
85
+ name = "RateLimitError";
86
+ /**
87
+ * Number of seconds the client should wait before retrying, or `null` if
88
+ * the server did not provide a `Retry-After` header.
89
+ */
90
+ retryAfter;
91
+ constructor(code, message, requestId, headers, retryAfter) {
92
+ super(429, code, message, requestId, headers);
93
+ this.retryAfter = retryAfter;
94
+ }
95
+ };
96
+ var InternalServerError = class extends APIError {
97
+ name = "InternalServerError";
98
+ constructor(code, message, requestId, headers) {
99
+ super(500, code, message, requestId, headers);
100
+ }
101
+ };
102
+ var ConnectionError = class extends GunSpecError {
103
+ name = "ConnectionError";
104
+ /** The original error thrown by `fetch`. */
105
+ cause;
106
+ constructor(message, cause) {
107
+ super(message);
108
+ this.cause = cause;
109
+ }
110
+ };
111
+ var TimeoutError = class extends GunSpecError {
112
+ name = "TimeoutError";
113
+ /** The timeout duration in milliseconds that was exceeded. */
114
+ timeoutMs;
115
+ constructor(timeoutMs) {
116
+ super(`Request timed out after ${timeoutMs}ms`);
117
+ this.timeoutMs = timeoutMs;
118
+ }
119
+ };
120
+ function parseRetryAfter(headers) {
121
+ const raw = headers.get("Retry-After");
122
+ if (raw === null) return null;
123
+ const seconds = Number(raw);
124
+ if (!Number.isNaN(seconds) && seconds >= 0) return seconds;
125
+ const date = Date.parse(raw);
126
+ if (!Number.isNaN(date)) {
127
+ const delta = Math.ceil((date - Date.now()) / 1e3);
128
+ return delta > 0 ? delta : 0;
129
+ }
130
+ return null;
131
+ }
132
+ function createAPIError(status, body, requestId, headers) {
133
+ const code = body?.error?.code ?? `HTTP_${status}`;
134
+ const message = body?.error?.message ?? `Request failed with status ${status}`;
135
+ switch (status) {
136
+ case 400:
137
+ return new BadRequestError(code, message, requestId, headers);
138
+ case 401:
139
+ return new AuthenticationError(code, message, requestId, headers);
140
+ case 403:
141
+ return new PermissionError(code, message, requestId, headers);
142
+ case 404:
143
+ return new NotFoundError(code, message, requestId, headers);
144
+ case 429:
145
+ return new RateLimitError(code, message, requestId, headers, parseRetryAfter(headers));
146
+ case 500:
147
+ return new InternalServerError(code, message, requestId, headers);
148
+ default:
149
+ return new APIError(status, code, message, requestId, headers);
150
+ }
151
+ }
152
+
153
+ // src/core/retry.ts
154
+ var DEFAULTS = {
155
+ maxRetries: 2,
156
+ initialDelayMs: 500,
157
+ maxDelayMs: 8e3,
158
+ multiplier: 2
159
+ };
160
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
161
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
162
+ function resolveRetryConfig(config) {
163
+ return {
164
+ maxRetries: config?.maxRetries ?? DEFAULTS.maxRetries,
165
+ initialDelayMs: config?.initialDelayMs ?? DEFAULTS.initialDelayMs,
166
+ maxDelayMs: config?.maxDelayMs ?? DEFAULTS.maxDelayMs,
167
+ multiplier: config?.multiplier ?? DEFAULTS.multiplier
168
+ };
169
+ }
170
+ function isRetryable(error, method) {
171
+ if (!IDEMPOTENT_METHODS.has(method.toUpperCase())) {
172
+ return false;
173
+ }
174
+ if (error instanceof ConnectionError || error instanceof TimeoutError) {
175
+ return true;
176
+ }
177
+ if (error instanceof APIError) {
178
+ return RETRYABLE_STATUS_CODES.has(error.status);
179
+ }
180
+ return false;
181
+ }
182
+ function computeDelay(attempt, config, error) {
183
+ const base = config.initialDelayMs * Math.pow(config.multiplier, attempt);
184
+ const jitter = 1 + (Math.random() * 0.4 - 0.2);
185
+ let delay = Math.min(base * jitter, config.maxDelayMs);
186
+ if (error instanceof RateLimitError && error.retryAfter !== null) {
187
+ const retryAfterMs = error.retryAfter * 1e3;
188
+ delay = Math.max(delay, retryAfterMs);
189
+ }
190
+ return Math.round(delay);
191
+ }
192
+ function sleep(ms, signal) {
193
+ return new Promise((resolve, reject) => {
194
+ if (signal?.aborted) {
195
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
196
+ return;
197
+ }
198
+ const timer = setTimeout(resolve, ms);
199
+ signal?.addEventListener(
200
+ "abort",
201
+ () => {
202
+ clearTimeout(timer);
203
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
204
+ },
205
+ { once: true }
206
+ );
207
+ });
208
+ }
209
+ async function withRetry(fn, method, config, signal) {
210
+ let lastError;
211
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
212
+ try {
213
+ return await fn();
214
+ } catch (error) {
215
+ lastError = error;
216
+ const isLastAttempt = attempt === config.maxRetries;
217
+ if (isLastAttempt || !isRetryable(error, method)) {
218
+ throw error;
219
+ }
220
+ const delay = computeDelay(attempt, config, error);
221
+ await sleep(delay, signal);
222
+ }
223
+ }
224
+ throw lastError;
225
+ }
226
+
227
+ // src/core/http-client.ts
228
+ var DEFAULT_BASE_URL = "https://api.gunspec.io";
229
+ var DEFAULT_TIMEOUT_MS = 3e4;
230
+ var SDK_USER_AGENT = "@gunspec/sdk";
231
+ function serialiseQuery(params) {
232
+ const parts = [];
233
+ for (const [key, value] of Object.entries(params)) {
234
+ if (value === void 0) continue;
235
+ if (Array.isArray(value)) {
236
+ for (const item of value) {
237
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);
238
+ }
239
+ } else {
240
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
241
+ }
242
+ }
243
+ return parts.length > 0 ? `?${parts.join("&")}` : "";
244
+ }
245
+ function parseRateLimitHeaders(headers) {
246
+ const parse = (name) => {
247
+ const raw = headers.get(name);
248
+ if (raw === null) return null;
249
+ const num = Number(raw);
250
+ return Number.isNaN(num) ? null : num;
251
+ };
252
+ return {
253
+ limit: parse("X-RateLimit-Limit"),
254
+ remaining: parse("X-RateLimit-Remaining"),
255
+ reset: parse("X-RateLimit-Reset")
256
+ };
257
+ }
258
+ function extractRequestId(headers) {
259
+ return headers.get("X-Request-Id") ?? "";
260
+ }
261
+ function composeSignals(timeoutMs, externalSignal) {
262
+ const timeoutController = new AbortController();
263
+ const timer = setTimeout(() => timeoutController.abort(new TimeoutError(timeoutMs)), timeoutMs);
264
+ if (typeof AbortSignal !== "undefined" && "any" in AbortSignal) {
265
+ const signals = [timeoutController.signal];
266
+ if (externalSignal) signals.push(externalSignal);
267
+ const composed2 = AbortSignal.any(signals);
268
+ return {
269
+ signal: composed2,
270
+ cleanup: () => clearTimeout(timer)
271
+ };
272
+ }
273
+ const composed = new AbortController();
274
+ const onTimeoutAbort = () => composed.abort(timeoutController.signal.reason);
275
+ const onExternalAbort = () => {
276
+ if (externalSignal) composed.abort(externalSignal.reason);
277
+ };
278
+ timeoutController.signal.addEventListener("abort", onTimeoutAbort, { once: true });
279
+ externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
280
+ return {
281
+ signal: composed.signal,
282
+ cleanup: () => {
283
+ clearTimeout(timer);
284
+ timeoutController.signal.removeEventListener("abort", onTimeoutAbort);
285
+ externalSignal?.removeEventListener("abort", onExternalAbort);
286
+ }
287
+ };
288
+ }
289
+ var HttpClient = class {
290
+ baseUrl;
291
+ defaultTimeout;
292
+ defaultHeaders;
293
+ apiKey;
294
+ retryConfig;
295
+ constructor(config = {}) {
296
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
297
+ this.defaultTimeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
298
+ this.apiKey = resolveApiKey(config.auth ?? {});
299
+ this.retryConfig = resolveRetryConfig(config.retry);
300
+ this.defaultHeaders = {
301
+ "Accept": "application/json",
302
+ "User-Agent": SDK_USER_AGENT,
303
+ ...config.headers
304
+ };
305
+ }
306
+ /**
307
+ * Execute an HTTP request and return the unwrapped response.
308
+ *
309
+ * The API's `{ success: true, data: T }` envelope is stripped so that
310
+ * callers receive `T` directly via {@link APIResponse.data}.
311
+ *
312
+ * @typeParam T - The expected type of the `data` field in the response.
313
+ * @param config - Request configuration.
314
+ * @returns The unwrapped API response.
315
+ * @throws {@link APIError} on non-2xx responses.
316
+ * @throws {@link ConnectionError} on network failures.
317
+ * @throws {@link TimeoutError} when the request exceeds the timeout.
318
+ */
319
+ async request(config) {
320
+ return withRetry(
321
+ () => this.executeRequest(config),
322
+ config.method,
323
+ this.retryConfig,
324
+ config.signal
325
+ );
326
+ }
327
+ /**
328
+ * Execute an HTTP request and return a paginated response.
329
+ *
330
+ * The API's `{ success: true, data: T[], pagination }` envelope is
331
+ * unwrapped into a {@link PaginatedResponse}.
332
+ *
333
+ * @typeParam T - The element type of the paginated list.
334
+ * @param config - Request configuration.
335
+ * @returns The unwrapped paginated response.
336
+ * @throws {@link APIError} on non-2xx responses.
337
+ * @throws {@link ConnectionError} on network failures.
338
+ * @throws {@link TimeoutError} when the request exceeds the timeout.
339
+ */
340
+ async requestPaginated(config) {
341
+ return withRetry(
342
+ () => this.executePaginatedRequest(config),
343
+ config.method,
344
+ this.retryConfig,
345
+ config.signal
346
+ );
347
+ }
348
+ /**
349
+ * Convenience wrapper for a GET request returning a single resource.
350
+ */
351
+ async get(path, query) {
352
+ return this.request({ method: "GET", path, query });
353
+ }
354
+ /**
355
+ * Convenience wrapper for a GET request returning a paginated list.
356
+ */
357
+ async getPaginated(path, query) {
358
+ return this.requestPaginated({ method: "GET", path, query });
359
+ }
360
+ /**
361
+ * Convenience wrapper for a POST request.
362
+ */
363
+ async post(path, body, query) {
364
+ return this.request({ method: "POST", path, body, query });
365
+ }
366
+ /**
367
+ * Convenience wrapper for a PUT request.
368
+ */
369
+ async put(path, body, query) {
370
+ return this.request({ method: "PUT", path, body, query });
371
+ }
372
+ /**
373
+ * Convenience wrapper for a DELETE request.
374
+ */
375
+ async delete(path, query) {
376
+ return this.request({ method: "DELETE", path, query });
377
+ }
378
+ // -----------------------------------------------------------------------
379
+ // Internal execution
380
+ // -----------------------------------------------------------------------
381
+ async executeRequest(config) {
382
+ const response = await this.doFetch(config);
383
+ const requestId = extractRequestId(response.headers);
384
+ const rateLimit = parseRateLimitHeaders(response.headers);
385
+ if (!response.ok) {
386
+ await this.throwAPIError(response, requestId);
387
+ }
388
+ const json = await response.json();
389
+ return {
390
+ data: json.data,
391
+ status: response.status,
392
+ headers: response.headers,
393
+ requestId,
394
+ rateLimit
395
+ };
396
+ }
397
+ async executePaginatedRequest(config) {
398
+ const response = await this.doFetch(config);
399
+ const requestId = extractRequestId(response.headers);
400
+ const rateLimit = parseRateLimitHeaders(response.headers);
401
+ if (!response.ok) {
402
+ await this.throwAPIError(response, requestId);
403
+ }
404
+ const json = await response.json();
405
+ return {
406
+ data: json.data,
407
+ pagination: json.pagination,
408
+ status: response.status,
409
+ headers: response.headers,
410
+ requestId,
411
+ rateLimit
412
+ };
413
+ }
414
+ /**
415
+ * Perform the raw `fetch` call with merged headers, query string, timeout,
416
+ * and body serialisation.
417
+ */
418
+ async doFetch(config) {
419
+ const url = this.buildUrl(config.path, config.query);
420
+ const timeoutMs = config.timeout ?? this.defaultTimeout;
421
+ const { signal, cleanup } = composeSignals(timeoutMs, config.signal);
422
+ const headers = {
423
+ ...this.defaultHeaders,
424
+ ...buildAuthHeaders(this.apiKey),
425
+ ...config.headers
426
+ };
427
+ if (config.body !== void 0) {
428
+ headers["Content-Type"] = "application/json";
429
+ }
430
+ try {
431
+ const response = await fetch(url, {
432
+ method: config.method,
433
+ headers,
434
+ body: config.body !== void 0 ? JSON.stringify(config.body) : void 0,
435
+ signal
436
+ });
437
+ return response;
438
+ } catch (error) {
439
+ if (error instanceof TimeoutError) {
440
+ throw error;
441
+ }
442
+ if (error instanceof DOMException && error.name === "AbortError") {
443
+ if (config.signal?.aborted) {
444
+ throw error;
445
+ }
446
+ throw new TimeoutError(timeoutMs);
447
+ }
448
+ throw new ConnectionError(
449
+ error instanceof Error ? error.message : "Network request failed",
450
+ error
451
+ );
452
+ } finally {
453
+ cleanup();
454
+ }
455
+ }
456
+ /**
457
+ * Build the full URL from base URL, path, and query parameters.
458
+ */
459
+ buildUrl(path, query) {
460
+ const qs = query ? serialiseQuery(query) : "";
461
+ return `${this.baseUrl}${path}${qs}`;
462
+ }
463
+ /**
464
+ * Parse an error response body and throw the appropriate {@link APIError}.
465
+ */
466
+ async throwAPIError(response, requestId) {
467
+ let body = null;
468
+ try {
469
+ body = await response.json();
470
+ } catch {
471
+ }
472
+ throw createAPIError(response.status, body, requestId, response.headers);
473
+ }
474
+ };
475
+
476
+ // src/resources/firearms.ts
477
+ var FirearmsResource = class {
478
+ constructor(client) {
479
+ this.client = client;
480
+ }
481
+ // ---------------------------------------------------------------------------
482
+ // Collection endpoints
483
+ // ---------------------------------------------------------------------------
484
+ /**
485
+ * List firearms with optional filters and pagination.
486
+ *
487
+ * @param params - Optional query parameters for filtering, sorting, and pagination.
488
+ * @returns A paginated list of firearms matching the given filters.
489
+ * @throws {BadRequestError} If any filter value is invalid.
490
+ * @throws {AuthenticationError} If the API key is missing or invalid.
491
+ *
492
+ * @example
493
+ * ```typescript
494
+ * const result = await client.firearms.list({
495
+ * manufacturer: 'beretta',
496
+ * status: 'in_production',
497
+ * sort: 'name',
498
+ * order: 'asc',
499
+ * page: 1,
500
+ * per_page: 25,
501
+ * });
502
+ * console.log(result.data); // Firearm[]
503
+ * console.log(result.pagination.totalPages);
504
+ * ```
505
+ */
506
+ async list(params) {
507
+ return this.client.getPaginated("/v1/firearms", params);
508
+ }
509
+ /**
510
+ * Auto-paginate through all firearms matching the given filters.
511
+ *
512
+ * Returns an async iterator that fetches pages on demand, yielding
513
+ * individual {@link Firearm} objects. Useful for processing large
514
+ * result sets without managing pagination manually.
515
+ *
516
+ * @param params - Optional query parameters for filtering and sorting.
517
+ * @returns An async iterable iterator yielding individual firearms.
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * for await (const firearm of client.firearms.listAutoPaging({ manufacturer: 'colt' })) {
522
+ * console.log(firearm.name);
523
+ * }
524
+ * ```
525
+ */
526
+ async *listAutoPaging(params) {
527
+ let page = params?.page ?? 1;
528
+ while (true) {
529
+ const result = await this.list({ ...params, page });
530
+ for (const item of result.data) {
531
+ yield item;
532
+ }
533
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
534
+ page++;
535
+ }
536
+ }
537
+ /**
538
+ * Full-text search across firearms.
539
+ *
540
+ * @param params - Search query and pagination parameters.
541
+ * @returns A paginated list of firearms matching the search query.
542
+ * @throws {BadRequestError} If the search query is empty or too long.
543
+ * @throws {AuthenticationError} If the API key is missing or invalid.
544
+ *
545
+ * @example
546
+ * ```typescript
547
+ * const result = await client.firearms.search({ q: '9mm compact' });
548
+ * console.log(result.data.length);
549
+ * ```
550
+ */
551
+ async search(params) {
552
+ return this.client.getPaginated("/v1/firearms/search", params);
553
+ }
554
+ /**
555
+ * Compare up to 5 firearms side by side.
556
+ *
557
+ * @param params - Object containing comma-separated firearm IDs.
558
+ * @returns An array of firearms with full details for comparison.
559
+ * @throws {BadRequestError} If more than 5 IDs are provided or any ID is invalid.
560
+ * @throws {NotFoundError} If any of the specified firearms do not exist.
561
+ *
562
+ * @example
563
+ * ```typescript
564
+ * const { data } = await client.firearms.compare({ ids: 'glock-g17,sig-sauer-p320,beretta-92fs' });
565
+ * console.log(data.items.length); // 3
566
+ * ```
567
+ */
568
+ async compare(params) {
569
+ return this.client.get("/v1/firearms/compare", params);
570
+ }
571
+ /**
572
+ * Retrieve game metadata for firearms (archetypes, stat ranges, etc.).
573
+ *
574
+ * @param params - Optional archetype filter.
575
+ * @returns Game metadata including archetype definitions and stat ranges.
576
+ * @throws {AuthenticationError} If the API key is missing or invalid.
577
+ *
578
+ * @example
579
+ * ```typescript
580
+ * const { data } = await client.firearms.gameMeta({ archetype: 'sniper' });
581
+ * ```
582
+ */
583
+ async gameMeta(params) {
584
+ return this.client.get("/v1/firearms/game-meta", params);
585
+ }
586
+ /**
587
+ * List all known action types across the database.
588
+ *
589
+ * @returns An array of distinct action type strings.
590
+ * @throws {AuthenticationError} If the API key is missing or invalid.
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * const { data } = await client.firearms.actionTypes();
595
+ * // ['Semi-automatic', 'Bolt action', 'Lever action', ...]
596
+ * ```
597
+ */
598
+ async actionTypes() {
599
+ return this.client.get("/v1/firearms/action-types");
600
+ }
601
+ /**
602
+ * Get available filter options for the firearms list endpoint.
603
+ *
604
+ * Returns distinct values for manufacturers, calibers, categories,
605
+ * action types, countries, and statuses that can be used as filter values.
606
+ *
607
+ * @returns An object mapping filter fields to their available values.
608
+ * @throws {AuthenticationError} If the API key is missing or invalid.
609
+ *
610
+ * @example
611
+ * ```typescript
612
+ * const { data } = await client.firearms.filterOptions();
613
+ * console.log(data.manufacturers); // ['beretta', 'colt', ...]
614
+ * console.log(data.categories); // ['pistol', 'rifle', ...]
615
+ * ```
616
+ */
617
+ async filterOptions() {
618
+ return this.client.get("/v1/firearms/filter-options");
619
+ }
620
+ /**
621
+ * Get a random firearm, optionally filtered by category or country.
622
+ *
623
+ * @param params - Optional category and country filters.
624
+ * @returns A single randomly selected firearm.
625
+ * @throws {AuthenticationError} If the API key is missing or invalid.
626
+ *
627
+ * @example
628
+ * ```typescript
629
+ * const { data } = await client.firearms.random({ category: 'pistol' });
630
+ * console.log(data.name);
631
+ * ```
632
+ */
633
+ async random(params) {
634
+ return this.client.get("/v1/firearms/random", params);
635
+ }
636
+ /**
637
+ * Get top firearms ranked by a specific statistic.
638
+ *
639
+ * @param params - The stat to rank by and optional category/limit filters.
640
+ * @returns An ordered array of firearms ranked by the specified stat.
641
+ * @throws {BadRequestError} If the stat value is not a recognized ranking metric.
642
+ * @throws {AuthenticationError} If the API key is missing or invalid.
643
+ *
644
+ * @example
645
+ * ```typescript
646
+ * const { data } = await client.firearms.top({
647
+ * stat: 'lightest',
648
+ * category: 'pistol',
649
+ * limit: 5,
650
+ * });
651
+ * ```
652
+ */
653
+ async top(params) {
654
+ return this.client.get("/v1/firearms/top", params);
655
+ }
656
+ /**
657
+ * Compare two firearms in a head-to-head matchup.
658
+ *
659
+ * @param params - The slugs of the two firearms to compare.
660
+ * @returns A detailed head-to-head comparison with per-stat breakdowns.
661
+ * @throws {NotFoundError} If either firearm does not exist.
662
+ * @throws {AuthenticationError} If the API key is missing or invalid.
663
+ *
664
+ * @example
665
+ * ```typescript
666
+ * const { data } = await client.firearms.headToHead({ a: 'glock-g17', b: 'sig-sauer-p320' });
667
+ * console.log(data.winner);
668
+ * ```
669
+ */
670
+ async headToHead(params) {
671
+ return this.client.get("/v1/firearms/head-to-head", params);
672
+ }
673
+ /**
674
+ * Filter firearms by a specific feature.
675
+ *
676
+ * @param params - The feature name and optional category filter with pagination.
677
+ * @returns A paginated list of firearms that have the specified feature.
678
+ * @throws {BadRequestError} If the feature name is empty.
679
+ * @throws {AuthenticationError} If the API key is missing or invalid.
680
+ *
681
+ * @example
682
+ * ```typescript
683
+ * const result = await client.firearms.byFeature({ feature: 'threaded-barrel' });
684
+ * ```
685
+ */
686
+ async byFeature(params) {
687
+ return this.client.getPaginated("/v1/firearms/by-feature", params);
688
+ }
689
+ /**
690
+ * Filter firearms by action type.
691
+ *
692
+ * @param params - The action type string with pagination.
693
+ * @returns A paginated list of firearms with the specified action type.
694
+ * @throws {BadRequestError} If the action type is empty.
695
+ * @throws {AuthenticationError} If the API key is missing or invalid.
696
+ *
697
+ * @example
698
+ * ```typescript
699
+ * const result = await client.firearms.byAction({ action: 'semi-automatic' });
700
+ * ```
701
+ */
702
+ async byAction(params) {
703
+ return this.client.getPaginated("/v1/firearms/by-action", params);
704
+ }
705
+ /**
706
+ * Filter firearms by frame/component material.
707
+ *
708
+ * @param params - The material name, component type, and pagination.
709
+ * @returns A paginated list of firearms using the specified material.
710
+ * @throws {BadRequestError} If the material or component is invalid.
711
+ * @throws {AuthenticationError} If the API key is missing or invalid.
712
+ *
713
+ * @example
714
+ * ```typescript
715
+ * const result = await client.firearms.byMaterial({
716
+ * material: 'polymer',
717
+ * component: 'frame',
718
+ * });
719
+ * ```
720
+ */
721
+ async byMaterial(params) {
722
+ return this.client.getPaginated("/v1/firearms/by-material", params);
723
+ }
724
+ /**
725
+ * Filter firearms by designer name.
726
+ *
727
+ * @param params - The designer name string with pagination.
728
+ * @returns A paginated list of firearms designed by the specified person.
729
+ * @throws {BadRequestError} If the designer name is empty.
730
+ * @throws {AuthenticationError} If the API key is missing or invalid.
731
+ *
732
+ * @example
733
+ * ```typescript
734
+ * const result = await client.firearms.byDesigner({ designer: 'John Browning' });
735
+ * ```
736
+ */
737
+ async byDesigner(params) {
738
+ return this.client.getPaginated("/v1/firearms/by-designer", params);
739
+ }
740
+ /**
741
+ * Get firearms ranked by computed power rating.
742
+ *
743
+ * @param params - Optional category filter with pagination.
744
+ * @returns A paginated list of firearms with their power ratings.
745
+ * @throws {AuthenticationError} If the API key is missing or invalid.
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * const result = await client.firearms.powerRating({ category: 'rifle' });
750
+ * for (const item of result.data) {
751
+ * console.log(`${item.name}: ${item.rating}`);
752
+ * }
753
+ * ```
754
+ */
755
+ async powerRating(params) {
756
+ return this.client.getPaginated("/v1/firearms/power-rating", params);
757
+ }
758
+ /**
759
+ * Get firearms arranged in a chronological timeline.
760
+ *
761
+ * @param params - Optional year range, category filter, and pagination.
762
+ * @returns A paginated list of firearms ordered by introduction date.
763
+ * @throws {AuthenticationError} If the API key is missing or invalid.
764
+ *
765
+ * @example
766
+ * ```typescript
767
+ * const result = await client.firearms.timeline({ from: 1900, to: 1950 });
768
+ * ```
769
+ */
770
+ async timeline(params) {
771
+ return this.client.getPaginated("/v1/firearms/timeline", params);
772
+ }
773
+ /**
774
+ * Filter firearms by military conflict.
775
+ *
776
+ * @param params - The conflict identifier string with pagination.
777
+ * @returns A paginated list of firearms used in the specified conflict.
778
+ * @throws {BadRequestError} If the conflict name is empty.
779
+ * @throws {AuthenticationError} If the API key is missing or invalid.
780
+ *
781
+ * @example
782
+ * ```typescript
783
+ * const result = await client.firearms.byConflict({ conflict: 'world-war-ii' });
784
+ * ```
785
+ */
786
+ async byConflict(params) {
787
+ return this.client.getPaginated("/v1/firearms/by-conflict", params);
788
+ }
789
+ // ---------------------------------------------------------------------------
790
+ // Single-resource endpoints
791
+ // ---------------------------------------------------------------------------
792
+ /**
793
+ * Get a single firearm by its slug or ID.
794
+ *
795
+ * @param id - The firearm slug (e.g. `"glock-g17"`) or numeric ID.
796
+ * @returns The full firearm record with all available fields.
797
+ * @throws {NotFoundError} If no firearm matches the given identifier.
798
+ * @throws {AuthenticationError} If the API key is missing or invalid.
799
+ *
800
+ * @example
801
+ * ```typescript
802
+ * const { data } = await client.firearms.get('beretta-92fs');
803
+ * console.log(data.name); // "Beretta 92FS"
804
+ * console.log(data.manufacturerId); // "beretta"
805
+ * ```
806
+ */
807
+ async get(id) {
808
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}`);
809
+ }
810
+ /**
811
+ * Get all variants of a firearm.
812
+ *
813
+ * @param id - The parent firearm slug or ID.
814
+ * @returns An array of variant firearms derived from the parent.
815
+ * @throws {NotFoundError} If the parent firearm does not exist.
816
+ * @throws {AuthenticationError} If the API key is missing or invalid.
817
+ *
818
+ * @example
819
+ * ```typescript
820
+ * const { data } = await client.firearms.getVariants('colt-1911');
821
+ * console.log(data.map(v => v.name));
822
+ * ```
823
+ */
824
+ async getVariants(id) {
825
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/variants`);
826
+ }
827
+ /**
828
+ * Get images for a firearm.
829
+ *
830
+ * @param id - The firearm slug or ID.
831
+ * @returns An array of image records with URLs, dimensions, and metadata.
832
+ * @throws {NotFoundError} If the firearm does not exist.
833
+ * @throws {AuthenticationError} If the API key is missing or invalid.
834
+ *
835
+ * @example
836
+ * ```typescript
837
+ * const { data } = await client.firearms.getImages('sig-sauer-p226');
838
+ * for (const img of data) {
839
+ * console.log(img.url, img.width, img.height);
840
+ * }
841
+ * ```
842
+ */
843
+ async getImages(id) {
844
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/images`);
845
+ }
846
+ /**
847
+ * Get computed game statistics for a firearm.
848
+ *
849
+ * @param id - The firearm slug or ID.
850
+ * @returns Game-balanced stats including damage, accuracy, range, etc.
851
+ * @throws {NotFoundError} If the firearm does not exist.
852
+ * @throws {AuthenticationError} If the API key is missing or invalid.
853
+ *
854
+ * @example
855
+ * ```typescript
856
+ * const { data } = await client.firearms.getGameStats('ak-47');
857
+ * console.log(data.damage, data.accuracy, data.range);
858
+ * ```
859
+ */
860
+ async getGameStats(id) {
861
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/game-stats`);
862
+ }
863
+ /**
864
+ * Get physical dimensions for a firearm.
865
+ *
866
+ * @param id - The firearm slug or ID.
867
+ * @returns Detailed dimension data (length, height, width, barrel length, weight).
868
+ * @throws {NotFoundError} If the firearm does not exist.
869
+ * @throws {AuthenticationError} If the API key is missing or invalid.
870
+ *
871
+ * @example
872
+ * ```typescript
873
+ * const { data } = await client.firearms.getDimensions('glock-g19');
874
+ * console.log(`${data.overallLengthMm}mm overall`);
875
+ * ```
876
+ */
877
+ async getDimensions(id) {
878
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/dimensions`);
879
+ }
880
+ /**
881
+ * Get known military/law-enforcement adopters of a firearm.
882
+ *
883
+ * @param id - The firearm slug or ID.
884
+ * @returns An array of users/adopters with country and organization data.
885
+ * @throws {NotFoundError} If the firearm does not exist.
886
+ * @throws {AuthenticationError} If the API key is missing or invalid.
887
+ *
888
+ * @example
889
+ * ```typescript
890
+ * const { data } = await client.firearms.getUsers('beretta-m9');
891
+ * for (const user of data) {
892
+ * console.log(user.country, user.organization);
893
+ * }
894
+ * ```
895
+ */
896
+ async getUsers(id) {
897
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/users`);
898
+ }
899
+ /**
900
+ * Get the family tree (lineage) of a firearm.
901
+ *
902
+ * @param id - The firearm slug or ID.
903
+ * @returns A tree structure showing predecessors, descendants, and related models.
904
+ * @throws {NotFoundError} If the firearm does not exist.
905
+ * @throws {AuthenticationError} If the API key is missing or invalid.
906
+ *
907
+ * @example
908
+ * ```typescript
909
+ * const { data } = await client.firearms.getFamilyTree('m16a2');
910
+ * console.log(data.ancestors, data.descendants);
911
+ * ```
912
+ */
913
+ async getFamilyTree(id) {
914
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/family-tree`);
915
+ }
916
+ /**
917
+ * Get firearms similar to a given firearm.
918
+ *
919
+ * @param id - The firearm slug or ID.
920
+ * @returns An array of similar firearms ranked by similarity score.
921
+ * @throws {NotFoundError} If the firearm does not exist.
922
+ * @throws {AuthenticationError} If the API key is missing or invalid.
923
+ *
924
+ * @example
925
+ * ```typescript
926
+ * const { data } = await client.firearms.getSimilar('glock-g17');
927
+ * for (const match of data) {
928
+ * console.log(match.name, match.similarityScore);
929
+ * }
930
+ * ```
931
+ */
932
+ async getSimilar(id) {
933
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/similar`);
934
+ }
935
+ /**
936
+ * Get the worldwide adoption map for a firearm.
937
+ *
938
+ * @param id - The firearm slug or ID.
939
+ * @returns Adoption data by country with usage type and date ranges.
940
+ * @throws {NotFoundError} If the firearm does not exist.
941
+ * @throws {AuthenticationError} If the API key is missing or invalid.
942
+ *
943
+ * @example
944
+ * ```typescript
945
+ * const { data } = await client.firearms.getAdoptionMap('fn-fal');
946
+ * console.log(data.countries.length, 'countries adopted this firearm');
947
+ * ```
948
+ */
949
+ async getAdoptionMap(id) {
950
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/adoption-map`);
951
+ }
952
+ /**
953
+ * Get the full game profile for a firearm.
954
+ *
955
+ * @param id - The firearm slug or ID.
956
+ * @returns Game-specific profile including archetype, tier, and stat breakdown.
957
+ * @throws {NotFoundError} If the firearm does not exist.
958
+ * @throws {AuthenticationError} If the API key is missing or invalid.
959
+ *
960
+ * @example
961
+ * ```typescript
962
+ * const { data } = await client.firearms.getGameProfile('mp5');
963
+ * console.log(data.archetype, data.tier);
964
+ * ```
965
+ */
966
+ async getGameProfile(id) {
967
+ return this.client.get(`/v1/firearms/${encodeURIComponent(id)}/game-profile`);
968
+ }
969
+ /**
970
+ * Get an SVG silhouette (line art) of a firearm.
971
+ *
972
+ * @param id - The firearm slug or ID.
973
+ * @param params - Optional format and stroke customization parameters.
974
+ * @returns Silhouette data in the requested format (raw SVG, data URI, or JSON).
975
+ * @throws {NotFoundError} If the firearm does not exist or has no silhouette.
976
+ * @throws {AuthenticationError} If the API key is missing or invalid.
977
+ *
978
+ * @example
979
+ * ```typescript
980
+ * const { data } = await client.firearms.getSilhouette('ak-47', {
981
+ * format: 'datauri',
982
+ * stroke_width: 2,
983
+ * stroke_color: '#333',
984
+ * });
985
+ * // Use data.dataUri in an <img> tag
986
+ * ```
987
+ */
988
+ async getSilhouette(id, params) {
989
+ return this.client.get(
990
+ `/v1/firearms/${encodeURIComponent(id)}/silhouette`,
991
+ params
992
+ );
993
+ }
994
+ /**
995
+ * Calculate ballistics for a firearm with specific ammunition.
996
+ *
997
+ * @param id - The firearm slug or ID.
998
+ * @param params - Ammunition ID and optional ballistic parameters.
999
+ * @returns Calculated ballistic data including velocity, energy, and trajectory.
1000
+ * @throws {NotFoundError} If the firearm or ammunition does not exist.
1001
+ * @throws {BadRequestError} If the ammunition is incompatible with the firearm.
1002
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1003
+ *
1004
+ * @example
1005
+ * ```typescript
1006
+ * const { data } = await client.firearms.calculate('glock-g17', {
1007
+ * ammo_id: '9mm-federal-hst-124gr',
1008
+ * });
1009
+ * console.log(data.muzzleVelocity, data.muzzleEnergy);
1010
+ * ```
1011
+ */
1012
+ async calculate(id, params) {
1013
+ return this.client.get(
1014
+ `/v1/firearms/${encodeURIComponent(id)}/calculate`,
1015
+ params
1016
+ );
1017
+ }
1018
+ /**
1019
+ * Load a firearm with ammunition and get combined performance data.
1020
+ *
1021
+ * @param id - The firearm slug or ID.
1022
+ * @param params - Optional ammunition ID to load.
1023
+ * @returns Combined firearm + ammunition data with computed performance metrics.
1024
+ * @throws {NotFoundError} If the firearm does not exist.
1025
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1026
+ *
1027
+ * @example
1028
+ * ```typescript
1029
+ * const { data } = await client.firearms.load('sig-sauer-p226', {
1030
+ * ammo_id: '9mm-winchester-ranger-147gr',
1031
+ * });
1032
+ * ```
1033
+ */
1034
+ async load(id, params) {
1035
+ return this.client.get(
1036
+ `/v1/firearms/${encodeURIComponent(id)}/load`,
1037
+ params
1038
+ );
1039
+ }
1040
+ };
1041
+
1042
+ // src/resources/manufacturers.ts
1043
+ var ManufacturersResource = class {
1044
+ constructor(client) {
1045
+ this.client = client;
1046
+ }
1047
+ /**
1048
+ * List manufacturers with optional filters and pagination.
1049
+ *
1050
+ * @param params - Optional query parameters for filtering by country, sorting, and pagination.
1051
+ * @returns A paginated list of manufacturers.
1052
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1053
+ *
1054
+ * @example
1055
+ * ```typescript
1056
+ * const result = await client.manufacturers.list({
1057
+ * country: 'DE',
1058
+ * sort: 'name',
1059
+ * order: 'asc',
1060
+ * per_page: 50,
1061
+ * });
1062
+ * ```
1063
+ */
1064
+ async list(params) {
1065
+ return this.client.getPaginated("/v1/manufacturers", params);
1066
+ }
1067
+ /**
1068
+ * Auto-paginate through all manufacturers matching the given filters.
1069
+ *
1070
+ * Returns an async iterator that fetches pages on demand, yielding
1071
+ * individual {@link Manufacturer} objects.
1072
+ *
1073
+ * @param params - Optional query parameters for filtering and sorting.
1074
+ * @returns An async iterable iterator yielding individual manufacturers.
1075
+ *
1076
+ * @example
1077
+ * ```typescript
1078
+ * for await (const mfr of client.manufacturers.listAutoPaging()) {
1079
+ * console.log(mfr.name, mfr.country);
1080
+ * }
1081
+ * ```
1082
+ */
1083
+ async *listAutoPaging(params) {
1084
+ let page = params?.page ?? 1;
1085
+ while (true) {
1086
+ const result = await this.list({ ...params, page });
1087
+ for (const item of result.data) {
1088
+ yield item;
1089
+ }
1090
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
1091
+ page++;
1092
+ }
1093
+ }
1094
+ /**
1095
+ * Get a single manufacturer by its slug or ID.
1096
+ *
1097
+ * @param id - The manufacturer slug (e.g. `"beretta"`) or numeric ID.
1098
+ * @returns The full manufacturer record.
1099
+ * @throws {NotFoundError} If no manufacturer matches the given identifier.
1100
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1101
+ *
1102
+ * @example
1103
+ * ```typescript
1104
+ * const { data } = await client.manufacturers.get('sig-sauer');
1105
+ * console.log(data.name, data.foundedYear, data.country);
1106
+ * ```
1107
+ */
1108
+ async get(id) {
1109
+ return this.client.get(`/v1/manufacturers/${encodeURIComponent(id)}`);
1110
+ }
1111
+ /**
1112
+ * Get all firearms produced by a manufacturer.
1113
+ *
1114
+ * @param id - The manufacturer slug or ID.
1115
+ * @param params - Optional pagination parameters.
1116
+ * @returns A paginated list of firearms from the specified manufacturer.
1117
+ * @throws {NotFoundError} If the manufacturer does not exist.
1118
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1119
+ *
1120
+ * @example
1121
+ * ```typescript
1122
+ * const result = await client.manufacturers.getFirearms('colt', { per_page: 50 });
1123
+ * console.log(`Colt makes ${result.pagination.total} firearms`);
1124
+ * ```
1125
+ */
1126
+ async getFirearms(id, params) {
1127
+ return this.client.getPaginated(
1128
+ `/v1/manufacturers/${encodeURIComponent(id)}/firearms`,
1129
+ params
1130
+ );
1131
+ }
1132
+ /**
1133
+ * Get a chronological timeline for a manufacturer.
1134
+ *
1135
+ * @param id - The manufacturer slug or ID.
1136
+ * @returns Timeline data with key events, product launches, and milestones.
1137
+ * @throws {NotFoundError} If the manufacturer does not exist.
1138
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1139
+ *
1140
+ * @example
1141
+ * ```typescript
1142
+ * const { data } = await client.manufacturers.getTimeline('browning');
1143
+ * for (const event of data.events) {
1144
+ * console.log(event.year, event.description);
1145
+ * }
1146
+ * ```
1147
+ */
1148
+ async getTimeline(id) {
1149
+ return this.client.get(`/v1/manufacturers/${encodeURIComponent(id)}/timeline`);
1150
+ }
1151
+ /**
1152
+ * Get aggregate statistics for a manufacturer.
1153
+ *
1154
+ * @param id - The manufacturer slug or ID.
1155
+ * @returns Statistical summary including firearm counts, caliber distribution, and more.
1156
+ * @throws {NotFoundError} If the manufacturer does not exist.
1157
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1158
+ *
1159
+ * @example
1160
+ * ```typescript
1161
+ * const { data } = await client.manufacturers.getStats('glock');
1162
+ * console.log(data.totalFirearms, data.caliberBreakdown);
1163
+ * ```
1164
+ */
1165
+ async getStats(id) {
1166
+ return this.client.get(`/v1/manufacturers/${encodeURIComponent(id)}/stats`);
1167
+ }
1168
+ };
1169
+
1170
+ // src/resources/calibers.ts
1171
+ var CalibersResource = class {
1172
+ constructor(client) {
1173
+ this.client = client;
1174
+ }
1175
+ /**
1176
+ * List calibers with optional filters and pagination.
1177
+ *
1178
+ * @param params - Optional query parameters for filtering by cartridge type, primer type, etc.
1179
+ * @returns A paginated list of calibers.
1180
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1181
+ *
1182
+ * @example
1183
+ * ```typescript
1184
+ * const result = await client.calibers.list({
1185
+ * cartridge_type: 'centerfire',
1186
+ * primer_type: 'boxer',
1187
+ * per_page: 50,
1188
+ * });
1189
+ * ```
1190
+ */
1191
+ async list(params) {
1192
+ return this.client.getPaginated("/v1/calibers", params);
1193
+ }
1194
+ /**
1195
+ * Auto-paginate through all calibers matching the given filters.
1196
+ *
1197
+ * Returns an async iterator that fetches pages on demand, yielding
1198
+ * individual {@link Caliber} objects.
1199
+ *
1200
+ * @param params - Optional query parameters for filtering and sorting.
1201
+ * @returns An async iterable iterator yielding individual calibers.
1202
+ *
1203
+ * @example
1204
+ * ```typescript
1205
+ * for await (const caliber of client.calibers.listAutoPaging()) {
1206
+ * console.log(caliber.name, caliber.bulletDiameterMm);
1207
+ * }
1208
+ * ```
1209
+ */
1210
+ async *listAutoPaging(params) {
1211
+ let page = params?.page ?? 1;
1212
+ while (true) {
1213
+ const result = await this.list({ ...params, page });
1214
+ for (const item of result.data) {
1215
+ yield item;
1216
+ }
1217
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
1218
+ page++;
1219
+ }
1220
+ }
1221
+ /**
1222
+ * Compare up to 5 calibers side by side.
1223
+ *
1224
+ * @param params - Object containing comma-separated caliber IDs.
1225
+ * @returns An array of calibers with full details for comparison.
1226
+ * @throws {BadRequestError} If more than 5 IDs are provided.
1227
+ * @throws {NotFoundError} If any of the specified calibers do not exist.
1228
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1229
+ *
1230
+ * @example
1231
+ * ```typescript
1232
+ * const { data } = await client.calibers.compare({
1233
+ * ids: '9x19mm-parabellum,45-acp,40-s-w',
1234
+ * });
1235
+ * ```
1236
+ */
1237
+ async compare(params) {
1238
+ return this.client.get("/v1/calibers/compare", params);
1239
+ }
1240
+ /**
1241
+ * Get ballistics data for a caliber at a specified distance.
1242
+ *
1243
+ * @param params - Caliber ID and distance in meters.
1244
+ * @returns Ballistic data including velocity, energy, and drop at the specified distance.
1245
+ * @throws {NotFoundError} If the caliber does not exist.
1246
+ * @throws {BadRequestError} If the distance is out of range.
1247
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1248
+ *
1249
+ * @example
1250
+ * ```typescript
1251
+ * const { data } = await client.calibers.ballistics({
1252
+ * id: '9x19mm-parabellum',
1253
+ * distance: 100,
1254
+ * });
1255
+ * console.log(data.velocity, data.energy, data.drop);
1256
+ * ```
1257
+ */
1258
+ async ballistics(params) {
1259
+ return this.client.get("/v1/calibers/ballistics", params);
1260
+ }
1261
+ /**
1262
+ * Get a single caliber by its slug or ID.
1263
+ *
1264
+ * @param id - The caliber slug (e.g. `"9x19mm-parabellum"`) or numeric ID.
1265
+ * @returns The full caliber record with all specifications.
1266
+ * @throws {NotFoundError} If no caliber matches the given identifier.
1267
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1268
+ *
1269
+ * @example
1270
+ * ```typescript
1271
+ * const { data } = await client.calibers.get('45-acp');
1272
+ * console.log(data.name, data.bulletDiameterMm, data.caseLengthMm);
1273
+ * ```
1274
+ */
1275
+ async get(id) {
1276
+ return this.client.get(`/v1/calibers/${encodeURIComponent(id)}`);
1277
+ }
1278
+ /**
1279
+ * Get all firearms that use a specific caliber.
1280
+ *
1281
+ * @param id - The caliber slug or ID.
1282
+ * @param params - Optional pagination parameters.
1283
+ * @returns A paginated list of firearms chambered in the specified caliber.
1284
+ * @throws {NotFoundError} If the caliber does not exist.
1285
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1286
+ *
1287
+ * @example
1288
+ * ```typescript
1289
+ * const result = await client.calibers.getFirearms('9x19mm-parabellum', { per_page: 50 });
1290
+ * console.log(`${result.pagination.total} firearms use 9mm`);
1291
+ * ```
1292
+ */
1293
+ async getFirearms(id, params) {
1294
+ return this.client.getPaginated(
1295
+ `/v1/calibers/${encodeURIComponent(id)}/firearms`,
1296
+ params
1297
+ );
1298
+ }
1299
+ /**
1300
+ * Get the parent caliber chain (ancestry) for a caliber.
1301
+ *
1302
+ * @param id - The caliber slug or ID.
1303
+ * @returns An ordered array of calibers from the given caliber up to the root ancestor.
1304
+ * @throws {NotFoundError} If the caliber does not exist.
1305
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1306
+ *
1307
+ * @example
1308
+ * ```typescript
1309
+ * const { data } = await client.calibers.getParentChain('300-blackout');
1310
+ * // [300 Blackout, 5.56x45mm NATO, .223 Remington, ...]
1311
+ * ```
1312
+ */
1313
+ async getParentChain(id) {
1314
+ return this.client.get(`/v1/calibers/${encodeURIComponent(id)}/parent-chain`);
1315
+ }
1316
+ /**
1317
+ * Get the full family tree of related calibers.
1318
+ *
1319
+ * @param id - The caliber slug or ID.
1320
+ * @returns An array of calibers in the same family (parent, siblings, children).
1321
+ * @throws {NotFoundError} If the caliber does not exist.
1322
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1323
+ *
1324
+ * @example
1325
+ * ```typescript
1326
+ * const { data } = await client.calibers.getFamily('9x19mm-parabellum');
1327
+ * console.log(data.map(c => c.name));
1328
+ * ```
1329
+ */
1330
+ async getFamily(id) {
1331
+ return this.client.get(`/v1/calibers/${encodeURIComponent(id)}/family`);
1332
+ }
1333
+ /**
1334
+ * Get ammunition loads available for a caliber.
1335
+ *
1336
+ * @param id - The caliber slug or ID.
1337
+ * @param params - Optional pagination parameters.
1338
+ * @returns A paginated list of ammunition loads for the specified caliber.
1339
+ * @throws {NotFoundError} If the caliber does not exist.
1340
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1341
+ *
1342
+ * @example
1343
+ * ```typescript
1344
+ * const result = await client.calibers.getAmmunition('9x19mm-parabellum', { per_page: 25 });
1345
+ * for (const ammo of result.data) {
1346
+ * console.log(ammo.name, ammo.bulletWeightGrains);
1347
+ * }
1348
+ * ```
1349
+ */
1350
+ async getAmmunition(id, params) {
1351
+ return this.client.getPaginated(
1352
+ `/v1/calibers/${encodeURIComponent(id)}/ammunition`,
1353
+ params
1354
+ );
1355
+ }
1356
+ };
1357
+
1358
+ // src/resources/categories.ts
1359
+ var CategoriesResource = class {
1360
+ constructor(client) {
1361
+ this.client = client;
1362
+ }
1363
+ /**
1364
+ * List all firearm categories.
1365
+ *
1366
+ * @returns An array of all category records.
1367
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1368
+ *
1369
+ * @example
1370
+ * ```typescript
1371
+ * const { data } = await client.categories.list();
1372
+ * // [{ slug: 'pistol', name: 'Pistol' }, { slug: 'rifle', name: 'Rifle' }, ...]
1373
+ * ```
1374
+ */
1375
+ async list() {
1376
+ return this.client.get("/v1/categories");
1377
+ }
1378
+ /**
1379
+ * Get all firearms within a specific category.
1380
+ *
1381
+ * @param slug - The category slug (e.g. `"pistol"`, `"rifle"`, `"shotgun"`).
1382
+ * @param params - Optional pagination parameters.
1383
+ * @returns A paginated list of firearms in the specified category.
1384
+ * @throws {NotFoundError} If the category slug does not exist.
1385
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1386
+ *
1387
+ * @example
1388
+ * ```typescript
1389
+ * const result = await client.categories.getFirearms('shotgun', {
1390
+ * per_page: 25,
1391
+ * sort: 'name',
1392
+ * order: 'asc',
1393
+ * });
1394
+ * console.log(`${result.pagination.total} shotguns in database`);
1395
+ * ```
1396
+ */
1397
+ async getFirearms(slug, params) {
1398
+ return this.client.getPaginated(
1399
+ `/v1/categories/${encodeURIComponent(slug)}/firearms`,
1400
+ params
1401
+ );
1402
+ }
1403
+ };
1404
+
1405
+ // src/resources/stats.ts
1406
+ var StatsResource = class {
1407
+ constructor(client) {
1408
+ this.client = client;
1409
+ }
1410
+ /**
1411
+ * Get a high-level summary of the database.
1412
+ *
1413
+ * @returns Summary counts for firearms, manufacturers, calibers, and categories.
1414
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1415
+ *
1416
+ * @example
1417
+ * ```typescript
1418
+ * const { data } = await client.stats.summary();
1419
+ * console.log(`Database has ${data.totalFirearms} firearms`);
1420
+ * ```
1421
+ */
1422
+ async summary() {
1423
+ return this.client.get("/v1/stats/summary");
1424
+ }
1425
+ /**
1426
+ * Get firearm counts grouped by production status.
1427
+ *
1428
+ * @returns Counts for in-production, discontinued, and prototype firearms.
1429
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1430
+ *
1431
+ * @example
1432
+ * ```typescript
1433
+ * const { data } = await client.stats.productionStatus();
1434
+ * // { in_production: 342, discontinued: 891, prototype: 12 }
1435
+ * ```
1436
+ */
1437
+ async productionStatus() {
1438
+ return this.client.get("/v1/stats/production-status");
1439
+ }
1440
+ /**
1441
+ * Get field coverage statistics across the database.
1442
+ *
1443
+ * Shows the percentage of firearms that have data for each field,
1444
+ * useful for assessing data completeness.
1445
+ *
1446
+ * @returns Per-field coverage percentages.
1447
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1448
+ *
1449
+ * @example
1450
+ * ```typescript
1451
+ * const { data } = await client.stats.fieldCoverage();
1452
+ * console.log(`Weight field: ${data.weight}% coverage`);
1453
+ * ```
1454
+ */
1455
+ async fieldCoverage() {
1456
+ return this.client.get("/v1/stats/field-coverage");
1457
+ }
1458
+ /**
1459
+ * Get the most popular calibers by firearm count.
1460
+ *
1461
+ * @param params - Optional limit parameter.
1462
+ * @returns An ordered list of calibers ranked by the number of firearms using them.
1463
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1464
+ *
1465
+ * @example
1466
+ * ```typescript
1467
+ * const { data } = await client.stats.popularCalibers({ limit: 10 });
1468
+ * for (const entry of data) {
1469
+ * console.log(`${entry.name}: ${entry.count} firearms`);
1470
+ * }
1471
+ * ```
1472
+ */
1473
+ async popularCalibers(params) {
1474
+ return this.client.get("/v1/stats/calibers/popular", params);
1475
+ }
1476
+ /**
1477
+ * Get the most prolific manufacturers by firearm count.
1478
+ *
1479
+ * @param params - Optional limit and category filter.
1480
+ * @returns An ordered list of manufacturers ranked by the number of firearms produced.
1481
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1482
+ *
1483
+ * @example
1484
+ * ```typescript
1485
+ * const { data } = await client.stats.prolificManufacturers({
1486
+ * limit: 10,
1487
+ * category: 'pistol',
1488
+ * });
1489
+ * ```
1490
+ */
1491
+ async prolificManufacturers(params) {
1492
+ return this.client.get("/v1/stats/manufacturers/prolific", params);
1493
+ }
1494
+ /**
1495
+ * Get firearm counts grouped by category.
1496
+ *
1497
+ * @returns Counts per category (pistol, rifle, shotgun, etc.).
1498
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1499
+ *
1500
+ * @example
1501
+ * ```typescript
1502
+ * const { data } = await client.stats.byCategory();
1503
+ * // [{ category: 'pistol', count: 512 }, { category: 'rifle', count: 234 }, ...]
1504
+ * ```
1505
+ */
1506
+ async byCategory() {
1507
+ return this.client.get("/v1/stats/by-category");
1508
+ }
1509
+ /**
1510
+ * Get firearm statistics for a specific decade/era.
1511
+ *
1512
+ * @param params - The decade string (e.g. `"1990s"`).
1513
+ * @returns Statistics for firearms introduced during the specified decade.
1514
+ * @throws {BadRequestError} If the decade format is invalid (must be like `"1990s"`).
1515
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1516
+ *
1517
+ * @example
1518
+ * ```typescript
1519
+ * const { data } = await client.stats.byEra({ decade: '1940s' });
1520
+ * ```
1521
+ */
1522
+ async byEra(params) {
1523
+ return this.client.get("/v1/stats/by-era", params);
1524
+ }
1525
+ /**
1526
+ * Get statistics about materials used across all firearms.
1527
+ *
1528
+ * @returns Material usage counts and percentages by component type.
1529
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1530
+ *
1531
+ * @example
1532
+ * ```typescript
1533
+ * const { data } = await client.stats.materials();
1534
+ * ```
1535
+ */
1536
+ async materials() {
1537
+ return this.client.get("/v1/stats/materials");
1538
+ }
1539
+ /**
1540
+ * Get firearm adoption statistics for a specific country.
1541
+ *
1542
+ * @param params - The country code (e.g. `"US"`, `"GB"`).
1543
+ * @returns Adoption data for the specified country.
1544
+ * @throws {BadRequestError} If the country code is invalid.
1545
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1546
+ *
1547
+ * @example
1548
+ * ```typescript
1549
+ * const { data } = await client.stats.adoptionByCountry({ code: 'US' });
1550
+ * ```
1551
+ */
1552
+ async adoptionByCountry(params) {
1553
+ return this.client.get("/v1/stats/adoption/by-country", params);
1554
+ }
1555
+ /**
1556
+ * Get firearm adoption statistics by usage type.
1557
+ *
1558
+ * @param params - The usage type (e.g. `"military"`, `"law_enforcement"`, `"civilian"`).
1559
+ * @returns Adoption data for the specified usage type.
1560
+ * @throws {BadRequestError} If the type is not a recognized usage category.
1561
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1562
+ *
1563
+ * @example
1564
+ * ```typescript
1565
+ * const { data } = await client.stats.adoptionByType({ type: 'military' });
1566
+ * ```
1567
+ */
1568
+ async adoptionByType(params) {
1569
+ return this.client.get("/v1/stats/adoption/by-type", params);
1570
+ }
1571
+ /**
1572
+ * Get firearm counts grouped by action type.
1573
+ *
1574
+ * @param params - Optional category filter.
1575
+ * @returns Counts per action type, optionally filtered by category.
1576
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1577
+ *
1578
+ * @example
1579
+ * ```typescript
1580
+ * const { data } = await client.stats.actionTypes({ category: 'rifle' });
1581
+ * ```
1582
+ */
1583
+ async actionTypes(params) {
1584
+ return this.client.get("/v1/stats/action-types", params);
1585
+ }
1586
+ /**
1587
+ * Get feature frequency statistics across the database.
1588
+ *
1589
+ * @param params - Optional category filter and result limit.
1590
+ * @returns An ordered list of features ranked by frequency of occurrence.
1591
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1592
+ *
1593
+ * @example
1594
+ * ```typescript
1595
+ * const { data } = await client.stats.featureFrequency({
1596
+ * category: 'pistol',
1597
+ * limit: 20,
1598
+ * });
1599
+ * ```
1600
+ */
1601
+ async featureFrequency(params) {
1602
+ return this.client.get("/v1/stats/feature-frequency", params);
1603
+ }
1604
+ /**
1605
+ * Get caliber popularity trends across historical eras.
1606
+ *
1607
+ * @param params - Optional from/to decade range (e.g. `"1940s"` to `"2020s"`).
1608
+ * @returns Caliber popularity data broken down by decade.
1609
+ * @throws {BadRequestError} If the decade format is invalid.
1610
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1611
+ *
1612
+ * @example
1613
+ * ```typescript
1614
+ * const { data } = await client.stats.caliberPopularityByEra({
1615
+ * from_decade: '1940s',
1616
+ * to_decade: '2020s',
1617
+ * });
1618
+ * ```
1619
+ */
1620
+ async caliberPopularityByEra(params) {
1621
+ return this.client.get("/v1/stats/caliber-popularity-by-era", params);
1622
+ }
1623
+ };
1624
+
1625
+ // src/resources/game.ts
1626
+ var GameResource = class {
1627
+ constructor(client) {
1628
+ this.client = client;
1629
+ }
1630
+ /**
1631
+ * Get a balance report showing outlier firearms.
1632
+ *
1633
+ * Identifies firearms whose game stats deviate significantly from
1634
+ * the average, useful for game balancing.
1635
+ *
1636
+ * @param params - Optional threshold for outlier detection (0-100).
1637
+ * @returns A balance report with overpowered and underpowered firearms.
1638
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1639
+ *
1640
+ * @example
1641
+ * ```typescript
1642
+ * const { data } = await client.game.balanceReport({ threshold: 15 });
1643
+ * console.log(data.length, 'firearms flagged as outliers');
1644
+ * ```
1645
+ */
1646
+ async balanceReport(params) {
1647
+ return this.client.get("/v1/game/balance-report", params);
1648
+ }
1649
+ /**
1650
+ * Get a tier list of firearms ranked by a specific stat.
1651
+ *
1652
+ * @param params - Optional category filter and stat to rank by.
1653
+ * @returns A tier list with firearms grouped into S/A/B/C/D/F tiers.
1654
+ * @throws {BadRequestError} If the stat is not a valid game stat.
1655
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1656
+ *
1657
+ * @example
1658
+ * ```typescript
1659
+ * const { data } = await client.game.tierList({
1660
+ * stat: 'accuracy',
1661
+ * category: 'rifle',
1662
+ * });
1663
+ * console.log('S-tier:', data.tiers.S.map(f => f.name));
1664
+ * ```
1665
+ */
1666
+ async tierList(params) {
1667
+ return this.client.get("/v1/game/tier-list", params);
1668
+ }
1669
+ /**
1670
+ * Get a game-balanced matchup between two firearms.
1671
+ *
1672
+ * @param params - The slugs of the two firearms to compare.
1673
+ * @returns A detailed matchup result with per-stat comparisons and a winner.
1674
+ * @throws {NotFoundError} If either firearm does not exist.
1675
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1676
+ *
1677
+ * @example
1678
+ * ```typescript
1679
+ * const { data } = await client.game.matchups({ a: 'ak-47', b: 'm4-carbine' });
1680
+ * console.log(data.winner, data.statComparisons);
1681
+ * ```
1682
+ */
1683
+ async matchups(params) {
1684
+ return this.client.get("/v1/game/matchups", params);
1685
+ }
1686
+ /**
1687
+ * Get a roster of firearms best suited for a specific game role.
1688
+ *
1689
+ * @param params - The role to query and optional count limit.
1690
+ * @returns A roster of firearms ranked by suitability for the given role.
1691
+ * @throws {BadRequestError} If the role is not a recognized game role.
1692
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1693
+ *
1694
+ * @example
1695
+ * ```typescript
1696
+ * const { data } = await client.game.roleRoster({
1697
+ * role: 'sniper',
1698
+ * count: 10,
1699
+ * });
1700
+ * for (const firearm of data) {
1701
+ * console.log(firearm.name, firearm.roleScore);
1702
+ * }
1703
+ * ```
1704
+ */
1705
+ async roleRoster(params) {
1706
+ return this.client.get("/v1/game/role-roster", params);
1707
+ }
1708
+ /**
1709
+ * Get the statistical distribution for a specific game stat.
1710
+ *
1711
+ * Shows how firearms are distributed across value ranges for a given
1712
+ * stat, useful for understanding the spread and identifying balance issues.
1713
+ *
1714
+ * @param params - The stat to analyze (e.g. `"damage"`, `"accuracy"`).
1715
+ * @returns Distribution data including histogram buckets and summary statistics.
1716
+ * @throws {BadRequestError} If the stat is not a valid game stat.
1717
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1718
+ *
1719
+ * @example
1720
+ * ```typescript
1721
+ * const { data } = await client.game.statDistribution({ stat: 'damage' });
1722
+ * console.log(data.mean, data.median, data.buckets);
1723
+ * ```
1724
+ */
1725
+ async statDistribution(params) {
1726
+ return this.client.get("/v1/game/stat-distribution", params);
1727
+ }
1728
+ };
1729
+
1730
+ // src/resources/game-stats.ts
1731
+ var GameStatsResource = class {
1732
+ constructor(client) {
1733
+ this.client = client;
1734
+ }
1735
+ /**
1736
+ * List all available game stats snapshot versions.
1737
+ *
1738
+ * @returns An array of version records with metadata about each snapshot.
1739
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1740
+ *
1741
+ * @example
1742
+ * ```typescript
1743
+ * const { data } = await client.gameStats.listVersions();
1744
+ * for (const version of data) {
1745
+ * console.log(version.version, version.createdAt, version.description);
1746
+ * }
1747
+ * ```
1748
+ */
1749
+ async listVersions() {
1750
+ return this.client.get("/v1/game-stats/versions");
1751
+ }
1752
+ /**
1753
+ * List all firearms in a specific game stats snapshot version.
1754
+ *
1755
+ * @param version - The snapshot version string (e.g. `"1.0.0"`).
1756
+ * @param params - Optional pagination parameters.
1757
+ * @returns A paginated list of firearms with their game stats for the given version.
1758
+ * @throws {NotFoundError} If the specified version does not exist.
1759
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1760
+ *
1761
+ * @example
1762
+ * ```typescript
1763
+ * const result = await client.gameStats.listFirearms('1.0.0', { per_page: 50 });
1764
+ * for (const firearm of result.data) {
1765
+ * console.log(firearm.name, firearm.damage);
1766
+ * }
1767
+ * ```
1768
+ */
1769
+ async listFirearms(version, params) {
1770
+ return this.client.getPaginated(
1771
+ `/v1/game-stats/versions/${encodeURIComponent(version)}/firearms`,
1772
+ params
1773
+ );
1774
+ }
1775
+ /**
1776
+ * Get a single firearm's game stats from a specific snapshot version.
1777
+ *
1778
+ * @param version - The snapshot version string (e.g. `"1.0.0"`).
1779
+ * @param id - The firearm slug or ID.
1780
+ * @returns The firearm's game stats as captured in the specified version.
1781
+ * @throws {NotFoundError} If the version or firearm does not exist in the snapshot.
1782
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1783
+ *
1784
+ * @example
1785
+ * ```typescript
1786
+ * const { data } = await client.gameStats.getFirearm('1.0.0', 'glock-g17');
1787
+ * console.log(data.damage, data.accuracy, data.range);
1788
+ * ```
1789
+ */
1790
+ async getFirearm(version, id) {
1791
+ return this.client.get(
1792
+ `/v1/game-stats/versions/${encodeURIComponent(version)}/firearms/${encodeURIComponent(id)}`
1793
+ );
1794
+ }
1795
+ };
1796
+
1797
+ // src/resources/ammunition.ts
1798
+ var AmmunitionResource = class {
1799
+ constructor(client) {
1800
+ this.client = client;
1801
+ }
1802
+ /**
1803
+ * List ammunition with optional filters and pagination.
1804
+ *
1805
+ * @param params - Optional query parameters for filtering by caliber, bullet type, etc.
1806
+ * @returns A paginated list of ammunition loads.
1807
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1808
+ *
1809
+ * @example
1810
+ * ```typescript
1811
+ * const result = await client.ammunition.list({
1812
+ * caliber_id: '9x19mm-parabellum',
1813
+ * bullet_type: 'hollow-point',
1814
+ * per_page: 25,
1815
+ * });
1816
+ * ```
1817
+ */
1818
+ async list(params) {
1819
+ return this.client.getPaginated("/v1/ammunition", params);
1820
+ }
1821
+ /**
1822
+ * Auto-paginate through all ammunition matching the given filters.
1823
+ *
1824
+ * Returns an async iterator that fetches pages on demand, yielding
1825
+ * individual {@link Ammunition} objects.
1826
+ *
1827
+ * @param params - Optional query parameters for filtering and sorting.
1828
+ * @returns An async iterable iterator yielding individual ammunition loads.
1829
+ *
1830
+ * @example
1831
+ * ```typescript
1832
+ * for await (const ammo of client.ammunition.listAutoPaging({ caliber_id: '45-acp' })) {
1833
+ * console.log(ammo.name, ammo.bulletWeightGrains);
1834
+ * }
1835
+ * ```
1836
+ */
1837
+ async *listAutoPaging(params) {
1838
+ let page = params?.page ?? 1;
1839
+ while (true) {
1840
+ const result = await this.list({ ...params, page });
1841
+ for (const item of result.data) {
1842
+ yield item;
1843
+ }
1844
+ if (!result.pagination.totalPages || page >= result.pagination.totalPages) break;
1845
+ page++;
1846
+ }
1847
+ }
1848
+ /**
1849
+ * Get a single ammunition load by its slug or ID.
1850
+ *
1851
+ * @param id - The ammunition slug (e.g. `"9mm-federal-hst-124gr"`) or numeric ID.
1852
+ * @returns The full ammunition record with specifications.
1853
+ * @throws {NotFoundError} If no ammunition matches the given identifier.
1854
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1855
+ *
1856
+ * @example
1857
+ * ```typescript
1858
+ * const { data } = await client.ammunition.get('9mm-federal-hst-124gr');
1859
+ * console.log(data.name, data.muzzleVelocityFps, data.bulletWeightGrains);
1860
+ * ```
1861
+ */
1862
+ async get(id) {
1863
+ return this.client.get(`/v1/ammunition/${encodeURIComponent(id)}`);
1864
+ }
1865
+ /**
1866
+ * Get a bullet profile SVG image for an ammunition load.
1867
+ *
1868
+ * Returns a raw SVG string representing the bullet projectile shape.
1869
+ * This endpoint returns raw SVG content, not the standard JSON envelope.
1870
+ *
1871
+ * @param id - The ammunition slug or ID.
1872
+ * @returns The raw SVG string of the bullet profile.
1873
+ * @throws {NotFoundError} If the ammunition does not exist.
1874
+ *
1875
+ * @example
1876
+ * ```typescript
1877
+ * const svg = await client.ammunition.getBulletSvg('9mm-federal-hst-124gr');
1878
+ * // Insert into DOM: element.innerHTML = svg;
1879
+ * ```
1880
+ */
1881
+ async getBulletSvg(id) {
1882
+ const response = await this.client.get(
1883
+ `/v1/ammunition/${encodeURIComponent(id)}/bullet.svg`
1884
+ );
1885
+ return response.data;
1886
+ }
1887
+ /**
1888
+ * Get ballistic trajectory data for an ammunition load.
1889
+ *
1890
+ * @param id - The ammunition slug or ID.
1891
+ * @param params - Optional barrel length and distance parameters.
1892
+ * @returns Ballistic trajectory data at specified distances.
1893
+ * @throws {NotFoundError} If the ammunition does not exist.
1894
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1895
+ *
1896
+ * @example
1897
+ * ```typescript
1898
+ * const { data } = await client.ammunition.ballistics('9mm-federal-hst-124gr', {
1899
+ * barrel_length_mm: 102,
1900
+ * distances: '0,25,50,100,200',
1901
+ * });
1902
+ * for (const point of data.trajectory) {
1903
+ * console.log(`${point.distanceM}m: ${point.velocityFps} fps, ${point.energyFtLbs} ft-lbs`);
1904
+ * }
1905
+ * ```
1906
+ */
1907
+ async ballistics(id, params) {
1908
+ return this.client.get(
1909
+ `/v1/ammunition/${encodeURIComponent(id)}/ballistics`,
1910
+ params
1911
+ );
1912
+ }
1913
+ };
1914
+
1915
+ // src/resources/countries.ts
1916
+ var CountriesResource = class {
1917
+ constructor(client) {
1918
+ this.client = client;
1919
+ }
1920
+ /**
1921
+ * List all countries that have adopted at least one firearm.
1922
+ *
1923
+ * @returns An array of country records with codes and names.
1924
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1925
+ *
1926
+ * @example
1927
+ * ```typescript
1928
+ * const { data } = await client.countries.list();
1929
+ * for (const country of data) {
1930
+ * console.log(country.code, country.name);
1931
+ * }
1932
+ * ```
1933
+ */
1934
+ async list() {
1935
+ return this.client.get("/v1/countries");
1936
+ }
1937
+ /**
1938
+ * Get the full firearm arsenal for a specific country.
1939
+ *
1940
+ * Returns all firearms adopted by the country, grouped by usage type
1941
+ * (military, law enforcement, etc.).
1942
+ *
1943
+ * @param code - The country code (e.g. `"US"`, `"GB"`, `"DE"`).
1944
+ * @returns The country's arsenal data with firearms grouped by adoption type.
1945
+ * @throws {NotFoundError} If the country code is not found in the database.
1946
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1947
+ *
1948
+ * @example
1949
+ * ```typescript
1950
+ * const { data } = await client.countries.getArsenal('US');
1951
+ * console.log(`${data.country.name} has ${data.totalFirearms} firearms`);
1952
+ * for (const group of data.groups) {
1953
+ * console.log(`${group.type}: ${group.firearms.length} firearms`);
1954
+ * }
1955
+ * ```
1956
+ */
1957
+ async getArsenal(code) {
1958
+ return this.client.get(`/v1/countries/${encodeURIComponent(code)}/arsenal`);
1959
+ }
1960
+ };
1961
+
1962
+ // src/resources/conflicts.ts
1963
+ var ConflictsResource = class {
1964
+ constructor(client) {
1965
+ this.client = client;
1966
+ }
1967
+ /**
1968
+ * List all military conflicts in the database.
1969
+ *
1970
+ * Returns conflicts with metadata including name, date range, and
1971
+ * participating nations. Use the conflict identifiers with
1972
+ * `client.firearms.byConflict()` to find firearms used in a specific conflict.
1973
+ *
1974
+ * @returns An array of all conflict records.
1975
+ * @throws {AuthenticationError} If the API key is missing or invalid.
1976
+ *
1977
+ * @example
1978
+ * ```typescript
1979
+ * const { data } = await client.conflicts.list();
1980
+ * for (const conflict of data) {
1981
+ * console.log(`${conflict.name} (${conflict.startYear}-${conflict.endYear})`);
1982
+ * }
1983
+ *
1984
+ * // Then find firearms used in a conflict:
1985
+ * const wwii = await client.firearms.byConflict({ conflict: 'world-war-ii' });
1986
+ * ```
1987
+ */
1988
+ async list() {
1989
+ return this.client.get("/v1/conflicts");
1990
+ }
1991
+ };
1992
+
1993
+ // src/resources/data-quality.ts
1994
+ var DataQualityResource = class {
1995
+ constructor(client) {
1996
+ this.client = client;
1997
+ }
1998
+ /**
1999
+ * Get overall data coverage statistics for the database.
2000
+ *
2001
+ * Returns per-field and aggregate coverage metrics showing what
2002
+ * percentage of records have data for each field.
2003
+ *
2004
+ * @returns Data coverage statistics including per-field percentages.
2005
+ * @throws {AuthenticationError} If the API key is missing or invalid.
2006
+ *
2007
+ * @example
2008
+ * ```typescript
2009
+ * const { data } = await client.dataQuality.coverage();
2010
+ * console.log(`Overall: ${data.overallCoverage}%`);
2011
+ * for (const field of data.fields) {
2012
+ * console.log(`${field.name}: ${field.coverage}%`);
2013
+ * }
2014
+ * ```
2015
+ */
2016
+ async coverage() {
2017
+ return this.client.get("/v1/data/coverage");
2018
+ }
2019
+ /**
2020
+ * Get firearms with confidence scores below a specified threshold.
2021
+ *
2022
+ * Useful for identifying records that may need additional data
2023
+ * verification or enrichment.
2024
+ *
2025
+ * @param params - Optional threshold and pagination parameters.
2026
+ * @returns A paginated list of firearms with their confidence scores.
2027
+ * @throws {BadRequestError} If the threshold is outside the 0-1 range.
2028
+ * @throws {AuthenticationError} If the API key is missing or invalid.
2029
+ *
2030
+ * @example
2031
+ * ```typescript
2032
+ * const result = await client.dataQuality.confidence({
2033
+ * below: 0.5,
2034
+ * per_page: 25,
2035
+ * });
2036
+ * for (const item of result.data) {
2037
+ * console.log(`${item.name}: confidence ${item.confidenceScore}`);
2038
+ * }
2039
+ * ```
2040
+ */
2041
+ async confidence(params) {
2042
+ return this.client.getPaginated("/v1/data/confidence", params);
2043
+ }
2044
+ };
2045
+
2046
+ // src/resources/favorites.ts
2047
+ var FavoritesResource = class {
2048
+ constructor(client) {
2049
+ this.client = client;
2050
+ }
2051
+ async list(params) {
2052
+ return this.client.getPaginated("/v1/me/favorites", params);
2053
+ }
2054
+ async add(firearmId) {
2055
+ return this.client.post(`/v1/me/favorites/${encodeURIComponent(firearmId)}`);
2056
+ }
2057
+ async remove(firearmId) {
2058
+ return this.client.delete(`/v1/me/favorites/${encodeURIComponent(firearmId)}`);
2059
+ }
2060
+ };
2061
+
2062
+ // src/resources/reports.ts
2063
+ var ReportsResource = class {
2064
+ constructor(client) {
2065
+ this.client = client;
2066
+ }
2067
+ async create(params) {
2068
+ return this.client.post("/v1/me/reports", params);
2069
+ }
2070
+ async list(params) {
2071
+ return this.client.getPaginated("/v1/me/reports", params);
2072
+ }
2073
+ };
2074
+
2075
+ // src/resources/support.ts
2076
+ var SupportResource = class {
2077
+ constructor(client) {
2078
+ this.client = client;
2079
+ }
2080
+ async create(params) {
2081
+ return this.client.post("/v1/me/support", params);
2082
+ }
2083
+ async list(params) {
2084
+ return this.client.getPaginated("/v1/me/support", params);
2085
+ }
2086
+ async get(ticketId) {
2087
+ return this.client.get(`/v1/me/support/${encodeURIComponent(ticketId)}`);
2088
+ }
2089
+ async reply(ticketId, params) {
2090
+ return this.client.post(`/v1/me/support/${encodeURIComponent(ticketId)}/replies`, params);
2091
+ }
2092
+ };
2093
+
2094
+ // src/resources/webhooks.ts
2095
+ var WebhooksResource = class {
2096
+ constructor(client) {
2097
+ this.client = client;
2098
+ }
2099
+ async list(params) {
2100
+ return this.client.getPaginated("/v1/me/webhooks", params);
2101
+ }
2102
+ async create(params) {
2103
+ return this.client.post("/v1/me/webhooks", params);
2104
+ }
2105
+ async get(id) {
2106
+ return this.client.get(`/v1/me/webhooks/${encodeURIComponent(id)}`);
2107
+ }
2108
+ async update(id, params) {
2109
+ return this.client.put(`/v1/me/webhooks/${encodeURIComponent(id)}`, params);
2110
+ }
2111
+ async delete(id) {
2112
+ return this.client.delete(`/v1/me/webhooks/${encodeURIComponent(id)}`);
2113
+ }
2114
+ async test(id) {
2115
+ return this.client.post(`/v1/me/webhooks/${encodeURIComponent(id)}/test`);
2116
+ }
2117
+ };
2118
+
2119
+ // src/resources/usage.ts
2120
+ var UsageResource = class {
2121
+ constructor(client) {
2122
+ this.client = client;
2123
+ }
2124
+ async get(params) {
2125
+ return this.client.get("/v1/me/usage", params);
2126
+ }
2127
+ };
2128
+
2129
+ // src/version.ts
2130
+ var VERSION = "0.1.0";
2131
+
2132
+ // src/client.ts
2133
+ var GunSpec = class {
2134
+ /** Firearms specifications, search, comparison, and related data */
2135
+ firearms;
2136
+ /** Firearm manufacturers and their products */
2137
+ manufacturers;
2138
+ /** Ammunition calibers and cartridge specifications */
2139
+ calibers;
2140
+ /** Firearm categories (pistol, rifle, shotgun, etc.) */
2141
+ categories;
2142
+ /** Aggregate statistics across the database */
2143
+ stats;
2144
+ /** Game development tools — balance reports, tier lists, matchups */
2145
+ game;
2146
+ /** Versioned game stats snapshots for pinning game builds */
2147
+ gameStats;
2148
+ /** Ammunition loads, ballistics, and bullet data */
2149
+ ammunition;
2150
+ /** Countries and their military/law enforcement arsenals */
2151
+ countries;
2152
+ /** Armed conflicts and the firearms used in them */
2153
+ conflicts;
2154
+ /** Data quality metrics (Enterprise tier) */
2155
+ dataQuality;
2156
+ /** User's favorited firearms */
2157
+ favorites;
2158
+ /** Data quality reports */
2159
+ reports;
2160
+ /** Support tickets (paid tiers) */
2161
+ support;
2162
+ /** Webhook endpoint management (studio+ tier) */
2163
+ webhooks;
2164
+ /** API usage statistics */
2165
+ usage;
2166
+ /** The underlying HTTP client (for advanced use) */
2167
+ _client;
2168
+ constructor(options = {}) {
2169
+ this._client = new HttpClient({
2170
+ baseUrl: options.baseURL ?? "https://api.gunspec.io",
2171
+ timeout: options.timeout ?? 3e4,
2172
+ auth: {
2173
+ apiKey: options.apiKey
2174
+ },
2175
+ retry: options.retry,
2176
+ headers: {
2177
+ ...options.defaultHeaders,
2178
+ "User-Agent": `gunspec-sdk/typescript/${VERSION}`,
2179
+ "X-SDK-Version": VERSION,
2180
+ "X-SDK-Language": "typescript"
2181
+ }
2182
+ });
2183
+ this.firearms = new FirearmsResource(this._client);
2184
+ this.manufacturers = new ManufacturersResource(this._client);
2185
+ this.calibers = new CalibersResource(this._client);
2186
+ this.categories = new CategoriesResource(this._client);
2187
+ this.stats = new StatsResource(this._client);
2188
+ this.game = new GameResource(this._client);
2189
+ this.gameStats = new GameStatsResource(this._client);
2190
+ this.ammunition = new AmmunitionResource(this._client);
2191
+ this.countries = new CountriesResource(this._client);
2192
+ this.conflicts = new ConflictsResource(this._client);
2193
+ this.dataQuality = new DataQualityResource(this._client);
2194
+ this.favorites = new FavoritesResource(this._client);
2195
+ this.reports = new ReportsResource(this._client);
2196
+ this.support = new SupportResource(this._client);
2197
+ this.webhooks = new WebhooksResource(this._client);
2198
+ this.usage = new UsageResource(this._client);
2199
+ }
2200
+ };
2201
+
2202
+ // src/core/pagination.ts
2203
+ var Page = class _Page {
2204
+ /** The items on this page. */
2205
+ data;
2206
+ /** Pagination metadata returned by the server. */
2207
+ pagination;
2208
+ /** The request ID for this page's HTTP response. */
2209
+ requestId;
2210
+ /** The base query parameters used to fetch this page (without `page`). */
2211
+ baseQuery;
2212
+ /** The fetcher function used to retrieve subsequent pages. */
2213
+ fetcher;
2214
+ /**
2215
+ * @internal
2216
+ * Consumers should not construct `Page` instances directly. Use the
2217
+ * resource methods on the high-level client instead.
2218
+ */
2219
+ constructor(response, baseQuery, fetcher) {
2220
+ this.data = response.data;
2221
+ this.pagination = response.pagination;
2222
+ this.requestId = response.requestId;
2223
+ this.baseQuery = baseQuery;
2224
+ this.fetcher = fetcher;
2225
+ }
2226
+ /**
2227
+ * Whether there is a next page of results.
2228
+ *
2229
+ * When `totalPages` is available (pro/enterprise tiers) the check is exact.
2230
+ * Otherwise the heuristic is: if the current page returned a full page of
2231
+ * results (i.e. `data.length === limit`), there is likely another page.
2232
+ */
2233
+ hasNextPage() {
2234
+ if (this.pagination.totalPages !== void 0) {
2235
+ return this.pagination.page < this.pagination.totalPages;
2236
+ }
2237
+ return this.data.length >= this.pagination.limit;
2238
+ }
2239
+ /**
2240
+ * Fetch the next page of results.
2241
+ *
2242
+ * @returns A new {@link Page} instance for the next page.
2243
+ * @throws {Error} If there is no next page. Always check
2244
+ * {@link hasNextPage} first.
2245
+ */
2246
+ async getNextPage() {
2247
+ if (!this.hasNextPage()) {
2248
+ throw new Error("No more pages available. Check hasNextPage() before calling getNextPage().");
2249
+ }
2250
+ const nextQuery = {
2251
+ ...this.baseQuery,
2252
+ page: this.pagination.page + 1
2253
+ };
2254
+ const response = await this.fetcher(nextQuery);
2255
+ return new _Page(response, this.baseQuery, this.fetcher);
2256
+ }
2257
+ /**
2258
+ * Fetch the previous page of results.
2259
+ *
2260
+ * @returns A new {@link Page} instance for the previous page.
2261
+ * @throws {Error} If this is already the first page.
2262
+ */
2263
+ async getPreviousPage() {
2264
+ if (this.pagination.page <= 1) {
2265
+ throw new Error("Already on the first page. Cannot navigate to a previous page.");
2266
+ }
2267
+ const prevQuery = {
2268
+ ...this.baseQuery,
2269
+ page: this.pagination.page - 1
2270
+ };
2271
+ const response = await this.fetcher(prevQuery);
2272
+ return new _Page(response, this.baseQuery, this.fetcher);
2273
+ }
2274
+ /**
2275
+ * Async iterator that yields every item across **all** pages, starting
2276
+ * from the current page.
2277
+ *
2278
+ * Fetches subsequent pages on demand (lazily) so memory usage stays
2279
+ * constant regardless of the total result set size.
2280
+ *
2281
+ * @example
2282
+ * ```ts
2283
+ * const firstPage = await client.firearms.list({ limit: 50 });
2284
+ * for await (const firearm of firstPage) {
2285
+ * // Iterates page 1, then auto-fetches page 2, 3, ... until exhausted.
2286
+ * console.log(firearm.name);
2287
+ * }
2288
+ * ```
2289
+ */
2290
+ async *[Symbol.asyncIterator]() {
2291
+ let current = this;
2292
+ while (true) {
2293
+ for (const item of current.data) {
2294
+ yield item;
2295
+ }
2296
+ if (!current.hasNextPage()) {
2297
+ break;
2298
+ }
2299
+ current = await current.getNextPage();
2300
+ }
2301
+ }
2302
+ };
2303
+ function createPage(response, baseQuery, fetcher) {
2304
+ return new Page(response, baseQuery, fetcher);
2305
+ }
2306
+
2307
+ exports.APIError = APIError;
2308
+ exports.AmmunitionResource = AmmunitionResource;
2309
+ exports.AuthenticationError = AuthenticationError;
2310
+ exports.BadRequestError = BadRequestError;
2311
+ exports.CalibersResource = CalibersResource;
2312
+ exports.CategoriesResource = CategoriesResource;
2313
+ exports.ConflictsResource = ConflictsResource;
2314
+ exports.ConnectionError = ConnectionError;
2315
+ exports.CountriesResource = CountriesResource;
2316
+ exports.DataQualityResource = DataQualityResource;
2317
+ exports.FavoritesResource = FavoritesResource;
2318
+ exports.FirearmsResource = FirearmsResource;
2319
+ exports.GameResource = GameResource;
2320
+ exports.GameStatsResource = GameStatsResource;
2321
+ exports.GunSpec = GunSpec;
2322
+ exports.GunSpecError = GunSpecError;
2323
+ exports.HttpClient = HttpClient;
2324
+ exports.InternalServerError = InternalServerError;
2325
+ exports.ManufacturersResource = ManufacturersResource;
2326
+ exports.NotFoundError = NotFoundError;
2327
+ exports.Page = Page;
2328
+ exports.PermissionError = PermissionError;
2329
+ exports.RateLimitError = RateLimitError;
2330
+ exports.ReportsResource = ReportsResource;
2331
+ exports.StatsResource = StatsResource;
2332
+ exports.SupportResource = SupportResource;
2333
+ exports.TimeoutError = TimeoutError;
2334
+ exports.UsageResource = UsageResource;
2335
+ exports.VERSION = VERSION;
2336
+ exports.WebhooksResource = WebhooksResource;
2337
+ exports.buildAuthHeaders = buildAuthHeaders;
2338
+ exports.createAPIError = createAPIError;
2339
+ exports.createPage = createPage;
2340
+ exports.resolveApiKey = resolveApiKey;
2341
+ //# sourceMappingURL=index.cjs.map
2342
+ //# sourceMappingURL=index.cjs.map