@caldik/client 0.1.7 → 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,7 +165,7 @@ 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 `session` cookie and server persistence automatically. Cart
168
+ The SDK manages the `cart_{storeId}` cookie and server persistence automatically. Cart
169
169
  requests, responses, and saved browser state do not require version numbers.
170
170
  Existing saved carts are restored automatically after upgrading.
171
171
 
@@ -389,8 +389,14 @@ await caldik.customer.session()
389
389
  ```ts
390
390
  const { customer } = await caldik.customers.me()
391
391
  console.log(customer.name, customer.email)
392
+ console.log(customer.totalOrders, customer.totalSpent, customer.currency)
392
393
  ```
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
+
394
400
  **List the customer's orders**
395
401
 
396
402
  ```ts
@@ -533,6 +539,7 @@ await caldik.customer.register.verify(code)
533
539
 
534
540
  > **Note:** The `username` field is provided by the Customer Usernames plugin. Omit this field when the plugin is not installed.
535
541
 
542
+
536
543
  ## Payment Methods and Settings
537
544
 
538
545
  Retrieve customer authentication settings, checkout configuration, and available payment methods.
@@ -17,6 +17,7 @@ const safeKey = (value) => typeof value === "string" && /^[a-zA-Z][a-zA-Z0-9_\-]
17
17
  "prototype",
18
18
  "then"
19
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(" "));
20
21
  function schemaType(schema, depth = 0) {
21
22
  if (depth > 8 || !schema || typeof schema !== "object" || Array.isArray(schema)) throw new Error("Invalid plugin type schema");
22
23
  if (schema.enum) {
@@ -46,8 +47,8 @@ function generatePluginTypes(metadata) {
46
47
  if (!metadata || !Array.isArray(metadata.plugins) || metadata.plugins.length > 100) throw new Error("Invalid installed plugin metadata");
47
48
  const names = /* @__PURE__ */ new Set();
48
49
  const lines = [
49
- "// Generated by Caldik. Plugin contracts refresh automatically during caldik dev.",
50
- "import type { CaldikRequest } from \"@caldik/client\";",
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";`,
51
52
  "",
52
53
  "declare module \"@caldik/client\" {",
53
54
  " interface CaldikPlugins {"
@@ -56,14 +57,64 @@ function generatePluginTypes(metadata) {
56
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");
57
58
  names.add(plugin.namespace);
58
59
  lines.push(` readonly ${JSON.stringify(plugin.namespace)}: {`);
60
+ const tree = Object.create(null);
59
61
  for (const [name, method] of Object.entries(plugin.methods).sort(([a], [b]) => a.localeCompare(b))) {
60
- if (!safeKey(name) || !method || typeof method !== "object") throw new Error("Invalid plugin method");
61
- const input = schemaType(method.input);
62
- const output = schemaType(method.output);
63
- const empty = method.input.type === "object" && !Object.keys(method.input.properties ?? {}).length;
64
- const optional = method.input.type === "object" && !method.input.required?.length;
65
- lines.push(` readonly ${JSON.stringify(name)}: (${empty ? "" : `input${optional ? "?" : ""}: ${input}`}) => CaldikRequest<${output}>;`);
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 };
66
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, " ");
67
118
  lines.push(" };");
68
119
  }
69
120
  lines.push(" }", "}", "");
package/dist/index.d.mts CHANGED
@@ -204,7 +204,7 @@ interface StorefrontOrder {
204
204
  code: string | null;
205
205
  percentage: string;
206
206
  amount: string;
207
- source: "code" | "loyalty";
207
+ source: "code" | "plugin";
208
208
  } | null;
209
209
  currency: string;
210
210
  }
