@caldik/client 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -165,6 +165,10 @@ The following fields are omitted by default unless explicitly requested using `i
165
165
 
166
166
  ### Cart Management
167
167
 
168
+ The SDK manages the `cart_{storeId}` cookie and server persistence automatically. Cart
169
+ requests, responses, and saved browser state do not require version numbers.
170
+ Existing saved carts are restored automatically after upgrading.
171
+
168
172
  **Get the cart**
169
173
 
170
174
  Refresh the current items and applied discount from the server.
@@ -370,14 +374,95 @@ Omit `rememberMe` to use the default session duration.
370
374
 
371
375
  ### Session Check
372
376
 
373
- Retrieve the current customer's authentication status, account details, and session expiration.
377
+ Check whether the shopper is authenticated. HTTP 200 has no body and resolves to
378
+ `undefined`; HTTP 401 rejects with `CaldikHttpError`. Customer details are returned
379
+ by `me()`.
380
+
381
+ ```ts
382
+ await caldik.customer.session()
383
+ ```
384
+
385
+ ### Current Customer
386
+
387
+ **Get the signed-in customer**
388
+
389
+ ```ts
390
+ const { customer } = await caldik.customers.me()
391
+ console.log(customer.name, customer.email)
392
+ console.log(customer.totalOrders, customer.totalSpent, customer.currency)
393
+ ```
394
+
395
+ `totalOrders` counts this customer's orders in the store. `totalSpent` is a
396
+ decimal string summing successful payments after discounts in the store's
397
+ currency; unpaid, failed, refunded and other-currency orders are excluded from
398
+ spending. This profile does not include `shortId` or `displayId`.
399
+
400
+ **List the customer's orders**
401
+
402
+ ```ts
403
+ const { orders, nextCursor } = await caldik.customers.orders.list({ limit: 5 })
404
+ ```
405
+
406
+ **Read the next page**
407
+
408
+ ```ts
409
+ if (nextCursor) {
410
+ const nextPage = await caldik.customers.orders.list({ cursor: nextCursor })
411
+ }
412
+ ```
413
+
414
+ **Get one of the customer's orders**
374
415
 
375
416
  ```ts
