@wix/himalaya-cli 0.941.0 → 0.943.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.
Files changed (26) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/index.d.ts +10 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/wix-data/src/index.d.ts +50 -3
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-data.mjs +16 -2
  4. package/dist/cli.mjs +6 -1
  5. package/guides/sdk-reference.json +45 -21
  6. package/package.json +2 -2
  7. package/templates/beauty-salon/tier5-src/wix-spaces-collections/index.ts +60 -12
  8. package/templates/branded-lite/dev/index.ts +224 -14
  9. package/templates/branded-lite/tier5-src/himi-branded-lite-badges/index.ts +11 -8
  10. package/templates/branded-lite/tier5-src/himi-branded-lite-coupons/index.ts +17 -6
  11. package/templates/branded-lite/tier5-src/himi-branded-lite-donations/index.ts +11 -8
  12. package/templates/branded-lite/tier5-src/himi-branded-lite-forms/index.ts +11 -10
  13. package/templates/branded-lite/tier5-src/himi-branded-lite-galleries/index.ts +12 -9
  14. package/templates/branded-lite/tier5-src/himi-branded-lite-giftcards/index.ts +11 -8
  15. package/templates/branded-lite/tier5-src/himi-branded-lite-inbox/index.ts +11 -8
  16. package/templates/branded-lite/tier5-src/himi-branded-lite-invoices/index.ts +11 -8
  17. package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +11 -8
  18. package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +13 -8
  19. package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +11 -8
  20. package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +11 -8
  21. package/templates/branded-lite/tier5-src/himi-branded-lite-reviews/index.ts +11 -8
  22. package/templates/branded-lite/tier5-src/himi-branded-lite-videos/index.ts +12 -9
  23. package/templates/branded-lite/tier5-src/wix-spaces-collections/index.ts +60 -12
  24. package/templates/gym-fitness/tier5-src/wix-spaces-collections/index.ts +60 -12
  25. package/templates/portfolio-creative/tier5-src/wix-spaces-collections/index.ts +60 -12
  26. package/templates/retail-store/tier5-src/wix-spaces-collections/index.ts +60 -12
@@ -185,7 +185,17 @@ export interface WixUpdateBookingResponse {
185
185
  export interface WixRescheduleBookingResponse {
186
186
  booking: WixBooking;
187
187
  }
188
+ /**
189
+ * @scope owner — requires an owner / business identity — a visitor session gets 403.
190
+ *
191
+ * POST /bookings/v2/bookings/query
192
+ */
188
193
  export declare function queryBookings(req?: WixQueryBookingsRequest, opts?: WixCallOptions): Promise<WixQueryBookingsResponse>;
194
+ /**
195
+ * @scope owner — requires an owner / business identity — a visitor session gets 403.
196
+ *
197
+ * GET /bookings/v1/bookings
198
+ */
189
199
  export declare function listBookings(req?: WixListBookingsRequest, opts?: WixCallOptions): Promise<WixListBookingsResponse>;
190
200
  export declare function getBooking(bookingId: string, opts?: WixCallOptions): Promise<WixGetBookingResponse>;
191
201
  export declare function createBooking(req?: WixCreateBookingRequest, opts?: WixCallOptions): Promise<WixCreateBookingResponse>;
@@ -50,9 +50,56 @@ export interface WixUpdateDataItemRequest {
50
50
  };
51
51
  }