@@ -324,11 +324,12 @@ interface CustomerProfileResponse {
324
324
  success: boolean;
325
325
  customer: {
326
326
  id: string;
327
- shortId: string;
328
- displayId: string;
329
327
  name: string | null;
330
328
  email: string | null;
331
- phone: string | null;
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;
332
333
  createdAt: number;
333
334
  updatedAt: number;
334
335
  extensions: Record<string, Record<string, string | number | boolean>>;
@@ -360,7 +361,7 @@ interface CustomerOrder {
360
361
  code: string | null;
361
362
  percentage: string;
362
363
  amount: string;
363
- source: "code" | "loyalty";
364
+ source: "code" | "plugin";
364
365
  } | null;
365
366
  }
366
367
  interface CustomerOrderItem {
@@ -484,7 +485,9 @@ declare class CaldikHttpClient {
484
485
  customerAuthenticated(): Promise<void>;
485
486
  private captureCookies;
486
487
  private baseUrl;
488
+ private response;
487
489
  private request;
490
+ stream(path: string, input: unknown, signal: AbortSignal, cursor?: string): Promise<Response>;
488
491
  get<T>(path: string, params?: Record<string, string | number>, options?: RequestOptions): CaldikRequest<T>;
489
492
  post<T>(path: string, input?: unknown, options?: RequestOptions): CaldikRequest<T>;
490
493
  }
@@ -668,6 +671,11 @@ declare class CartApi {
668
671
  checkout(input?: CartCheckoutInput): Promise<OrderResponse>;
669
672
  }
670
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
671
679
  //#region src/index.d.ts
672
680
  /** Populated automatically from the store's plugin contracts during caldik dev. */
673
681
  interface CaldikPlugins {}
@@ -703,4 +711,4 @@ declare class CaldikClient {
703
711
  batch<T extends BatchInput>(input: T): Promise<BatchResult<T>>;
704
712
  }
705
713
  //#endregion
706
- export { AppliedDiscount, BatchInput, BatchResult, CaldikBatchError, CaldikClient, CaldikClientOptions, CaldikHttpError, CaldikPlugins, CaldikRequest, 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 };
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
@@ -124,7 +124,7 @@ var CaldikHttpClient = class {
124
124
  return this.storeId || boundRuntimeStoreId();
125
125
  }
126
126
  hasCartSession() {
127
- if (this.cookies.has("session")) return true;
127
+ if (this.cookies.has(`cart_${this.storeContextId()}`) || this.cookies.has("session")) return true;
128
128
  const prefix = `caldik_cart_session_${this.storeContextId()}`;
129
129
  return [...this.cookies.keys()].some((name) => name === prefix || name.startsWith(`${prefix}_`));
130
130
  }
@@ -163,7 +163,7 @@ var CaldikHttpClient = class {
163
163
  if (!boundStoreId && !this.apiKey) throw new Error("Caldik authentication is unavailable. External servers must provide a private apiKey.");
164
164
  return `${this.apiUrl.replace(/\/+$/, "")}/v1/stores/${encodeURIComponent(storeId)}`;
165
165
  }
166
- async request(method, path, input, params, options) {
166
+ async response(method, path, input, params, options, signal) {
167
167
  const url = new URL(`${this.baseUrl()}${path}`);
168
168
  if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));
169
169
  const headers = { ...options?.headers };
@@ -179,16 +179,30 @@ var CaldikHttpClient = class {
179
179
  headers,
180
180
  ...input !== void 0 ? { body: JSON.stringify(input) } : {},
181
181
  cache: "default",
182
- credentials: "include"
182
+ credentials: "include",
183
+ signal
183
184
  });
184
185
  if (!isBrowserRuntime() && options?.access === "public") await this.captureCookies(response);
185
186
  if (!response.ok) {
186
187
  const error = await response.clone().json().catch(() => null);
187
188
  throw new CaldikHttpError(typeof error?.message === "string" && error.message ? error.message : `${response.status} ${response.statusText}`.trim(), response.status);
188
189
  }
190
+ return response;
191
+ }
192
+ async request(method, path, input, params, options) {
193
+ const response = await this.response(method, path, input, params, options);
189
194
  if (options?.empty) return void 0;
190
195
  return response.json();
191
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
+ }
192
206
  get(path, params, options) {
193
207
  return new CaldikRequest(this, {
194
208
  method: "GET",
@@ -715,6 +729,91 @@ function batchRequests(client, input) {
715
729
  });
716
730
  }
717
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
718
817
  //#region src/plugins.ts
719
818
  function createPluginApi(client) {
720
819
  const namespaces = /* @__PURE__ */ new Map();
@@ -725,9 +824,25 @@ function createPluginApi(client) {
725
824
  ].includes(value);
726
825
  return new Proxy(Object.create(null), { get(_target, namespace) {
727
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
+ });
728
843
  if (!namespaces.has(namespace)) namespaces.set(namespace, new Proxy(Object.create(null), { get(_target, method) {
729
844
  if (typeof method !== "string" || !safe(method)) return void 0;
730
- return (input = {}) => client.post(`/plugins/sdk/${encodeURIComponent(namespace)}/${encodeURIComponent(method)}`, input, { access: "public" });
845
+ return methodProxy([method]);
731
846
  } }));
732
847
  return namespaces.get(namespace);
733
848
  } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caldik/client",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -1,4 +1,4 @@
1
- // Generated by Caldik. Plugin contracts refresh automatically during caldik dev.
1
+ // Generated by Caldik.
2
2
  import type { CaldikRequest } from "@caldik/client";
3
3
 
4
4
  declare module "@caldik/client" {