376
- const {
377
- authenticated,
378
- customer,
379
- expiresAt,
380
- } = await caldik.customer.session()
417
+ const { order } = await caldik.customers.orders.get("ORDER_...")
418
+ console.log(order.items)
419
+ ```
420
+
421
+ **Get the customer's balance**
422
+
423
+ ```ts
424
+ const { balance } = await caldik.customers.balance.get()
425
+ console.log(balance.amount, balance.currency)
426
+ ```
427
+
428
+ The default currency is the store's currency. Pass `{ currency: "USD" }` to read
429
+ another balance. Amounts are decimal strings; a missing balance is `"0.00"`.
430
+
431
+ ### Batch Reads
432
+
433
+ **Fetch several resources in one HTTP request**
434
+
435
+ ```ts
436
+ const data = await caldik.batch({
437
+ customer: caldik.customers.me(),
438
+ orders: caldik.orders.list({ limit: 5 }),
439
+ balance: caldik.customers.balance.get(),
440
+ rank: caldik.plugins.loyalty.getRank(),
441
+ })
442
+
443
+ console.log(data.customer.customer.email)
444
+ console.log(data.rank.rank?.name)
445
+ ```
446
+
447
+ Batches accept up to 25 distinct, unstarted read requests from the same client.
448
+ Build the requests in the batch expression; already awaited requests cannot be
449
+ batched. Catalog reads and read-only plugin methods are also supported. Each
450
+ result retains the same shape as an individual call, including `undefined` for
451
+ a successful session check. Batches are independent reads, not a transaction.
452
+
453
+ **Handle a partially failed batch**
454
+
455
+ ```ts
456
+ import { CaldikBatchError } from "@caldik/client"
457
+
458
+ try {
459
+ const data = await caldik.batch({ customer: caldik.customers.me() })
460
+ } catch (error) {
461
+ if (error instanceof CaldikBatchError) {
462
+ console.log(error.errors) // HTTP status and response for each failed key
463
+ console.log(error.results) // successful results remain available
464
+ }
465
+ }
381
466
  ```
382
467
 
383
468
  ### Password Reset
@@ -454,6 +539,7 @@ await caldik.customer.register.verify(code)
454
539
 
455
540
  > **Note:** The `username` field is provided by the Customer Usernames plugin. Omit this field when the plugin is not installed.
456
541
 
542
+
457
543
  ## Payment Methods and Settings
458
544
 
459
545
  Retrieve customer authentication settings, checkout configuration, and available payment methods.
@@ -0,0 +1,17 @@
1
+ //#region development/refresh-plugin-types.d.mts
2
+ type PluginContractFetcher = (url: URL, options: RequestInit) => Promise<Response>;
3
+ declare function refreshPluginTypes(options: {
4
+ storeId: string;
5
+ apiUrl?: string;
6
+ output?: string;
7
+ projectRoot?: string;
8
+ signal?: AbortSignal;
9
+ timeoutMs?: number;
10
+ fetcher?: PluginContractFetcher;
11
+ }): Promise<{
12
+ changed: boolean;
13
+ output: string;
14
+ types: string;
15
+ }>;
16
+ //#endregion
17
+ export { type PluginContractFetcher, refreshPluginTypes };
@@ -0,0 +1,204 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region development/plugin-types.mjs
6
+ const types = /* @__PURE__ */ new Set([
7
+ "object",
8
+ "array",
9
+ "string",
10
+ "number",
11
+ "integer",
12
+ "boolean",
13
+ "null"
14
+ ]);
15
+ const safeKey = (value) => typeof value === "string" && /^[a-zA-Z][a-zA-Z0-9_\-]{0,63}$/.test(value) && ![
16
+ "constructor",
17
+ "prototype",
18
+ "then"
19
+ ].includes(value);
20
+ const reservedParameters = new Set("await break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public".split(" "));
21
+ function schemaType(schema, depth = 0) {
22
+ if (depth > 8 || !schema || typeof schema !== "object" || Array.isArray(schema)) throw new Error("Invalid plugin type schema");
23
+ if (schema.enum) {
24
+ if (!Array.isArray(schema.enum) || !schema.enum.length || schema.enum.length > 64 || schema.enum.some((value) => value !== null && ![
25
+ "string",
26
+ "number",
27
+ "boolean"
28
+ ].includes(typeof value))) throw new Error("Invalid plugin enum");
29
+ return schema.enum.map((value) => JSON.stringify(value)).join(" | ");
30
+ }
31
+ if (schema.anyOf) {
32
+ if (!Array.isArray(schema.anyOf) || !schema.anyOf.length || schema.anyOf.length > 8) throw new Error("Invalid plugin union");
33
+ return schema.anyOf.map((value) => `(${schemaType(value, depth + 1)})`).join(" | ");
34
+ }
35
+ if (!types.has(schema.type)) throw new Error("Unsupported plugin type");
36
+ if (schema.type === "array") return `Array<${schemaType(schema.items, depth + 1)}>`;
37
+ if (schema.type === "object") {
38
+ const properties = schema.properties ?? {};
39
+ if (!properties || typeof properties !== "object" || Array.isArray(properties) || Object.keys(properties).length > 32) throw new Error("Invalid plugin object");
40
+ const required = schema.required ?? [];
41
+ if (!Array.isArray(required) || !required.every((value) => typeof value === "string" && Object.hasOwn(properties, value))) throw new Error("Invalid required properties");
42
+ return `{ ${Object.entries(properties).map(([name, value]) => `${JSON.stringify(name)}${required.includes(name) ? "" : "?"}: ${schemaType(value, depth + 1)};`).join(" ")} }`;
43
+ }
44
+ return schema.type === "integer" ? "number" : schema.type;
45
+ }
46
+ function generatePluginTypes(metadata) {
47
+ if (!metadata || !Array.isArray(metadata.plugins) || metadata.plugins.length > 100) throw new Error("Invalid installed plugin metadata");
48
+ const names = /* @__PURE__ */ new Set();
49
+ const lines = [
50
+ "// Generated by Caldik.",
51
+ `import type { CaldikRequest${metadata.plugins.some((plugin) => plugin?.methods && typeof plugin.methods === "object" && Object.values(plugin.methods).some((method) => method?.subscription)) ? ", CaldikSubscriptionOptions" : ""} } from "@caldik/client";`,
52
+ "",
53
+ "declare module \"@caldik/client\" {",
54
+ " interface CaldikPlugins {"
55
+ ];
56
+ for (const plugin of [...metadata.plugins].sort((a, b) => String(a.namespace).localeCompare(String(b.namespace)))) {
57
+ if (!safeKey(plugin.namespace) || names.has(plugin.namespace) || !plugin.methods || typeof plugin.methods !== "object" || Array.isArray(plugin.methods) || Object.keys(plugin.methods).length > 16) throw new Error("Invalid plugin namespace");
58
+ names.add(plugin.namespace);
59
+ lines.push(` readonly ${JSON.stringify(plugin.namespace)}: {`);
60
+ const tree = Object.create(null);
61
+ for (const [name, method] of Object.entries(plugin.methods).sort(([a], [b]) => a.localeCompare(b))) {
62
+ const keys = name.split(".");
63
+ if (keys.length > 4 || !keys.every(safeKey) || !method || typeof method !== "object") throw new Error("Invalid plugin method");
64
+ let branch = tree;
65
+ for (const key of keys.slice(0, -1)) {
66
+ branch[key] ??= { children: Object.create(null) };
67
+ if (!branch[key].children) throw new Error("Conflicting plugin method group");
68
+ branch = branch[key].children;
69
+ }
70
+ if (branch[keys.at(-1)]) throw new Error("Conflicting plugin method group");
71
+ branch[keys.at(-1)] = { method };
72
+ }
73
+ const render = (branch, indent) => {
74
+ for (const [name, entry] of Object.entries(branch)) {
75
+ if (entry.children) {
76
+ lines.push(`${indent}readonly ${JSON.stringify(name)}: {`);
77
+ render(entry.children, indent + " ");
78
+ lines.push(`${indent}};`);
79
+ continue;
80
+ }
81
+ const method = entry.method, input = schemaType(method.input), output = schemaType(method.output);
82
+ let parameters;
83
+ if (method.parameters !== void 0) {
84
+ const properties = method.input.properties ?? {}, required = method.input.required ?? [];
85
+ if (!Array.isArray(method.parameters) || !method.parameters.length || method.parameters.length > 4 || new Set(method.parameters).size !== method.parameters.length || method.input.type !== "object" || method.parameters.some((key) => !safeKey(key) || !Object.hasOwn(properties, key)) || Object.keys(properties).some((key) => !method.parameters.includes(key))) throw new Error("Invalid plugin parameters");
86
+ let optional = false;
87
+ parameters = method.parameters.map((key, index) => {
88
+ const needed = required.includes(key);
89
+ if (needed && optional) throw new Error("Required parameter follows optional parameter");
90
+ optional ||= !needed;
91
+ let identifier = key;
92
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key) || reservedParameters.has(key)) {
93
+ identifier = `argument_${index + 1}`;
94
+ while (method.parameters.includes(identifier)) identifier += "_";
95
+ }
96
+ return `${identifier}${needed ? "" : "?"}: ${schemaType(properties[key])}`;
97
+ }).join(", ");
98
+ } else {
99
+ const empty = method.input.type === "object" && !Object.keys(method.input.properties ?? {}).length;
100
+ const optional = method.input.type === "object" && !method.input.required?.length;
101
+ parameters = empty ? "" : `input${optional ? "?" : ""}: ${input}`;
102
+ }
103
+ if (method.subscription) {
104
+ const events = method.subscription.events;
105
+ if (!method.parameters || method.parameters.some((key) => !method.input.required?.includes(key)) || !events || typeof events !== "object" || Array.isArray(events) || !Object.keys(events).length || Object.keys(events).length > 8 || Object.keys(events).some((key) => !safeKey(key) || [
106
+ "onError",
107
+ "ready",
108
+ "error"
109
+ ].includes(key))) throw new Error("Invalid plugin subscription");
110
+ const type = `{ ${Object.entries(events).map(([key, schema]) => `${JSON.stringify(key)}: ${schemaType(schema)};`).join(" ")} }`;
111
+ let optionName = "callbacks";
112
+ while (method.parameters.includes(optionName)) optionName += "_";
113
+ lines.push(`${indent}readonly ${JSON.stringify(name)}: (${parameters}${parameters ? ", " : ""}${optionName}: CaldikSubscriptionOptions<${type}>) => () => void;`);
114
+ } else lines.push(`${indent}readonly ${JSON.stringify(name)}: (${parameters}) => CaldikRequest<${output}>;`);
115
+ }
116
+ };
117
+ render(tree, " ");
118
+ lines.push(" };");
119
+ }
120
+ lines.push(" }", "}", "");
121
+ return lines.join("\n");
122
+ }
123
+ const packageTypesPath = resolve(fileURLToPath(new URL("../", import.meta.url)), "types/plugins.d.ts");
124
+ const generatedPrefix = "// Generated by Caldik.";
125
+ async function readOptional(path) {
126
+ try {
127
+ return await readFile(path, "utf8");
128
+ } catch (error) {
129
+ if (error.code === "ENOENT") return "";
130
+ throw error;
131
+ }
132
+ }
133
+ /** Replace the directory entry so Bun's hard-linked package cache is never modified. */
134
+ async function writeGeneratedTypes(output, types) {
135
+ const previous = await readOptional(output);
136
+ if (previous && !previous.startsWith(generatedPrefix)) throw new Error("Output contains handwritten declarations; choose another output path");
137
+ if (previous === types) return false;
138
+ await mkdir(dirname(output), { recursive: true });
139
+ const temporary = `${output}.${randomUUID()}.tmp`;
140
+ try {
141
+ await writeFile(temporary, types, "utf8");
142
+ await rename(temporary, output);
143
+ } finally {
144
+ await rm(temporary, { force: true });
145
+ }
146
+ return true;
147
+ }
148
+ async function responseText(response) {
149
+ const reader = response.body?.getReader();
150
+ if (!reader) throw new Error("Empty plugin contract response");
151
+ const chunks = [];
152
+ let bytes = 0;
153
+ try {
154
+ for (;;) {
155
+ const { value, done } = await reader.read();
156
+ if (done) break;
157
+ bytes += value.byteLength;
158
+ if (bytes > 1048576) throw new Error("Plugin contracts exceed 1 MiB");
159
+ chunks.push(value);
160
+ }
161
+ } finally {
162
+ await reader.cancel().catch(() => void 0);
163
+ }
164
+ return Buffer.concat(chunks).toString("utf8");
165
+ }
166
+ async function refreshPluginTypes({ storeId, apiUrl = "https://api.caldik.com", output = packageTypesPath, projectRoot, signal, timeoutMs = 5e3, fetcher = fetch }) {
167
+ signal?.throwIfAborted();
168
+ if (typeof storeId !== "string" || !/^STORE_[a-f0-9]{32}$/i.test(storeId)) throw new Error("The development store ID is invalid");
169
+ const base = new URL(apiUrl);
170
+ if (!["https:", "http:"].includes(base.protocol) || base.username || base.password || base.search || base.hash) throw new Error("API URL must be an HTTP(S) base URL without credentials, query or fragment");
171
+ const url = new URL(`${base.href.replace(/\/+$/, "")}/v1/stores/${encodeURIComponent(storeId)}/plugins/sdk`);
172
+ output = resolve(output);
173
+ if (!output.endsWith(".d.ts")) throw new Error("Plugin types must use a .d.ts output path");
174
+ const writeTypes = async (types) => {
175
+ const changed = await writeGeneratedTypes(output, types);
176
+ if (projectRoot && output === packageTypesPath) {
177
+ const snapshot = resolve(projectRoot, await stat(resolve(projectRoot, "src")).then((value) => value.isDirectory()).catch(() => false) ? "src/caldik.plugins.d.ts" : "caldik.plugins.d.ts");
178
+ const current = await readOptional(snapshot);
179
+ if (!current || current.startsWith(generatedPrefix)) await writeGeneratedTypes(snapshot, types);
180
+ }
181
+ return changed;
182
+ };
183
+ const context = `// Context: ${createHash("sha256").update(url.href).digest("hex")}\n`;
184
+ const previous = await readOptional(output);
185
+ if (previous.startsWith(generatedPrefix) && previous.includes("// Context: ") && !previous.includes(context)) await writeTypes(generatePluginTypes({ plugins: [] }));
186
+ const requestSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
187
+ const response = await fetcher(url, {
188
+ signal: requestSignal,
189
+ credentials: "omit",
190
+ headers: { Accept: "application/json" }
191
+ });
192
+ if (!response.ok) throw new Error(`Plugin contract request failed (HTTP ${response.status})`);
193
+ const metadata = JSON.parse(await responseText(response));
194
+ if (metadata.success !== true || metadata.storeId !== storeId) throw new Error("The API returned invalid contracts or contracts for another store");
195
+ const types = generatePluginTypes(metadata).replace(/\n/, `\n${context}`);
196
+ requestSignal.throwIfAborted();
197
+ return {
198
+ changed: await writeTypes(types),
199
+ output,
200
+ types
201
+ };
202
+ }
203
+ //#endregion
204
+ export { refreshPluginTypes };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="../types/plugins.d.ts" />
1
2
  //#region src/payment-providers.generated.d.ts
