colacloud 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,632 @@
1
+ // src/errors.ts
2
+ var ColaCloudError = class _ColaCloudError extends Error {
3
+ /** HTTP status code (if applicable) */
4
+ statusCode;
5
+ /** Error code from the API */
6
+ code;
7
+ /** Additional error details */
8
+ details;
9
+ constructor(message, statusCode, code, details) {
10
+ super(message);
11
+ this.name = "ColaCloudError";
12
+ this.statusCode = statusCode;
13
+ this.code = code;
14
+ this.details = details;
15
+ if (Error.captureStackTrace) {
16
+ Error.captureStackTrace(this, _ColaCloudError);
17
+ }
18
+ }
19
+ /**
20
+ * Create a ColaCloudError from an API error response
21
+ */
22
+ static fromResponse(response, statusCode) {
23
+ return new _ColaCloudError(
24
+ response.error,
25
+ statusCode,
26
+ response.code,
27
+ response.details
28
+ );
29
+ }
30
+ };
31
+ var AuthenticationError = class _AuthenticationError extends ColaCloudError {
32
+ constructor(message = "Invalid or missing API key") {
33
+ super(message, 401, "authentication_error");
34
+ this.name = "AuthenticationError";
35
+ if (Error.captureStackTrace) {
36
+ Error.captureStackTrace(this, _AuthenticationError);
37
+ }
38
+ }
39
+ };
40
+ var RateLimitError = class _RateLimitError extends ColaCloudError {
41
+ /** Rate limit information from headers */
42
+ rateLimit;
43
+ /** Seconds until the rate limit resets */
44
+ retryAfter;
45
+ constructor(message = "Rate limit exceeded", rateLimit = null, retryAfter = null) {
46
+ super(message, 429, "rate_limit_exceeded");
47
+ this.name = "RateLimitError";
48
+ this.rateLimit = rateLimit;
49
+ this.retryAfter = retryAfter;
50
+ if (Error.captureStackTrace) {
51
+ Error.captureStackTrace(this, _RateLimitError);
52
+ }
53
+ }
54
+ };
55
+ var NotFoundError = class _NotFoundError extends ColaCloudError {
56
+ /** The resource type that was not found */
57
+ resourceType;
58
+ /** The identifier that was searched for */
59
+ resourceId;
60
+ constructor(resourceType, resourceId) {
61
+ super(`${resourceType} not found: ${resourceId}`, 404, "not_found");
62
+ this.name = "NotFoundError";
63
+ this.resourceType = resourceType;
64
+ this.resourceId = resourceId;
65
+ if (Error.captureStackTrace) {
66
+ Error.captureStackTrace(this, _NotFoundError);
67
+ }
68
+ }
69
+ };
70
+ var ValidationError = class _ValidationError extends ColaCloudError {
71
+ /** Field-level validation errors */
72
+ fieldErrors;
73
+ constructor(message = "Invalid request parameters", fieldErrors) {
74
+ super(message, 400, "validation_error", fieldErrors);
75
+ this.name = "ValidationError";
76
+ this.fieldErrors = fieldErrors;
77
+ if (Error.captureStackTrace) {
78
+ Error.captureStackTrace(this, _ValidationError);
79
+ }
80
+ }
81
+ };
82
+ var ServerError = class _ServerError extends ColaCloudError {
83
+ constructor(message = "Internal server error", statusCode = 500) {
84
+ super(message, statusCode, "server_error");
85
+ this.name = "ServerError";
86
+ if (Error.captureStackTrace) {
87
+ Error.captureStackTrace(this, _ServerError);
88
+ }
89
+ }
90
+ };
91
+ var TimeoutError = class _TimeoutError extends ColaCloudError {
92
+ /** The timeout value in milliseconds */
93
+ timeoutMs;
94
+ constructor(timeoutMs) {
95
+ super(`Request timed out after ${timeoutMs}ms`, void 0, "timeout");
96
+ this.name = "TimeoutError";
97
+ this.timeoutMs = timeoutMs;
98
+ if (Error.captureStackTrace) {
99
+ Error.captureStackTrace(this, _TimeoutError);
100
+ }
101
+ }
102
+ };
103
+ var NetworkError = class _NetworkError extends ColaCloudError {
104
+ /** The original error that caused this */
105
+ cause;
106
+ constructor(message = "Network error", cause) {
107
+ super(message, void 0, "network_error");
108
+ this.name = "NetworkError";
109
+ this.cause = cause;
110
+ if (Error.captureStackTrace) {
111
+ Error.captureStackTrace(this, _NetworkError);
112
+ }
113
+ }
114
+ };
115
+
116
+ // src/pagination.ts
117
+ function createPaginatedIterator(options) {
118
+ const { params, fetchPage, startPage = 1, maxPages } = options;
119
+ return {
120
+ [Symbol.asyncIterator]() {
121
+ let currentPage = startPage;
122
+ let currentItems = [];
123
+ let currentIndex = 0;
124
+ let pagination = null;
125
+ let pagesFetched = 0;
126
+ let done = false;
127
+ return {
128
+ async next() {
129
+ while (currentIndex >= currentItems.length && !done) {
130
+ if (maxPages !== void 0 && pagesFetched >= maxPages) {
131
+ done = true;
132
+ break;
133
+ }
134
+ if (pagination !== null && currentPage > pagination.pages) {
135
+ done = true;
136
+ break;
137
+ }
138
+ const result = await fetchPage({
139
+ ...params,
140
+ page: currentPage
141
+ });
142
+ pagination = result.response.pagination;
143
+ currentItems = result.response.data;
144
+ currentIndex = 0;
145
+ currentPage++;
146
+ pagesFetched++;
147
+ if (currentItems.length === 0) {
148
+ done = true;
149
+ break;
150
+ }
151
+ }
152
+ if (currentIndex < currentItems.length) {
153
+ const item = currentItems[currentIndex];
154
+ currentIndex++;
155
+ if (item === void 0) {
156
+ return { done: true, value: void 0 };
157
+ }
158
+ return { done: false, value: item };
159
+ }
160
+ return { done: true, value: void 0 };
161
+ }
162
+ };
163
+ }
164
+ };
165
+ }
166
+ function createPaginatedIteratorWithMetadata(options) {
167
+ const { params, fetchPage, startPage = 1, maxPages } = options;
168
+ return {
169
+ [Symbol.asyncIterator]() {
170
+ let currentPage = startPage;
171
+ let currentItems = [];
172
+ let currentIndex = 0;
173
+ let pagination = null;
174
+ let rateLimit = null;
175
+ let pagesFetched = 0;
176
+ let done = false;
177
+ return {
178
+ async next() {
179
+ while (currentIndex >= currentItems.length && !done) {
180
+ if (maxPages !== void 0 && pagesFetched >= maxPages) {
181
+ done = true;
182
+ break;
183
+ }
184
+ if (pagination !== null && currentPage > pagination.pages) {
185
+ done = true;
186
+ break;
187
+ }
188
+ const result = await fetchPage({
189
+ ...params,
190
+ page: currentPage
191
+ });
192
+ pagination = result.response.pagination;
193
+ rateLimit = result.rateLimit;
194
+ currentItems = result.response.data;
195
+ currentIndex = 0;
196
+ currentPage++;
197
+ pagesFetched++;
198
+ if (currentItems.length === 0) {
199
+ done = true;
200
+ break;
201
+ }
202
+ }
203
+ if (currentIndex < currentItems.length) {
204
+ const item = currentItems[currentIndex];
205
+ if (item === void 0) {
206
+ return { done: true, value: void 0 };
207
+ }
208
+ const result = {
209
+ item,
210
+ page: currentPage - 1,
211
+ // We already incremented after fetch
212
+ indexInPage: currentIndex,
213
+ total: pagination?.total ?? 0,
214
+ rateLimit
215
+ };
216
+ currentIndex++;
217
+ return { done: false, value: result };
218
+ }
219
+ return { done: true, value: void 0 };
220
+ }
221
+ };
222
+ }
223
+ };
224
+ }
225
+ async function collectAll(iterator, maxItems) {
226
+ const items = [];
227
+ for await (const item of iterator) {
228
+ items.push(item);
229
+ if (maxItems !== void 0 && items.length >= maxItems) {
230
+ break;
231
+ }
232
+ }
233
+ return items;
234
+ }
235
+ async function take(iterator, count) {
236
+ return collectAll(iterator, count);
237
+ }
238
+
239
+ // src/client.ts
240
+ var DEFAULT_BASE_URL = "https://app.colacloud.us/api/v1";
241
+ var DEFAULT_TIMEOUT = 3e4;
242
+ function toSnakeCase(params) {
243
+ const result = {};
244
+ for (const [key, value] of Object.entries(params)) {
245
+ if (value === void 0 || value === null) continue;
246
+ const snakeKey = key.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
247
+ result[snakeKey] = String(value);
248
+ }
249
+ return result;
250
+ }
251
+ function parseRateLimitHeaders(headers) {
252
+ const limit = headers.get("X-RateLimit-Limit");
253
+ const remaining = headers.get("X-RateLimit-Remaining");
254
+ const reset = headers.get("X-RateLimit-Reset");
255
+ if (limit === null || remaining === null || reset === null) {
256
+ return null;
257
+ }
258
+ return {
259
+ limit: parseInt(limit, 10),
260
+ remaining: parseInt(remaining, 10),
261
+ reset: parseInt(reset, 10)
262
+ };
263
+ }
264
+ var ColaCloud = class {
265
+ apiKey;
266
+ baseUrl;
267
+ timeout;
268
+ /** COLA (Certificate of Label Approval) endpoints */
269
+ colas;
270
+ /** Permittee (business/permit holder) endpoints */
271
+ permittees;
272
+ /** Barcode lookup endpoint */
273
+ barcodes;
274
+ /** Usage statistics endpoint */
275
+ usage;
276
+ /**
277
+ * Create a new COLA Cloud API client
278
+ * @param config Configuration options
279
+ */
280
+ constructor(config) {
281
+ if (!config.apiKey) {
282
+ throw new Error("API key is required");
283
+ }
284
+ this.apiKey = config.apiKey;
285
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
286
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
287
+ this.colas = new ColasResource(this);
288
+ this.permittees = new PermitteesResource(this);
289
+ this.barcodes = new BarcodesResource(this);
290
+ this.usage = new UsageResource(this);
291
+ }
292
+ /**
293
+ * Make an authenticated request to the API
294
+ * @internal
295
+ */
296
+ async request(method, path, params) {
297
+ let url = `${this.baseUrl}${path}`;
298
+ if (method === "GET" && params && Object.keys(params).length > 0) {
299
+ const searchParams = new URLSearchParams(toSnakeCase(params));
300
+ url += `?${searchParams.toString()}`;
301
+ }
302
+ const controller = new AbortController();
303
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
304
+ try {
305
+ const response = await fetch(url, {
306
+ method,
307
+ headers: {
308
+ "X-API-Key": this.apiKey,
309
+ "Content-Type": "application/json",
310
+ Accept: "application/json"
311
+ },
312
+ signal: controller.signal
313
+ });
314
+ clearTimeout(timeoutId);
315
+ const rateLimit = parseRateLimitHeaders(response.headers);
316
+ if (!response.ok) {
317
+ await this.handleErrorResponse(response, rateLimit);
318
+ }
319
+ const data = await response.json();
320
+ return { data, rateLimit };
321
+ } catch (error) {
322
+ clearTimeout(timeoutId);
323
+ if (error instanceof Error && error.name === "AbortError") {
324
+ throw new TimeoutError(this.timeout);
325
+ }
326
+ if (error instanceof ColaCloudError) {
327
+ throw error;
328
+ }
329
+ throw new NetworkError(
330
+ error instanceof Error ? error.message : "Unknown error",
331
+ error instanceof Error ? error : void 0
332
+ );
333
+ }
334
+ }
335
+ /**
336
+ * Handle error responses from the API
337
+ */
338
+ async handleErrorResponse(response, rateLimit) {
339
+ let errorData;
340
+ try {
341
+ errorData = await response.json();
342
+ } catch {
343
+ errorData = { error: response.statusText || "Unknown error" };
344
+ }
345
+ const message = errorData.error || "Unknown error";
346
+ switch (response.status) {
347
+ case 401:
348
+ throw new AuthenticationError(message);
349
+ case 404:
350
+ throw new NotFoundError("Resource", "unknown");
351
+ case 429: {
352
+ const retryAfter = response.headers.get("Retry-After");
353
+ throw new RateLimitError(
354
+ message,
355
+ rateLimit,
356
+ retryAfter ? parseInt(retryAfter, 10) : null
357
+ );
358
+ }
359
+ case 400:
360
+ throw new ValidationError(
361
+ message,
362
+ errorData.details
363
+ );
364
+ default:
365
+ if (response.status >= 500) {
366
+ throw new ServerError(message, response.status);
367
+ }
368
+ throw ColaCloudError.fromResponse(errorData, response.status);
369
+ }
370
+ }
371
+ };
372
+ var ColasResource = class {
373
+ constructor(client) {
374
+ this.client = client;
375
+ }
376
+ /**
377
+ * List and search COLAs with pagination
378
+ * @param params Search and filter parameters
379
+ * @returns Paginated list of COLA summaries
380
+ */
381
+ async list(params = {}) {
382
+ const result = await this.client.request(
383
+ "GET",
384
+ "/colas",
385
+ params
386
+ );
387
+ return result.data;
388
+ }
389
+ /**
390
+ * List COLAs with rate limit information
391
+ * @param params Search and filter parameters
392
+ * @returns Paginated list with rate limit info
393
+ */
394
+ async listWithRateLimit(params = {}) {
395
+ return this.client.request(
396
+ "GET",
397
+ "/colas",
398
+ params
399
+ );
400
+ }
401
+ /**
402
+ * Get a single COLA by TTB ID
403
+ * @param ttbId The unique TTB identifier
404
+ * @returns Full COLA details including images and barcodes
405
+ */
406
+ async get(ttbId) {
407
+ try {
408
+ const result = await this.client.request(
409
+ "GET",
410
+ `/colas/${encodeURIComponent(ttbId)}`
411
+ );
412
+ return result.data.data;
413
+ } catch (error) {
414
+ if (error instanceof NotFoundError) {
415
+ throw new NotFoundError("COLA", ttbId);
416
+ }
417
+ throw error;
418
+ }
419
+ }
420
+ /**
421
+ * Get a single COLA with rate limit information
422
+ * @param ttbId The unique TTB identifier
423
+ * @returns COLA details with rate limit info
424
+ */
425
+ async getWithRateLimit(ttbId) {
426
+ try {
427
+ const result = await this.client.request(
428
+ "GET",
429
+ `/colas/${encodeURIComponent(ttbId)}`
430
+ );
431
+ return { data: result.data.data, rateLimit: result.rateLimit };
432
+ } catch (error) {
433
+ if (error instanceof NotFoundError) {
434
+ throw new NotFoundError("COLA", ttbId);
435
+ }
436
+ throw error;
437
+ }
438
+ }
439
+ /**
440
+ * Create an async iterator that automatically pages through all results
441
+ * @param params Search and filter parameters (page is ignored)
442
+ * @returns Async iterable that yields individual COLA summaries
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * for await (const cola of client.colas.iterate({ q: 'bourbon' })) {
447
+ * console.log(cola.ttb_id, cola.brand_name);
448
+ * }
449
+ * ```
450
+ */
451
+ iterate(params = {}) {
452
+ return createPaginatedIterator({
453
+ params,
454
+ fetchPage: async (p) => {
455
+ const result = await this.client.request(
456
+ "GET",
457
+ "/colas",
458
+ p
459
+ );
460
+ return { response: result.data, rateLimit: result.rateLimit };
461
+ }
462
+ });
463
+ }
464
+ };
465
+ var PermitteesResource = class {
466
+ constructor(client) {
467
+ this.client = client;
468
+ }
469
+ /**
470
+ * List and search permittees with pagination
471
+ * @param params Search and filter parameters
472
+ * @returns Paginated list of permittee summaries
473
+ */
474
+ async list(params = {}) {
475
+ const result = await this.client.request(
476
+ "GET",
477
+ "/permittees",
478
+ params
479
+ );
480
+ return result.data;
481
+ }
482
+ /**
483
+ * List permittees with rate limit information
484
+ * @param params Search and filter parameters
485
+ * @returns Paginated list with rate limit info
486
+ */
487
+ async listWithRateLimit(params = {}) {
488
+ return this.client.request(
489
+ "GET",
490
+ "/permittees",
491
+ params
492
+ );
493
+ }
494
+ /**
495
+ * Get a single permittee by permit number
496
+ * @param permitNumber The unique permit number
497
+ * @returns Full permittee details including recent COLAs
498
+ */
499
+ async get(permitNumber) {
500
+ try {
501
+ const result = await this.client.request(
502
+ "GET",
503
+ `/permittees/${encodeURIComponent(permitNumber)}`
504
+ );
505
+ return result.data.data;
506
+ } catch (error) {
507
+ if (error instanceof NotFoundError) {
508
+ throw new NotFoundError("Permittee", permitNumber);
509
+ }
510
+ throw error;
511
+ }
512
+ }
513
+ /**
514
+ * Get a single permittee with rate limit information
515
+ * @param permitNumber The unique permit number
516
+ * @returns Permittee details with rate limit info
517
+ */
518
+ async getWithRateLimit(permitNumber) {
519
+ try {
520
+ const result = await this.client.request(
521
+ "GET",
522
+ `/permittees/${encodeURIComponent(permitNumber)}`
523
+ );
524
+ return { data: result.data.data, rateLimit: result.rateLimit };
525
+ } catch (error) {
526
+ if (error instanceof NotFoundError) {
527
+ throw new NotFoundError("Permittee", permitNumber);
528
+ }
529
+ throw error;
530
+ }
531
+ }
532
+ /**
533
+ * Create an async iterator that automatically pages through all results
534
+ * @param params Search and filter parameters (page is ignored)
535
+ * @returns Async iterable that yields individual permittee summaries
536
+ *
537
+ * @example
538
+ * ```typescript
539
+ * for await (const permittee of client.permittees.iterate({ state: 'CA' })) {
540
+ * console.log(permittee.company_name);
541
+ * }
542
+ * ```
543
+ */
544
+ iterate(params = {}) {
545
+ return createPaginatedIterator({
546
+ params,
547
+ fetchPage: async (p) => {
548
+ const result = await this.client.request("GET", "/permittees", p);
549
+ return { response: result.data, rateLimit: result.rateLimit };
550
+ }
551
+ });
552
+ }
553
+ };
554
+ var BarcodesResource = class {
555
+ constructor(client) {
556
+ this.client = client;
557
+ }
558
+ /**
559
+ * Look up COLAs by barcode value
560
+ * @param barcodeValue The barcode to search for (UPC, EAN, etc.)
561
+ * @returns Barcode information and associated COLAs
562
+ */
563
+ async lookup(barcodeValue) {
564
+ try {
565
+ const result = await this.client.request("GET", `/barcode/${encodeURIComponent(barcodeValue)}`);
566
+ return result.data.data;
567
+ } catch (error) {
568
+ if (error instanceof NotFoundError) {
569
+ throw new NotFoundError("Barcode", barcodeValue);
570
+ }
571
+ throw error;
572
+ }
573
+ }
574
+ /**
575
+ * Look up barcode with rate limit information
576
+ * @param barcodeValue The barcode to search for
577
+ * @returns Barcode lookup result with rate limit info
578
+ */
579
+ async lookupWithRateLimit(barcodeValue) {
580
+ try {
581
+ const result = await this.client.request("GET", `/barcode/${encodeURIComponent(barcodeValue)}`);
582
+ return { data: result.data.data, rateLimit: result.rateLimit };
583
+ } catch (error) {
584
+ if (error instanceof NotFoundError) {
585
+ throw new NotFoundError("Barcode", barcodeValue);
586
+ }
587
+ throw error;
588
+ }
589
+ }
590
+ };
591
+ var UsageResource = class {
592
+ constructor(client) {
593
+ this.client = client;
594
+ }
595
+ /**
596
+ * Get API usage statistics for the current account
597
+ * @returns Usage statistics including limits and current usage
598
+ */
599
+ async get() {
600
+ const result = await this.client.request(
601
+ "GET",
602
+ "/usage"
603
+ );
604
+ return result.data.data;
605
+ }
606
+ /**
607
+ * Get usage statistics with rate limit information
608
+ * @returns Usage statistics with rate limit info
609
+ */
610
+ async getWithRateLimit() {
611
+ const result = await this.client.request(
612
+ "GET",
613
+ "/usage"
614
+ );
615
+ return { data: result.data.data, rateLimit: result.rateLimit };
616
+ }
617
+ };
618
+ export {
619
+ AuthenticationError,
620
+ ColaCloud,
621
+ ColaCloudError,
622
+ NetworkError,
623
+ NotFoundError,
624
+ RateLimitError,
625
+ ServerError,
626
+ TimeoutError,
627
+ ValidationError,
628
+ collectAll,
629
+ createPaginatedIterator,
630
+ createPaginatedIteratorWithMetadata,
631
+ take
632
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "colacloud",
3
+ "version": "1.0.0",
4
+ "description": "Official JavaScript/TypeScript SDK for the COLA Cloud API - access the TTB COLA Registry of alcohol product label approvals",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest run --coverage",
25
+ "lint": "eslint src tests --ext .ts",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "cola",
31
+ "ttb",
32
+ "alcohol",
33
+ "beverage",
34
+ "label",
35
+ "api",
36
+ "sdk",
37
+ "typescript"
38
+ ],
39
+ "author": "COLA Cloud",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/cola-cloud-us/colacloud-js.git"
44
+ },
45
+ "homepage": "https://colacloud.us",
46
+ "bugs": {
47
+ "url": "https://github.com/cola-cloud-us/colacloud-js/issues"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.10.0",
54
+ "tsup": "^8.0.1",
55
+ "typescript": "^5.3.0",
56
+ "vitest": "^1.1.0"
57
+ }
58
+ }