52
52
  /** Query items in a collection — POST /wix-data/v2/items/query.
53
- * Pass `returnTotalCount: true` to ask Wix to fill `pagingMetadata.total`
54
- * (offset paging only; omitted by default because counting is extra work). */
55
- export declare function queryDataItems(dataCollectionId: string, limit?: number, filter?: Record<string, unknown>, returnTotalCount?: boolean): Promise<WixQueryDataItemsResponse>;
53
+ *
54
+ * `filter` is the Wix Data v2 filter object, which the type cannot express (it is
55
+ * `Record<string, unknown>` by construction). A field name maps to a value for
56
+ * equality, or to an operator object:
57
+ *
58
+ * ```ts
59
+ * await queryDataItems("Listings", 20, { city: "Tel Aviv" });
60
+ * await queryDataItems("Listings", 20, { price: { $lte: 2_500_000 } });
61
+ * await queryDataItems("Listings", 20, { _id: { $in: [a, b] } });
62
+ * ```
63
+ *
64
+ * `offset` pages: ask for `limit` rows starting at `offset`. Pair it with
65
+ * `returnTotalCount: true`, which fills `pagingMetadata.total` so a caller knows
66
+ * when to stop — it is omitted by default because counting is extra work, and it
67
+ * reports total for OFFSET paging only, which is the mode this function uses.
68
+ *
69
+ * ```ts
70
+ * const first = await queryDataItems("Listings", 50, undefined, true);
71
+ * const total = first.pagingMetadata?.total ?? 0;
72
+ * const rest = await queryDataItems("Listings", 50, undefined, false, 50);
73
+ * ```
74
+ *
75
+ * To read ONE row, use `getDataItem` rather than a page plus a client-side
76
+ * `.find()` — that pattern breaks silently once the collection outgrows `limit`. */
77
+ export declare function queryDataItems(dataCollectionId: string, limit?: number, filter?: Record<string, unknown>, returnTotalCount?: boolean, offset?: number): Promise<WixQueryDataItemsResponse>;
78
+ /** Read ONE item by id — GET /wix-data/v2/items/{dataItemId}?dataCollectionId=…
79
+ *
80
+ * Reach for this on a detail screen. Before it existed the only read this package
81
+ * offered was `queryDataItems`, so a detail worker had to fetch a bounded page and
82
+ * find its row client-side:
83
+ *
84
+ * ```ts
85
+ * const res = await queryDataItems("Listings", 50); // ← DON'T
86
+ * const row = (res.dataItems ?? []).find((it) => it.id === id);
87
+ * ```
88
+ *
89
+ * That is correct against a handful of seeded rows and silently wrong once the
90
+ * collection outgrows `limit`. Ask for the row instead:
91
+ *
92
+ * ```ts
93
+ * const { dataItem } = await getDataItem("Listings", id); // ← DO
94
+ * ```
95
+ *
96
+ * The collection id rides the query string rather than a body, because the Wix
97
+ * gateway drops a GET body — and the string is built with `encodeURIComponent`
98
+ * rather than `URLSearchParams`, which the bare Tier-5 worker runtimes (JSCore /
99
+ * Hermes / QuickJS-ng) do not provide. */
100
+ export declare function getDataItem(dataCollectionId: string, dataItemId: string): Promise<{
101
+ dataItem?: WixDataItem;
102
+ }>;
56
103
  /** Insert an item — POST /wix-data/v2/items. */
57
104
  export declare function insertDataItem(dataCollectionId: string, data: Record<string, unknown>): Promise<{
58
105
  dataItem?: WixDataItem;
@@ -7,15 +7,28 @@ function wixCall(method, path, body) {
7
7
  }
8
8
  return globalThis.himalaya.wixApi.call(method, path, body);
9
9
  }