2
3
  declare const paymentProviderIds: readonly ["customer_balance", "stripe", "ovgc"];
3
4
  declare const checkoutPaymentProviderIds: readonly ["customer_balance", "stripe", "ovgc"];
@@ -200,9 +201,10 @@ interface StorefrontOrder {
200
201
  subtotal: string;
201
202
  amount: string;
202
203
  discount: {
203
- code: string;
204
+ code: string | null;
204
205
  percentage: string;
205
206
  amount: string;
207
+ source: "code" | "plugin";
206
208
  } | null;
207
209
  currency: string;
208
210
  }
@@ -294,15 +296,7 @@ interface CustomerEmailLoginInput {
294
296
  email: string;
295
297
  rememberMe?: boolean;
296
298
  }
297
- interface CustomerSessionResponse {
298
- success: boolean;
299
- authenticated: boolean;
300
- customer: {
301
- id: string;
302
- email: string | null;
303
- } | null;
304
- expiresAt: number | null;
305
- }
299
+ type CustomerSessionResponse = void;
306
300
  interface CustomerPasswordResetResponse extends CustomerLoginResponse {
307
301
  expiresAt: number;
308
302
  message: string;
@@ -330,14 +324,73 @@ interface CustomerProfileResponse {
330
324
  success: boolean;
331
325
  customer: {
332
326
  id: string;
327
+ name: string | null;
333
328
  email: string | null;
334
- extensions?: Record<string, Record<string, string | number | boolean>>;
329
+ phone: string | null; /** All orders belonging to this customer in this store. */
330
+ totalOrders: number; /** Successfully paid order amounts in the store currency, as a decimal string. */
331
+ totalSpent: string;
332
+ currency: string;
333
+ createdAt: number;
334
+ updatedAt: number;
335
+ extensions: Record<string, Record<string, string | number | boolean>>;
335
336
  };
336
- balances: Array<{
337
+ }
338
+ interface CustomerBalanceResponse {
339
+ success: boolean;
340
+ balance: {
337
341
  currency: string;
338
342
  amount: string;
343
+ };
344
+ }
345
+ interface CustomerOrderListOptions {
346
+ limit?: number;
347
+ cursor?: string;
348
+ }
349
+ interface CustomerOrder {
350
+ id: string;
351
+ shortId: string;
352
+ displayId: string;
353
+ paymentStatus: string;
354
+ fulfillmentStatus: string;
355
+ amount: string;
356
+ subtotal: string;
357
+ currency: string;
358
+ createdAt: number;
359
+ updatedAt: number;
360
+ discount: {
361
+ code: string | null;
362
+ percentage: string;
363
+ amount: string;
364
+ source: "code" | "plugin";
365
+ } | null;
366
+ }
367
+ interface CustomerOrderItem {
368
+ id: string;
369
+ productId: string | null;
370
+ productVariantId: string | null;
371
+ name: string;
372
+ description: string | null;
373
+ quantity: number;
374
+ unitAmount: string;
375
+ totalAmount: string;
376
+ customFields: Array<{
377
+ id: string;
378
+ name: string;
379
+ inputType: string;
380
+ value: string | number | boolean;
339
381
  }>;
340
382
  }
383
+ interface CustomerOrdersResponse {
384
+ success: boolean;
385
+ orders: CustomerOrder[];
386
+ nextCursor: string | null;
387
+ }
388
+ interface CustomerOrderResponse {
389
+ success: boolean;
390
+ order: CustomerOrder & {
391
+ items: CustomerOrderItem[];
392
+ };
393
+ }
341
394
  interface CustomerLoginSettings {
342
395
  passwordEnabled: boolean;
343
396
  }
@@ -373,6 +426,8 @@ interface CheckoutPaymentMethodsResponse {
373
426
  //#endregion
374
427
  //#region src/internal/types.d.ts
375
428
  interface RequestOptions {
429
+ /** Response intentionally has no JSON body. */
430
+ empty?: boolean;
376
431
  edgeCache?: boolean;
377
432
  /** Internal authorization policy. Protected hosted calls are the default. */
378
433
  access?: "public" | "runtime";
@@ -380,6 +435,29 @@ interface RequestOptions {
380
435
  headers?: Record<string, string>;
381
436
  }
382
437
  //#endregion
438
+ //#region src/internal/request.d.ts
439
+ interface RequestDescriptor {
440
+ method: "GET" | "POST";
441
+ path: string;
442
+ params?: Record<string, string | number>;
443
+ input?: unknown;
444
+ empty?: boolean;
445
+ }
446
+ /** Normal Promise scheduled on the next microtask so synchronous calls can join a batch. */
447
+ declare class CaldikRequest<T> extends Promise<T> {
448
+ readonly owner: object;
449
+ readonly descriptor: RequestDescriptor;
450
+ static get [Symbol.species](): PromiseConstructor;
451
+ private dispatched;
452
+ private bound;
453
+ private resolveRequest;
454
+ private rejectRequest;
455
+ constructor(owner: object, descriptor: RequestDescriptor, execute: () => Promise<T>);
456
+ get started(): boolean;
457
+ cancel(error: unknown): void;
458
+ bind(promise: Promise<T>): void;
459
+ }
460
+ //#endregion
383
461
  //#region src/internal/client.d.ts
384
462
  type HttpClientOptions = {
385
463
  apiKey?: string;
@@ -388,6 +466,10 @@ type HttpClientOptions = {
388
466
  request?: Request;
389
467
  onSetCookie?: (cookie: string) => void | Promise<void>;
390
468
  };
469
+ declare class CaldikHttpError extends Error {
470
+ readonly status: number;
471
+ constructor(message: string, status: number);
472
+ }
391
473
  declare class CaldikHttpClient {
392
474
  private readonly apiKey?;
393
475
  private readonly apiUrl;
@@ -403,60 +485,78 @@ declare class CaldikHttpClient {
403
485
  customerAuthenticated(): Promise<void>;
404
486
  private captureCookies;
405
487
  private baseUrl;
488
+ private response;
406
489
  private request;
407
- get<T>(path: string, params?: Record<string, string | number>, options?: RequestOptions): Promise<T>;
408
- post<T>(path: string, input?: unknown, options?: RequestOptions): Promise<T>;
490
+ stream(path: string, input: unknown, signal: AbortSignal, cursor?: string): Promise<Response>;
491
+ get<T>(path: string, params?: Record<string, string | number>, options?: RequestOptions): CaldikRequest<T>;
492
+ post<T>(path: string, input?: unknown, options?: RequestOptions): CaldikRequest<T>;
493
+ }
494
+ //#endregion
495
+ //#region src/batch.d.ts
496
+ type BatchInput = Record<string, CaldikRequest<any>>;
497
+ type BatchResult<T extends BatchInput> = { [K in keyof T]: Awaited<T[K]> };
498
+ declare class CaldikBatchError extends Error {
499
+ readonly results: Record<string, unknown>;
500
+ readonly errors: Record<string, {
501
+ status: number;
502
+ body: unknown;
503
+ }>;
504
+ constructor(results: Record<string, unknown>, errors: Record<string, {
505
+ status: number;
506
+ body: unknown;
507
+ }>);
409
508
  }
509
+ declare function batchRequests<T extends BatchInput>(client: CaldikHttpClient, input: T): Promise<BatchResult<T>>;
410
510
  //#endregion
411
511
  //#region src/categories.d.ts
412
512
  declare class CategoriesApi {
413
513
  private client;
414
514
  constructor(client: CaldikHttpClient);
415
- list(options?: number | CategoryListOptions): Promise<CategoriesResponse>;
416
- get(categoryId: string): Promise<CategoryResponse>;
515
+ list(options?: number | CategoryListOptions): CaldikRequest<CategoriesResponse>;
516
+ get(categoryId: string): CaldikRequest<CategoryResponse>;
417
517
  }
418
518
  //#endregion
419
519
  //#region src/products.d.ts
420
520
  declare class ProductsApi {
421
521
  private client;
422
522
  constructor(client: CaldikHttpClient);
423
- list(): Promise<ProductsResponse>;
424
- get(productIdOrSlug: string, options?: ProductGetOptions): Promise<ProductResponse>;
523
+ list(): CaldikRequest<ProductsResponse>;
524
+ get(productIdOrSlug: string, options?: ProductGetOptions): CaldikRequest<ProductResponse>;
425
525
  }
426
526
  //#endregion
427
527
  //#region src/reviews.d.ts
428
528
  declare class ReviewsApi {
429
529
  private client;
430
530
  constructor(client: CaldikHttpClient);
431
- list(limit?: number): Promise<ReviewsResponse>;
531
+ list(limit?: number): CaldikRequest<ReviewsResponse>;
432
532
  }
433
533
  //#endregion
434
534
  //#region src/store.d.ts
435
535
  declare class StoreApi {
436
536
  private client;
437
537
  constructor(client: CaldikHttpClient);
438
- get(): Promise<StoreResponse>;
538
+ get(): CaldikRequest<StoreResponse>;
439
539
  }
440
540
  //#endregion
441
541
  //#region src/checkout.d.ts
442
542
  declare class CheckoutApi {
443
543
  private client;
444
544
  readonly paymentmethods: {
445
- list: () => Promise<CheckoutPaymentMethodsResponse>;
545
+ list: () => CaldikRequest<CheckoutPaymentMethodsResponse>;
446
546
  };
447
547
  readonly settings: {
448
- get: () => Promise<CheckoutSettingsResponse>;
548
+ get: () => CaldikRequest<CheckoutSettingsResponse>;
449
549
  };
450
550
  constructor(client: CaldikHttpClient);
451
- create(input: CreateOrderInput): Promise<OrderResponse>;
452
- payWithBalance(orderId: string): Promise<OrderResponse>;
551
+ create(input: CreateOrderInput): CaldikRequest<OrderResponse>;
552
+ payWithBalance(orderId: string): CaldikRequest<OrderResponse>;
453
553
  }
454
554
  //#endregion
455
555
  //#region src/customer.d.ts
456
556
  declare class CustomerApi {
457
557
  private client;
458
558
  readonly settings: {
459
- get: () => Promise<CustomerLoginSettingsResponse>;
559
+ get: () => CaldikRequest<CustomerLoginSettingsResponse>;
460
560
  };
461
561
  readonly login: {
462
562
  (email: string, options?: {
@@ -477,18 +577,27 @@ declare class CustomerApi {
477
577
  requestReset: (email: string) => Promise<CustomerPasswordResetResponse>;
478
578
  reset: (input: CustomerPasswordResetInput) => Promise<CustomerPasswordResetVerifyResponse>;
479
579
  };
580
+ readonly orders: {
581
+ list: (options?: CustomerOrderListOptions) => CaldikRequest<CustomerOrdersResponse>;
582
+ get: (id: string) => CaldikRequest<CustomerOrderResponse>;
583
+ };
584
+ readonly balance: {
585
+ get: (options?: {
586
+ currency?: string;
587
+ }) => CaldikRequest<CustomerBalanceResponse>;
588
+ };
480
589
  constructor(client: CaldikHttpClient);
481
- /** Check a store-scoped session without raising an error for signed-out customers. */
482
- session(): Promise<CustomerSessionResponse>;
483
- /** Read the signed-in customer and their balances. */
484
- me(): Promise<CustomerProfileResponse>;
590
+ /** Resolves without a body on HTTP 200; rejects with status 401 when signed out. */
591
+ session(): CaldikRequest<void>;
592
+ /** Read the signed-in customer's profile. */
593
+ me(): CaldikRequest<CustomerProfileResponse>;
485
594
  }
486
595
  //#endregion
487
596
  //#region src/subscriptions.d.ts
488
597
  declare class SubscriptionsApi {
489
598
  private client;
490
599
  constructor(client: CaldikHttpClient);
491
- create(input: CreateSubscriptionInput): Promise<SubscriptionResponse>;
600
+ create(input: CreateSubscriptionInput): CaldikRequest<SubscriptionResponse>;
492
601
  }
493
602
  //#endregion
494
603
  //#region src/discounts.d.ts
@@ -511,7 +620,7 @@ interface DiscountQuoteResponse {
511
620
  declare class DiscountsApi {
512
621
  private client;
513
622
  constructor(client: CaldikHttpClient);
514
- check(input: CheckDiscountInput): Promise<DiscountQuoteResponse>;
623
+ check(input: CheckDiscountInput): CaldikRequest<DiscountQuoteResponse>;
515
624
  }
516
625
  //#endregion
517
626
  //#region src/cart.d.ts
@@ -549,7 +658,7 @@ declare class CartApi {
549
658
  private resolve;
550
659
  private reset;
551
660
  private mutate;
552
- private save;
661
+ private saveDiscount;
553
662
  private quote;
554
663
  get(): Promise<CartSnapshot>;
555
664
  add(item: ProductCommerceItem): Promise<CartSnapshot>;
@@ -562,7 +671,14 @@ declare class CartApi {
562
671
  checkout(input?: CartCheckoutInput): Promise<OrderResponse>;
563
672
  }
564
673
  //#endregion
674
+ //#region src/internal/realtime.d.ts
675
+ type CaldikSubscriptionOptions<T extends Record<string, unknown>> = { [K in keyof T]?: (value: T[K]) => void } & {
676
+ onError?: (error: Error) => void;
677
+ };
678
+ //#endregion
565
679
  //#region src/index.d.ts
680
+ /** Populated automatically from the store's plugin contracts during caldik dev. */
681
+ interface CaldikPlugins {}
566
682
  interface CaldikClientOptions {
567
683
  /** Store ID override for external servers or local browser development. */
568
684
  storeId?: string;
@@ -581,10 +697,18 @@ declare class CaldikClient {
581
697
  readonly store: StoreApi;
582
698
  readonly checkout: CheckoutApi;
583
699
  readonly customer: CustomerApi;
700
+ readonly customers: CustomerApi;
701
+ readonly orders: {
702
+ list: (options?: CustomerOrderListOptions) => CaldikRequest<CustomerOrdersResponse>;
703
+ get: (id: string) => CaldikRequest<CustomerOrderResponse>;
704
+ };
705
+ readonly plugins: CaldikPlugins;
584
706
  readonly subscriptions: SubscriptionsApi;
585
707
  readonly discounts: DiscountsApi;
586
708
  readonly cart: CartApi;
709
+ private readonly client;
587
710
  constructor(options?: CaldikClientOptions);
711
+ batch<T extends BatchInput>(input: T): Promise<BatchResult<T>>;
588
712
  }
589
713
  //#endregion
590
- export { AppliedDiscount, CaldikClient, CaldikClientOptions, CartApi, CartCheckoutInput, CartDiscount, CartItem, CartSnapshot, CategoriesResponse, Category, CategoryDetails, CategoryInclude, CategoryListOptions, CategoryMetadata, CategoryProduct, CategoryResponse, CheckDiscountInput, CheckoutPayment, CheckoutPaymentMethod, CheckoutPaymentMethodsResponse, CheckoutPaymentProviderId, CheckoutSettings, CheckoutSettingsResponse, CommerceItem, CreateOrderInput, CreateSubscriptionInput, CustomerEmailLoginInput, CustomerLoginResponse, CustomerLoginSettings, CustomerLoginSettingsResponse, CustomerLoginVerifyResponse, CustomerPasswordInput, CustomerPasswordResetInput, CustomerPasswordResetResponse, CustomerPasswordResetVerifyResponse, CustomerPasswordResponse, CustomerProfileResponse, CustomerRegistrationInput, CustomerRegistrationResponse, CustomerSessionResponse, DiscountQuoteResponse, DiscountsApi, ManualCommerceItem, OrderResponse, PaymentProviderId, Product, ProductCommerceItem, ProductCustomField, ProductCustomFieldInputType, ProductCustomFieldOption, ProductGetOptions, ProductImage, ProductMetadata, ProductResponse, ProductTag, ProductVariant, ProductsResponse, Review, ReviewsResponse, Store, StoreResponse, StoreSocials, StoreWebhookEvent, StoreWebhookEventName, StorefrontMetadata, StorefrontOrder, SubscriptionPaymentProviderId, SubscriptionResponse, checkoutPaymentProviderIds, paymentProviderIds, storeWebhookEventNames, subscriptionPaymentProviderIds };
714
+ export { AppliedDiscount, BatchInput, BatchResult, CaldikBatchError, CaldikClient, CaldikClientOptions, CaldikHttpError, CaldikPlugins, CaldikRequest, type CaldikSubscriptionOptions, CartApi, CartCheckoutInput, CartDiscount, CartItem, CartSnapshot, CategoriesResponse, Category, CategoryDetails, CategoryInclude, CategoryListOptions, CategoryMetadata, CategoryProduct, CategoryResponse, CheckDiscountInput, CheckoutPayment, CheckoutPaymentMethod, CheckoutPaymentMethodsResponse, CheckoutPaymentProviderId, CheckoutSettings, CheckoutSettingsResponse, CommerceItem, CreateOrderInput, CreateSubscriptionInput, CustomerBalanceResponse, CustomerEmailLoginInput, CustomerLoginResponse, CustomerLoginSettings, CustomerLoginSettingsResponse, CustomerLoginVerifyResponse, CustomerOrder, CustomerOrderItem, CustomerOrderListOptions, CustomerOrderResponse, CustomerOrdersResponse, CustomerPasswordInput, CustomerPasswordResetInput, CustomerPasswordResetResponse, CustomerPasswordResetVerifyResponse, CustomerPasswordResponse, CustomerProfileResponse, CustomerRegistrationInput, CustomerRegistrationResponse, CustomerSessionResponse, DiscountQuoteResponse, DiscountsApi, ManualCommerceItem, OrderResponse, PaymentProviderId, Product, ProductCommerceItem, ProductCustomField, ProductCustomFieldInputType, ProductCustomFieldOption, ProductGetOptions, ProductImage, ProductMetadata, ProductResponse, ProductTag, ProductVariant, ProductsResponse, Review, ReviewsResponse, Store, StoreResponse, StoreSocials, StoreWebhookEvent, StoreWebhookEventName, StorefrontMetadata, StorefrontOrder, SubscriptionPaymentProviderId, SubscriptionResponse, batchRequests, checkoutPaymentProviderIds, paymentProviderIds, storeWebhookEventNames, subscriptionPaymentProviderIds };
package/dist/index.mjs CHANGED
@@ -23,6 +23,56 @@ function defaultApiUrl(browser) {
23
23
  return environmentApiUrl(browser) || (typeof API_URL !== "undefined" ? API_URL : "https://api.caldik.com");
24
24
  }
25
25
  //#endregion
26
+ //#region src/internal/request.ts
27
+ /** Normal Promise scheduled on the next microtask so synchronous calls can join a batch. */
28
+ var CaldikRequest = class extends Promise {
29
+ owner;
30
+ descriptor;
31
+ static get [Symbol.species]() {
32
+ return Promise;
33
+ }
34
+ dispatched = false;
35
+ bound = false;
36
+ resolveRequest;
37
+ rejectRequest;
38
+ constructor(owner, descriptor, execute) {
39
+ let resolve;
40
+ let reject;
41
+ super((yes, no) => {
42
+ resolve = yes;
43
+ reject = no;
44
+ });
45
+ this.owner = owner;
46
+ this.descriptor = descriptor;
47
+ this.resolveRequest = resolve;
48
+ this.rejectRequest = reject;
49
+ queueMicrotask(() => {
50
+ if (this.bound) return;
51
+ this.dispatched = true;
52
+ try {
53
+ execute().then(resolve, reject);
54
+ } catch (error) {
55
+ reject(error);
56
+ }
57
+ });
58
+ }
59
+ get started() {
60
+ return this.dispatched || this.bound;
61
+ }
62
+ cancel(error) {
63
+ if (this.started) return;
64
+ this.bound = true;
65
+ this.rejectRequest(error);
66
+ this.catch(() => void 0);
67
+ }
68
+ bind(promise) {
69
+ if (this.started) throw new Error("An already started request cannot be batched");
70
+ this.bound = true;
71
+ promise.then(this.resolveRequest, this.rejectRequest);
72
+ this.catch(() => void 0);
73
+ }
74
+ };
75
+ //#endregion
26
76
  //#region src/internal/client.ts
27
77
  const STORE_ID_PATTERN = /^STORE_[a-f0-9]{32}$/i;
28
78
  const STOREFRONT_STORE_ID_ATTRIBUTE = "data-caldik-store-id";
@@ -74,6 +124,7 @@ var CaldikHttpClient = class {
74
124
  return this.storeId || boundRuntimeStoreId();
75
125
  }
76
126
  hasCartSession() {
127
+ if (this.cookies.has(`cart_${this.storeContextId()}`) || this.cookies.has("session")) return true;
77
128
  const prefix = `caldik_cart_session_${this.storeContextId()}`;
78
129
  return [...this.cookies.keys()].some((name) => name === prefix || name.startsWith(`${prefix}_`));
79
130
  }
@@ -112,7 +163,7 @@ var CaldikHttpClient = class {
112
163
  if (!boundStoreId && !this.apiKey) throw new Error("Caldik authentication is unavailable. External servers must provide a private apiKey.");
113
164
  return `${this.apiUrl.replace(/\/+$/, "")}/v1/stores/${encodeURIComponent(storeId)}`;
114
165
  }
115
- async request(method, path, input, params, options) {
166
+ async response(method, path, input, params, options, signal) {
116
167
  const url = new URL(`${this.baseUrl()}${path}`);
117
168
  if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));
118
169
  const headers = { ...options?.headers };
@@ -128,20 +179,45 @@ var CaldikHttpClient = class {
128
179
  headers,
129
180
  ...input !== void 0 ? { body: JSON.stringify(input) } : {},
130
181
  cache: "default",
131
- credentials: "include"
182
+ credentials: "include",
183
+ signal
132
184
  });
133
185
  if (!isBrowserRuntime() && options?.access === "public") await this.captureCookies(response);
134
186
  if (!response.ok) {
135
187
  const error = await response.clone().json().catch(() => null);
136
188
  throw new CaldikHttpError(typeof error?.message === "string" && error.message ? error.message : `${response.status} ${response.statusText}`.trim(), response.status);
137
189
  }
190
+ return response;
191
+ }
192
+ async request(method, path, input, params, options) {
193
+ const response = await this.response(method, path, input, params, options);
194
+ if (options?.empty) return void 0;
138
195
  return response.json();
139
196
  }
197
+ stream(path, input, signal, cursor) {
198
+ return this.response("POST", path, input, void 0, {
199
+ access: "public",
200
+ headers: {
201
+ Accept: "text/event-stream",
202
+ ...cursor ? { "Last-Event-ID": cursor } : {}
203
+ }
204
+ }, signal);
205
+ }
140
206
  get(path, params, options) {
141
- return this.request("GET", path, void 0, params, options);
207
+ return new CaldikRequest(this, {
208
+ method: "GET",
209
+ path,
210
+ params,
211
+ empty: options?.empty
212
+ }, () => this.request("GET", path, void 0, params, options));
142
213
  }
143
214
  post(path, input, options) {
144
- return this.request("POST", path, input, void 0, options);
215
+ return new CaldikRequest(this, {
216
+ method: "POST",
217
+ path,
218
+ input,
219
+ empty: options?.empty
220
+ }, () => this.request("POST", path, input, void 0, options));
145
221
  }
146
222
  };
147
223
  //#endregion
@@ -224,8 +300,15 @@ var CustomerApi = class {
224
300
  login;
225
301
  register;
226
302
  password;
303
+ orders;
304
+ balance;
227
305
  constructor(client) {
228
306
  this.client = client;
307
+ this.orders = {
308
+ list: (options = {}) => client.get("/customers/orders", { ...options }, { access: "public" }),
309
+ get: (id) => client.get(`/customers/orders/${encodeURIComponent(id)}`, void 0, { access: "public" })
310
+ };
311
+ this.balance = { get: (options = {}) => client.get("/customers/balance", { ...options }, { access: "public" }) };
229
312
  this.settings = { get: () => this.client.get("/customers/settings", void 0, { access: "public" }) };
230
313
  let pendingResetId;
231
314
  this.password = {
@@ -293,11 +376,14 @@ verify: async (code, challengeId = pendingRegistrationId) => {
293
376
  return response;
294
377
  } });
295
378
  }
296
- /** Check a store-scoped session without raising an error for signed-out customers. */
379
+ /** Resolves without a body on HTTP 200; rejects with status 401 when signed out. */
297
380
  session() {
298
- return this.client.get("/customers/session", void 0, { access: "public" });
381
+ return this.client.get("/customers/session", void 0, {
382
+ access: "public",
383
+ empty: true
384
+ });
299
385
  }
300
- /** Read the signed-in customer and their balances. */
386
+ /** Read the signed-in customer's profile. */
301
387
  me() {
302
388
  return this.client.get("/customers/me", void 0, { access: "public" });
303
389
  }
@@ -378,7 +464,6 @@ function discountFromQuote(quote) {
378
464
  var CartApi = class {
379
465
  client;
380
466
  cache = {
381
- version: 2,
382
467
  reference: null,
383
468
  snapshot: {
384
469
  items: [],
@@ -411,18 +496,16 @@ var CartApi = class {
411
496
  if (!storage) return;
412
497
  try {
413
498
  const parsed = JSON.parse(storage.getItem(this.key()) || "null");
414
- if (parsed?.version === 2) {
499
+ if (parsed?.snapshot) {
415
500
  const snapshot = parsed.snapshot;
416
501
  if (snapshot && Array.isArray(snapshot.items) && snapshot.items.every(isCartItem) && (parsed.reference === null || isReference(parsed.reference))) this.cache = {
417
- version: 2,
418
502
  reference: parsed.reference,
419
503
  snapshot: cloneSnapshot({
420
504
  items: snapshot.items,
421
505
  discount: isCartDiscount(snapshot.discount) ? snapshot.discount : null
422
506
  })
423
507
  };
424
- } else if (parsed?.version === 1 && Array.isArray(parsed.items) && parsed.items.every(isCartItem)) this.cache = {
425
- version: 2,
508
+ } else if (parsed && Array.isArray(parsed.items) && parsed.items.every(isCartItem)) this.cache = {
426
509
  reference: null,
427
510
  snapshot: cloneSnapshot({
428
511
  items: parsed.items,
@@ -444,7 +527,6 @@ var CartApi = class {
444
527
  }
445
528
  write(cart, token = this.cache.reference?.token, discount = null) {
446
529
  this.cache = {
447
- version: 2,
448
530
  reference: {
449
531
  id: cart.id,
450
532
  token: cart.customerId ? null : token
@@ -492,7 +574,6 @@ var CartApi = class {
492
574
  }
493
575
  reset() {
494
576
  this.cache = {
495
- version: 2,
496
577
  reference: null,
497
578
  snapshot: {
498
579
  items: [],
@@ -502,25 +583,11 @@ var CartApi = class {
502
583
  this.persistCache();
503
584
  }
504
585
  async mutate(operation, seed = true) {
505
- let cart = await this.resolve(false, seed);
506
- let response;
507
- try {
508
- response = await operation(cart);
509
- } catch (error) {
510
- if (!(error instanceof CaldikHttpError) || error.status !== 409) throw error;
511
- const refreshed = await this.resolve();
512
- if (refreshed.id !== cart.id || refreshed.status !== "active") throw error;
513
- cart = refreshed;
514
- response = await operation(cart);
515
- }
586
+ const response = await operation(await this.resolve(false, seed));
516
587
  return this.write(response.cart);
517
588
  }
518
- save(cart, items, discountCode) {
519
- return this.client.post(`/carts/${cart.id}`, {
520
- items: commerceItems(items),
521
- version: cart.version,
522
- discountCode
523
- }, this.options());
589
+ saveDiscount(cart, code) {
590
+ return this.client.post(`/carts/${cart.id}/discount`, { code }, this.options());
524
591
  }
525
592
  quote(cart, code) {
526
593
  if (!cart.items.length) throw new Error("Cannot check a discount on an empty cart");
@@ -541,7 +608,7 @@ var CartApi = class {
541
608
  404,
542
609
  422
543
610
  ].includes(error.status)) throw error;
544
- cart = (await this.save(cart, cart.items, null)).cart;
611
+ cart = (await this.saveDiscount(cart, null)).cart;
545
612
  }
546
613
  return this.write(cart, this.cache.reference?.token, discount);
547
614
  });
@@ -554,10 +621,7 @@ var CartApi = class {
554
621
  productId: item.productId.trim(),
555
622
  quantity: quantity(item.quantity)
556
623
  };
557
- return this.mutate((cart) => this.client.post(`/carts/${cart.id}/items`, {
558
- item: normalized,
559
- version: cart.version
560
- }, this.options()));
624
+ return this.mutate((cart) => this.client.post(`/carts/${cart.id}/items`, { item: normalized }, this.options()));
561
625
  });
562
626
  }
563
627
  updateQuantity(itemId, nextQuantity) {
@@ -565,11 +629,10 @@ var CartApi = class {
565
629
  const normalized = quantity(nextQuantity, true);
566
630
  return this.mutate((cart) => {
567
631
  if (!cart.items.some((item) => item.id === itemId)) throw new Error("Cart item not found");
568
- const items = cart.items.map((item) => item.id === itemId ? {
569
- ...item,
632
+ return this.client.post(`/carts/${cart.id}/items/quantity`, {
633
+ itemId,
570
634
  quantity: normalized
571
- } : item).filter((item) => item.quantity > 0);
572
- return this.save(cart, items, null);
635
+ }, this.options());
573
636
  });
574
637
  });
575
638
  }
@@ -577,7 +640,7 @@ var CartApi = class {
577
640
  return this.updateQuantity(itemId, 0);
578
641
  }
579
642
  clear() {
580
- return this.enqueue(() => this.mutate((cart) => this.save(cart, [], null), false));
643
+ return this.enqueue(() => this.mutate((cart) => this.client.post(`/carts/${cart.id}/clear`, {}, this.options()), false));
581
644
  }
582
645
  checkDiscount(code) {
583
646
  return this.enqueue(async () => this.quote(await this.resolve(), code));
@@ -587,7 +650,9 @@ var CartApi = class {
587
650
  let quote;
588
651
  const snapshot = await this.mutate(async (cart) => {
589
652
  quote = await this.quote(cart, code);
590
- return this.save(cart, cart.items, quote.discount.code);
653
+ const response = await this.saveDiscount(cart, quote.discount.code);
654
+ quote = await this.quote(response.cart, quote.discount.code);
655
+ return response;
591
656
  });
592
657
  snapshot.discount = discountFromQuote(quote);
593
658
  this.cache.snapshot = cloneSnapshot(snapshot);
@@ -596,22 +661,193 @@ var CartApi = class {
596
661
  });
597
662
  }
598
663
  removeDiscount() {
599
- return this.enqueue(() => this.mutate((cart) => this.save(cart, cart.items, null)));
664
+ return this.enqueue(() => this.mutate((cart) => this.saveDiscount(cart, null)));
600
665
  }
601
666
  checkout(input = {}) {
602
667
  return this.enqueue(async () => {
603
668
  const cart = await this.resolve(true);
604
669
  if (!cart.items.length) throw new Error("Cannot checkout an empty cart");
605
- const response = await this.client.post(`/carts/${cart.id}/checkout`, {
606
- ...input,
607
- version: cart.version
608
- }, this.options());
670
+ const response = await this.client.post(`/carts/${cart.id}/checkout`, input, this.options());
609
671
  this.reset();
610
672
  return response;
611
673
  });
612
674
  }
613
675
  };
614
676
  //#endregion
677
+ //#region src/batch.ts
678
+ var CaldikBatchError = class extends Error {
679
+ results;
680
+ errors;
681
+ constructor(results, errors) {
682
+ super(`Batch requests failed: ${Object.keys(errors).join(", ")}`);
683
+ this.results = results;
684
+ this.errors = errors;
685
+ this.name = "CaldikBatchError";
686
+ }
687
+ };
688
+ function batchRequests(client, input) {
689
+ const entries = Object.entries(input);
690
+ const seen = /* @__PURE__ */ new Set();
691
+ try {
692
+ if (!entries.length || entries.length > 25) throw new Error("A batch must contain between 1 and 25 requests");
693
+ for (const [key, request] of entries) {
694
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(key) || ["constructor", "prototype"].includes(key)) throw new Error("Invalid batch key");
695
+ if (!(request instanceof CaldikRequest) || request.owner !== client || request.started || seen.has(request)) throw new Error("Batch values must be unstarted, distinct requests from the same Caldik client");
696
+ if (request.descriptor.method !== "GET" && !/^\/plugins\/sdk\/[^/]+\/[^/]+$/.test(request.descriptor.path)) throw new Error("Batches support read requests only");
697
+ seen.add(request);
698
+ }
699
+ } catch (error) {
700
+ for (const [, request] of entries) if (request instanceof CaldikRequest) request.cancel(error);
701
+ throw error;
702
+ }
703
+ const response = client.post("/batch", { requests: Object.fromEntries(entries.map(([key, request]) => [key, {
704
+ method: request.descriptor.method,
705
+ path: request.descriptor.path,
706
+ query: request.descriptor.params,
707
+ input: request.descriptor.input
708
+ }])) }, { access: "public" }).then((value) => value.results);
709
+ for (const [key, request] of entries) request.bind(response.then((results) => {
710
+ const result = results[key];
711
+ if (!result) throw new Error(`Missing batch result: ${key}`);
712
+ if (result.status < 200 || result.status >= 300) {
713
+ const body = result.body;
714
+ throw new CaldikHttpError(body?.message || `HTTP ${result.status}`, result.status);
715
+ }
716
+ return request.descriptor.empty ? void 0 : result.body;
717
+ }));
718
+ return response.then((results) => {
719
+ const values = {};
720
+ const errors = {};
721
+ for (const [key, request] of entries) {
722
+ const result = results[key];
723
+ if (!result) throw new Error(`Missing batch result: ${key}`);
724
+ if (result.status >= 200 && result.status < 300) values[key] = request.descriptor.empty ? void 0 : result.body;
725
+ else errors[key] = result;
726
+ }
727
+ if (Object.keys(errors).length) throw new CaldikBatchError(values, errors);
728
+ return values;
729
+ });
730
+ }
731
+ //#endregion
732
+ //#region src/internal/realtime.ts
733
+ /** Native SSE transport. */
734
+ function subscribePlugin(client, path, args, callbacks) {
735
+ const controller = new AbortController();
736
+ let cursor;
737
+ let timer;
738
+ let retries = 0;
739
+ const report = (error) => {
740
+ try {
741
+ if (typeof callbacks.onError === "function") callbacks.onError(error instanceof Error ? error : /* @__PURE__ */ new Error("Realtime connection failed"));
742
+ } catch {}
743
+ };
744
+ const frame = (value) => {
745
+ let event = "message", data = "", id;
746
+ for (const line of value.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
747
+ else if (line.startsWith("data:")) data += (data ? "\n" : "") + line.slice(5).trimStart();
748
+ else if (line.startsWith("id:")) id = line.slice(3).trim();
749
+ if (event === "error") throw new CaldikHttpError("Subscription authorization changed", 403);
750
+ if (id && /^\d{1,20}$/.test(id)) cursor = id;
751
+ if (["ready", "heartbeat"].includes(event) || !data) return;
752
+ const callback = Object.hasOwn(callbacks, event) ? callbacks[event] : void 0;
753
+ if (typeof callback === "function") {
754
+ let parsed;
755
+ try {
756
+ parsed = JSON.parse(data);
757
+ } catch {
758
+ throw new Error("Invalid realtime event");
759
+ }
760
+ try {
761
+ callback(parsed);
762
+ } catch (error) {
763
+ report(error);
764
+ }
765
+ }
766
+ };
767
+ const connect = async () => {
768
+ let reader;
769
+ try {
770
+ const response = await client.stream(path, args, controller.signal, cursor);
771
+ if (!response.headers.get("Content-Type")?.startsWith("text/event-stream") || !response.body) throw new Error("Invalid realtime response");
772
+ reader = response.body.getReader();
773
+ retries = 0;
774
+ const decoder = new TextDecoder();
775
+ let buffer = "";
776
+ while (!controller.signal.aborted) {
777
+ const { value, done } = await reader.read();
778
+ if (done) break;
779
+ buffer += decoder.decode(value, { stream: true });
780
+ buffer = buffer.replaceAll("\r\n", "\n");
781
+ let end;
782
+ while ((end = buffer.indexOf("\n\n")) !== -1) {
783
+ const next = buffer.slice(0, end);
784
+ buffer = buffer.slice(end + 2);
785
+ if (next.length > 65536) throw new Error("Realtime event exceeds its size limit");
786
+ frame(next);
787
+ }
788
+ if (buffer.length > 65536) throw new Error("Realtime event exceeds its size limit");
789
+ }
790
+ } catch (error) {
791
+ if (controller.signal.aborted) return;
792
+ report(error);
793
+ if (error instanceof CaldikHttpError && [
794
+ 400,
795
+ 401,
796
+ 403,
797
+ 404,
798
+ 405
799
+ ].includes(error.status)) {
800
+ controller.abort();
801
+ return;
802
+ }
803
+ } finally {
804
+ await reader?.cancel().catch(() => void 0);
805
+ }
806
+ if (!controller.signal.aborted) timer = setTimeout(() => {
807
+ connect();
808
+ }, Math.min(1e4, 500 * 2 ** Math.min(retries++, 5)));
809
+ };
810
+ connect();
811
+ return () => {
812
+ controller.abort();
813
+ if (timer) clearTimeout(timer);
814
+ };
815
+ }
816
+ //#endregion
817
+ //#region src/plugins.ts
818
+ function createPluginApi(client) {
819
+ const namespaces = /* @__PURE__ */ new Map();
820
+ const safe = (value) => /^[a-zA-Z][a-zA-Z0-9_\-]{0,63}$/.test(value) && ![
821
+ "constructor",
822
+ "prototype",
823
+ "then"
824
+ ].includes(value);
825
+ return new Proxy(Object.create(null), { get(_target, namespace) {
826
+ if (typeof namespace !== "string" || !safe(namespace)) return void 0;
827
+ const methodProxy = (keys) => new Proxy(() => void 0, {
828
+ get(_target, key) {
829
+ if (typeof key !== "string" || !safe(key) || keys.length >= 4) return void 0;
830
+ return methodProxy([...keys, key]);
831
+ },
832
+ apply(_target, _this, args) {
833
+ while (args.length && args.at(-1) === void 0) args.pop();
834
+ const path = `/plugins/sdk/${encodeURIComponent(namespace)}/${encodeURIComponent(keys.join("."))}`;
835
+ if (keys.at(-1) === "subscribe") {
836
+ const callbacks = args.at(-1);
837
+ if (!callbacks || typeof callbacks !== "object" || Array.isArray(callbacks)) throw new Error("Provide subscription callbacks");
838
+ return subscribePlugin(client, path, args.slice(0, -1), callbacks);
839
+ }
840
+ return client.post(path, keys.length > 1 ? args : args[0] ?? {}, { access: "public" });
841
+ }
842
+ });
843
+ if (!namespaces.has(namespace)) namespaces.set(namespace, new Proxy(Object.create(null), { get(_target, method) {
844
+ if (typeof method !== "string" || !safe(method)) return void 0;
845
+ return methodProxy([method]);
846
+ } }));
847
+ return namespaces.get(namespace);
848
+ } });
849
+ }
850
+ //#endregion
615
851
  //#region src/payment-providers.generated.ts
616
852
  const paymentProviderIds = [
617
853
  "customer_balance",
@@ -662,9 +898,13 @@ var CaldikClient = class {
662
898
  store;
663
899
  checkout;
664
900
  customer;
901
+ customers;
902
+ orders;
903
+ plugins;
665
904
  subscriptions;
666
905
  discounts;
667
906
  cart;
907
+ client;
668
908
  constructor(options = {}) {
669
909
  const client = new CaldikHttpClient({
670
910
  apiKey: options.apiKey,
@@ -673,16 +913,23 @@ var CaldikClient = class {
673
913
  request: options.request,
674
914
  onSetCookie: options.onSetCookie
675
915
  });
916
+ this.client = client;
676
917
  this.categories = new CategoriesApi(client);
677
918
  this.products = new ProductsApi(client);
678
919
  this.reviews = new ReviewsApi(client);
679
920
  this.store = new StoreApi(client);
680
921
  this.checkout = new CheckoutApi(client);
681
922
  this.customer = new CustomerApi(client);
923
+ this.customers = this.customer;
924
+ this.orders = this.customer.orders;
925
+ this.plugins = createPluginApi(client);
682
926
  this.subscriptions = new SubscriptionsApi(client);
683
927
  this.discounts = new DiscountsApi(client);
684
928
  this.cart = new CartApi(client);
685
929
  }
930
+ batch(input) {
931
+ return batchRequests(this.client, input);
932
+ }
686
933
  };
687
934
  //#endregion
688
- export { CaldikClient, CartApi, DiscountsApi, checkoutPaymentProviderIds, paymentProviderIds, storeWebhookEventNames, subscriptionPaymentProviderIds };
935
+ export { CaldikBatchError, CaldikClient, CaldikHttpError, CaldikRequest, CartApi, DiscountsApi, batchRequests, checkoutPaymentProviderIds, paymentProviderIds, storeWebhookEventNames, subscriptionPaymentProviderIds };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caldik/client",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -8,16 +8,22 @@
8
8
  ".": {
9
9
  "types": "./dist/index.d.mts",
10
10
  "import": "./dist/index.mjs"
11
+ },
12
+ "./development": {
13
+ "types": "./dist/development.d.mts",
14
+ "default": "./dist/development.mjs"
11
15
  }
12
16
  },
13
17
  "files": [
14
- "dist"
18
+ "dist",
19
+ "types"
15
20
  ],
16
21
  "publishConfig": {
17
22
  "access": "public"
18
23
  },
19
24
  "scripts": {
20
- "build": "tsdown",
25
+ "build": "tsdown && node development/prepare-types.mjs",
26
+ "prepack": "node development/prepare-types.mjs",
21
27
  "sync:api-contracts": "bun scripts/sync-api-contracts.ts",
22
28
  "test": "bun test"
23
29
  },
@@ -0,0 +1,7 @@
1
+ // Generated by Caldik.
2
+ import type { CaldikRequest } from "@caldik/client";
3
+
4
+ declare module "@caldik/client" {
5
+ interface CaldikPlugins {
6
+ }
7
+ }