10
- function queryDataItems(dataCollectionId, limit = 50, filter, returnTotalCount = false) {
10
+ function queryDataItems(dataCollectionId, limit = 50, filter, returnTotalCount = false, offset = 0) {
11
11
  if (!dataCollectionId) throw new Error("queryDataItems: dataCollectionId is required");
12
12
  const body = {
13
13
  dataCollectionId,
14
- query: { paging: { limit }, ...filter ? { filter } : {} },
14
+ query: {
15
+ // `offset` is omitted when 0 so a caller that does not page sends the byte-identical
16
+ // body it sent before this parameter existed.
17
+ paging: { limit, ...offset ? { offset } : {} },
18
+ ...filter ? { filter } : {}
19
+ },
15
20
  ...returnTotalCount ? { returnTotalCount: true } : {}
16
21
  };
17
22
  return wixCall("POST", "/wix-data/v2/items/query", body);
18
23
  }
24
+ function getDataItem(dataCollectionId, dataItemId) {
25
+ if (!dataCollectionId) throw new Error("getDataItem: dataCollectionId is required");
26
+ if (!dataItemId) throw new Error("getDataItem: dataItemId is required");
27
+ return wixCall(
28
+ "GET",
29
+ `/wix-data/v2/items/${encodeURIComponent(dataItemId)}?dataCollectionId=${encodeURIComponent(dataCollectionId)}`
30
+ );
31
+ }
19
32
  function insertDataItem(dataCollectionId, data) {
20
33
  if (!dataCollectionId) throw new Error("insertDataItem: dataCollectionId is required");
21
34
  return wixCall("POST", "/wix-data/v2/items", {
@@ -36,6 +49,7 @@ function removeDataItem(dataCollectionId, id) {
36
49
  return wixCall("DELETE", `/wix-data/v2/items/${encodeURIComponent(id)}?${q}`);
37
50
  }
38
51
  export {
52
+ getDataItem,
39
53
  insertDataItem,
40
54
  queryDataItems,
41
55
  removeDataItem,
package/dist/cli.mjs CHANGED
@@ -1378,7 +1378,12 @@ function readTemplateFiles(srcRoot) {
1378
1378
  function scaffoldFromTemplate(name, dir, template) {
1379
1379
  const srcRoot = templateSourceDir(template);
1380
1380
  if (!existsSync2(join3(srcRoot, "himalaya.content.json"))) {
1381
- throw new Error(`unknown template "${template}" (available: ${availableTemplates().join(", ") || "none bundled"})`);
1381
+ throw new Error(
1382
+ `unknown template "${template}" \u2014 not bundled with this CLI.
1383
+ bundled: ${availableTemplates().join(", ") || "none"}
1384
+ The published catalog is larger: \`himi template list --remote\` lists it, and
1385
+ \`himi init <app> --template ${template} --remote\` fetches one from there.`
1386
+ );
1382
1387
  }
1383
1388
  return finishScaffold(materializeSource(readTemplateFiles(srcRoot), name, dir), template);
1384
1389
  }
@@ -6955,16 +6955,18 @@
6955
6955
  {
6956
6956
  "name": "queryBookings",
6957
6957
  "kind": "function",
6958
- "jsdoc": "",
6958
+ "jsdoc": "/**\n * @scope owner — requires an owner / business identity — a visitor session gets 403.\n *\n * POST /bookings/v2/bookings/query\n */",
6959
6959
  "decl": "export function queryBookings(\n req: WixQueryBookingsRequest = {} as WixQueryBookingsRequest,\n opts?: WixCallOptions,\n): Promise<WixQueryBookingsResponse>;",
6960
- "http": "POST /bookings/v2/bookings/query"
6960
+ "http": "POST /bookings/v2/bookings/query",
6961
+ "scope": "owner"
6961
6962
  },
6962
6963
  {
6963
6964
  "name": "listBookings",
6964
6965
  "kind": "function",
6965
- "jsdoc": "",
6966
+ "jsdoc": "/**\n * @scope owner — requires an owner / business identity — a visitor session gets 403.\n *\n * GET /bookings/v1/bookings\n */",
6966
6967
  "decl": "export function listBookings(\n req: WixListBookingsRequest = {} as WixListBookingsRequest,\n opts?: WixCallOptions,\n): Promise<WixListBookingsResponse>;",
6967
- "http": "GET /bookings/v1/bookings"
6968
+ "http": "GET /bookings/v1/bookings",
6969
+ "scope": "owner"
6968
6970
  },
6969
6971
  {
6970
6972
  "name": "getBooking",
@@ -10524,10 +10526,17 @@
10524
10526
  {
10525
10527
  "name": "queryDataItems",
10526
10528
  "kind": "function",
10527
- "jsdoc": "/** Query items in a collection — POST /wix-data/v2/items/query.\n * Pass `returnTotalCount: true` to ask Wix to fill `pagingMetadata.total`\n * (offset paging only; omitted by default because counting is extra work). */",
10528
- "decl": "export function queryDataItems(\n dataCollectionId: string,\n limit = 50,\n filter?: Record<string, unknown>,\n returnTotalCount = false,\n): Promise<WixQueryDataItemsResponse>;",
10529
+ "jsdoc": "/** Query items in a collection — POST /wix-data/v2/items/query.\n *\n * `filter` is the Wix Data v2 filter object, which the type cannot express (it is\n * `Record<string, unknown>` by construction). A field name maps to a value for\n * equality, or to an operator object:\n *\n * ```ts\n * await queryDataItems(\"Listings\", 20, { city: \"Tel Aviv\" });\n * await queryDataItems(\"Listings\", 20, { price: { $lte: 2_500_000 } });\n * await queryDataItems(\"Listings\", 20, { _id: { $in: [a, b] } });\n * ```\n *\n * `offset` pages: ask for `limit` rows starting at `offset`. Pair it with\n * `returnTotalCount: true`, which fills `pagingMetadata.total` so a caller knows\n * when to stop — it is omitted by default because counting is extra work, and it\n * reports total for OFFSET paging only, which is the mode this function uses.\n *\n * ```ts\n * const first = await queryDataItems(\"Listings\", 50, undefined, true);\n * const total = first.pagingMetadata?.total ?? 0;\n * const rest = await queryDataItems(\"Listings\", 50, undefined, false, 50);\n * ```\n *\n * To read ONE row, use `getDataItem` rather than a page plus a client-side\n * `.find()` — that pattern breaks silently once the collection outgrows `limit`. */",
10530
+ "decl": "export function queryDataItems(\n dataCollectionId: string,\n limit = 50,\n filter?: Record<string, unknown>,\n returnTotalCount = false,\n offset = 0,\n): Promise<WixQueryDataItemsResponse>;",
10529
10531
  "http": "POST /wix-data/v2/items/query"
10530
10532
  },
10533
+ {
10534
+ "name": "getDataItem",
10535
+ "kind": "function",
10536
+ "jsdoc": "/** Read ONE item by id — GET /wix-data/v2/items/{dataItemId}?dataCollectionId=…\n *\n * Reach for this on a detail screen. Before it existed the only read this package\n * offered was `queryDataItems`, so a detail worker had to fetch a bounded page and\n * find its row client-side:\n *\n * ```ts\n * const res = await queryDataItems(\"Listings\", 50); // ← DON'T\n * const row = (res.dataItems ?? []).find((it) => it.id === id);\n * ```\n *\n * That is correct against a handful of seeded rows and silently wrong once the\n * collection outgrows `limit`. Ask for the row instead:\n *\n * ```ts\n * const { dataItem } = await getDataItem(\"Listings\", id); // ← DO\n * ```\n *\n * The collection id rides the query string rather than a body, because the Wix\n * gateway drops a GET body — and the string is built with `encodeURIComponent`\n * rather than `URLSearchParams`, which the bare Tier-5 worker runtimes (JSCore /\n * Hermes / QuickJS-ng) do not provide. */",
10537
+ "decl": "export function getDataItem(\n dataCollectionId: string,\n dataItemId: string,\n): Promise<{ dataItem?: WixDataItem }>;",
10538
+ "http": "GET /wix-data/v2/items/{…}?dataCollectionId={…}"
10539
+ },
10531
10540
  {
10532
10541
  "name": "insertDataItem",
10533
10542
  "kind": "function",
@@ -19013,91 +19022,106 @@
19013
19022
  "name": "MuteStatus",
19014
19023
  "kind": "type",
19015
19024
  "jsdoc": "/** \"MUTED\" silences the topic on this channel; \"UNMUTED\" delivers it. */",
19016
- "decl": "export type MuteStatus = \"MUTED\" | \"UNMUTED\";"
19025
+ "decl": "export type MuteStatus = \"MUTED\" | \"UNMUTED\";",
19026
+ "verified": false
19017
19027
  },
19018
19028
  {
19019
19029
  "name": "Channel",
19020
19030
  "kind": "type",
19021
19031
  "jsdoc": "/** Delivery channel. The mobile push channel value is unconfirmed; \"MOBILE\" is\n * the working default the bundle passes. */",
19022
- "decl": "export type Channel = \"MOBILE\" | string;"
19032
+ "decl": "export type Channel = \"MOBILE\" | string;",
19033
+ "verified": false
19023
19034
  },
19024
19035
  {
19025
19036
  "name": "PreferenceState",
19026
19037
  "kind": "interface",
19027
19038
  "jsdoc": "",
19028
- "decl": "export interface PreferenceState {\n /** \"MUTED\" | \"UNMUTED\" — whether the topic is delivered on this channel. */\n muteStatus?: MuteStatus;\n}"
19039
+ "decl": "export interface PreferenceState {\n /** \"MUTED\" | \"UNMUTED\" — whether the topic is delivered on this channel. */\n muteStatus?: MuteStatus;\n}",
19040
+ "verified": false
19029
19041
  },
19030
19042
  {
19031
19043
  "name": "PreferenceStateData",
19032
19044
  "kind": "interface",
19033
19045
  "jsdoc": "",
19034
- "decl": "export interface PreferenceStateData {\n state?: PreferenceState;\n}"
19046
+ "decl": "export interface PreferenceStateData {\n state?: PreferenceState;\n}",
19047
+ "verified": false
19035
19048
  },
19036
19049
  {
19037
19050
  "name": "PreferenceInfo",
19038
19051
  "kind": "interface",
19039
19052
  "jsdoc": "/** One topic×channel preference for the implicit recipient. */",
19040
- "decl": "export interface PreferenceInfo {\n /** The notification topic id (matches NotificationTopic.id). */\n topicId: string;\n /** Delivery channel this preference applies to (e.g. \"MOBILE\"). */\n channel?: Channel;\n stateData?: PreferenceStateData;\n}"
19053
+ "decl": "export interface PreferenceInfo {\n /** The notification topic id (matches NotificationTopic.id). */\n topicId: string;\n /** Delivery channel this preference applies to (e.g. \"MOBILE\"). */\n channel?: Channel;\n stateData?: PreferenceStateData;\n}",
19054
+ "verified": false
19041
19055
  },
19042
19056
  {
19043
19057
  "name": "NotificationPreferences",
19044
19058
  "kind": "interface",
19045
19059
  "jsdoc": "",
19046
- "decl": "export interface NotificationPreferences {\n preferencesInfo?: PreferenceInfo[];\n}"
19060
+ "decl": "export interface NotificationPreferences {\n preferencesInfo?: PreferenceInfo[];\n}",
19061
+ "verified": false
19047
19062
  },
19048
19063
  {
19049
19064
  "name": "GetMyNotificationPreferencesResponse",
19050
19065
  "kind": "interface",
19051
19066
  "jsdoc": "",
19052
- "decl": "export interface GetMyNotificationPreferencesResponse {\n preferences?: NotificationPreferences;\n}"
19067
+ "decl": "export interface GetMyNotificationPreferencesResponse {\n preferences?: NotificationPreferences;\n}",
19068
+ "verified": false
19053
19069
  },
19054
19070
  {
19055
19071
  "name": "NotificationTopic",
19056
19072
  "kind": "interface",
19057
19073
  "jsdoc": "/** A notification topic — its id is the PreferenceInfo.topicId; displayName is\n * the row label shown in the UI. */",
19058
- "decl": "export interface NotificationTopic {\n id: string;\n displayName?: string;\n}"
19074
+ "decl": "export interface NotificationTopic {\n id: string;\n displayName?: string;\n}",
19075
+ "verified": false
19059
19076
  },
19060
19077
  {
19061
19078
  "name": "GetNotificationTopicsResponse",
19062
19079
  "kind": "interface",
19063
19080
  "jsdoc": "",
19064
- "decl": "export interface GetNotificationTopicsResponse {\n topics?: NotificationTopic[];\n}"
19081
+ "decl": "export interface GetNotificationTopicsResponse {\n topics?: NotificationTopic[];\n}",
19082
+ "verified": false
19065
19083
  },
19066
19084
  {
19067
19085
  "name": "UpsertMyNotificationPreferencesRequest",
19068
19086
  "kind": "interface",
19069
19087
  "jsdoc": "",
19070
- "decl": "export interface UpsertMyNotificationPreferencesRequest {\n preferencesInfo: PreferenceInfo[];\n}"
19088
+ "decl": "export interface UpsertMyNotificationPreferencesRequest {\n preferencesInfo: PreferenceInfo[];\n}",
19089
+ "verified": false
19071
19090
  },
19072
19091
  {
19073
19092
  "name": "UpsertMyNotificationPreferencesResponse",
19074
19093
  "kind": "interface",
19075
19094
  "jsdoc": "",
19076
- "decl": "export interface UpsertMyNotificationPreferencesResponse {\n preferences?: NotificationPreferences;\n}"
19095
+ "decl": "export interface UpsertMyNotificationPreferencesResponse {\n preferences?: NotificationPreferences;\n}",
19096
+ "verified": false
19077
19097
  },
19078
19098
  {
19079
19099
  "name": "muteStatusFor",
19080
19100
  "kind": "function",
19081
19101
  "jsdoc": "/** Map a UI \"on\" boolean to a MuteStatus: on ⇒ delivered (\"UNMUTED\"). */",
19082
- "decl": "export function muteStatusFor(on: boolean): MuteStatus;"
19102
+ "decl": "export function muteStatusFor(on: boolean): MuteStatus;",
19103
+ "verified": false
19083
19104
  },
19084
19105
  {
19085
19106
  "name": "getMyNotificationPreferences",
19086
19107
  "kind": "function",
19087
19108
  "jsdoc": "/** Get the signed-in member's notification preferences —\n * GET /notifications/v1/preferences-by-implicit-recipient. */",
19088
- "decl": "export function getMyNotificationPreferences(): Promise<GetMyNotificationPreferencesResponse>;"
19109
+ "decl": "export function getMyNotificationPreferences(): Promise<GetMyNotificationPreferencesResponse>;",
19110
+ "verified": false
19089
19111
  },
19090
19112
  {
19091
19113
  "name": "getNotificationTopics",
19092
19114
  "kind": "function",
19093
19115
  "jsdoc": "/** List the available notification topics (id + displayName for labels) —\n * GET /notifications/v1/preferences/topics. */",
19094
- "decl": "export function getNotificationTopics(): Promise<GetNotificationTopicsResponse>;"
19116
+ "decl": "export function getNotificationTopics(): Promise<GetNotificationTopicsResponse>;",
19117
+ "verified": false
19095
19118
  },
19096
19119
  {
19097
19120
  "name": "upsertMyNotificationPreferences",
19098
19121
  "kind": "function",
19099
19122
  "jsdoc": "/** Upsert one or more of the signed-in member's preferences —\n * POST /notifications/v1/preferences-by-implicit-recipient, body\n * { preferencesInfo }. Pass only the rows that changed. */",
19100
- "decl": "export function upsertMyNotificationPreferences(\n preferencesInfo: PreferenceInfo[]\n): Promise<UpsertMyNotificationPreferencesResponse>;"
19123
+ "decl": "export function upsertMyNotificationPreferences(\n preferencesInfo: PreferenceInfo[]\n): Promise<UpsertMyNotificationPreferencesResponse>;",
19124
+ "verified": false
19101
19125
  }
19102
19126
  ]
19103
19127
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.941.0",
3
+ "version": "0.943.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -64,5 +64,5 @@
64
64
  "artifactId": "himalaya-cli"
65
65
  }
66
66
  },
67
- "falconPackageHash": "f6ef221b31d3e6cb04fc7f80011d74de1d766b5da1d85c61f7a68530"
67
+ "falconPackageHash": "20d4d02bfd992d1556a2c7e77e8eae63a7b4ec23e7c9e082ff43e951"
68
68
  }
@@ -11,7 +11,12 @@ import {
11
11
  } from "@himalaya/wix-stores-collections";
12
12
 
13
13
  interface Row { id: string; name: string; subtitle: string }
14
- interface S { status: "loading" | "ready"; collections: Row[]; emptyText: string }
14
+ interface S {
15
+ status: "loading" | "ready" | "error";
16
+ collections: Row[];
17
+ emptyText: string;
18
+ error: string;
19
+ }
15
20
 
16
21
  function fromCategory(c: StoreCategory): Row {
17
22
  return { id: c.id || "", name: c.name || "Category", subtitle: categoryCount(c) };
@@ -23,26 +28,69 @@ function fromCollection(c: WixCollection): Row {
23
28
  return { id: cc.id || cc.name || "collection", name: cc.name || "Collection", subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "" };
24
29
  }
25
30
 
26
- async function loadCollections(): Promise<Row[]> {
31
+ type Loaded = { ok: true; rows: Row[] } | { ok: false; error: string };
32
+
33
+ async function loadCollections(): Promise<Loaded> {
27
34
  try {
28
35
  const r = await queryStoreCategories(50);
29
- const rows = (r.categories ?? []).map(fromCategory).filter((row) => row.id);
30
- if (rows.length) return rows;
31
- } catch { /* fall through to V1 */ }
36
+ // A V3 SUCCESS with zero rows is an empty catalogue, not a reason to try V1, and this is the
37
+ // line that matters. Written as `if (rows.length) return rows`, a modern (stores-v3) site
38
+ // that simply has no categories fell through to the V1 call — which `himi sdk
39
+ // wix-stores-collections` documents as answering HTTP 428 on exactly those sites — and the
40
+ // catch below turned that 428 into an empty list. The screen then told the visitor "No
41
+ // categories yet" over a dead endpoint.
42
+ //
43
+ // Only a V3 THROW earns the fallback, because a legacy V1-catalogue site is the single case
44
+ // the fallback exists for.
45
+ //
46
+ // WHAT IS ESTABLISHED vs WHAT IS ASSUMED, because the two are not the same here and a
47
+ // reviewer was right to ask. Established: V1-on-V3 answers HTTP 428 — `himi sdk
48
+ // wix-stores-collections` documents it and names the message. ASSUMED, and not yet
49
+ // verified against a live legacy site: that the reverse — the V3 Categories API, a separate
50
+ // app-namespaced service, called on a Catalog-V1 site — THROWS rather than answering
51
+ // `200 {}`. If it answers 200-empty, a legacy site stops reaching the fallback and shows
52
+ // "No categories yet" instead of its collections: the same wrong-empty-state this change
53
+ // fixes, moved to the other site type.
54
+ //
55
+ // It is left as a throw-only fallback deliberately, because the alternative is worse:
56
+ // falling back on a V3 empty result sends every modern site with a genuinely empty
57
+ // catalogue into the 428 path, and the catch below now surfaces that as "Couldn't load
58
+ // categories" over a catalogue that is simply empty. Confirm against a Catalog-V1 site
59
+ // before changing this; five templates carry this file.
60
+ return { ok: true, rows: (r.categories ?? []).map(fromCategory).filter((row) => row.id) };
61
+ } catch { /* V3 unavailable — a legacy V1 site is what the fallback is for */ }
32
62
  try {
33
- const r = await queryCollections({ query: { paging: { limit: 50 } } } as never);
34
- return (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection);
35
- } catch {
36
- return [];
63
+ // No `as never` here: `WixQueryCollectionsRequest.query` is `WixCollectionsQuery`, which
64
+ // declares `paging`, so this argument type-checks as written. Reach for a cast only when the
65
+ // types really cannot express the call — silencing them by habit hides the next real
66
+ // mismatch.
67
+ const r = await queryCollections({ query: { paging: { limit: 50 } } });
68
+ return { ok: true, rows: (((r as unknown as { collections?: WixCollection[] }).collections) ?? []).map(fromCollection) };
69
+ } catch (e) {
70
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
37
71
  }
38
72
  }
39
73
 
40
74
  const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollections", {
41
- state: { status: "loading", collections: [], emptyText: "" },
75
+ state: { status: "loading", collections: [], emptyText: "", error: "" },
42
76
  actions: {
43
77
  load: async () => {
44
- const collections = await loadCollections();
45
- return { status: "ready", collections, emptyText: collections.length ? "" : "No categories yet" };
78
+ const res = await loadCollections();
79
+ if (!res.ok) {
80
+ // `emptyText` carries the honest line as well as `error`, deliberately: this screen's
81
+ // descriptor binds only `emptyText`, so writing the failure there is what stops the
82
+ // visitor being told the catalogue is empty when the call actually failed. `status` and
83
+ // `error` are what an error-state binding and `himi test` read — bind them when you add
84
+ // a dedicated error view.
85
+ const line = "Couldn't load categories. Pull to refresh.";
86
+ return { status: "error", collections: [], emptyText: line, error: res.error };
87
+ }
88
+ return {
89
+ status: "ready",
90
+ collections: res.rows,
91
+ emptyText: res.rows.length ? "" : "No categories yet",
92
+ error: "",
93
+ };
46
94
  },
47
95
  },
48
96
  });