@jay-framework/wix-stores-v1 0.20.0 → 0.22.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.
@@ -4,8 +4,7 @@ import { makeJayStackComponent, makeJayInit } from "@jay-framework/fullstack-com
4
4
  import { registerReactiveGlobalContext, createSignal, createMemo, createEffect } from "@jay-framework/component";
5
5
  import { patch, REPLACE } from "@jay-framework/json-patch";
6
6
  import { createJayContext, useGlobalContext } from "@jay-framework/runtime";
7
- import { WIX_CLIENT_CONTEXT } from "@jay-framework/wix-server-client/client";
8
- import { products, collections } from "@wix/stores";
7
+ import { wixFetch, WIX_CLIENT_CONTEXT } from "@jay-framework/wix-server-client/client";
9
8
  import { createActionCaller } from "@jay-framework/stack-client-runtime";
10
9
  var StockStatus = /* @__PURE__ */ ((StockStatus2) => {
11
10
  StockStatus2[StockStatus2["OUT_OF_STOCK"] = 0] = "OUT_OF_STOCK";
@@ -17,21 +16,82 @@ var Selected = /* @__PURE__ */ ((Selected2) => {
17
16
  Selected2[Selected2["notSelected"] = 1] = "notSelected";
18
17
  return Selected2;
19
18
  })(Selected || {});
20
- const instances = {
21
- productsClientInstance: void 0,
22
- collectionsClientInstance: void 0
23
- };
24
- function getProductsClient(wixClient) {
25
- if (!instances.productsClientInstance) {
26
- instances.productsClientInstance = wixClient.use(products);
19
+ function normalizeProduct(product) {
20
+ const p = { ...product };
21
+ const raw = product;
22
+ if (raw.id && !p._id) {
23
+ p._id = raw.id;
24
+ }
25
+ return p;
26
+ }
27
+ async function queryProducts(client, request) {
28
+ const query = {};
29
+ if (request?.filter) query.filter = JSON.stringify(request.filter);
30
+ if (request?.sort) {
31
+ const v1Sort = request.sort.map((s) => ({
32
+ [s.fieldName]: (s.order || "ASC").toLowerCase()
33
+ }));
34
+ query.sort = JSON.stringify(v1Sort);
27
35
  }
28
- return instances.productsClientInstance;
36
+ if (request?.paging) query.paging = request.paging;
37
+ const result = await wixFetch(client, "/stores/v1/products/query", {
38
+ method: "POST",
39
+ body: {
40
+ query,
41
+ includeVariants: request?.includeVariants ?? true,
42
+ includeMerchantSpecificData: request?.includeMerchantSpecificData ?? true
43
+ }
44
+ });
45
+ if (result.products) {
46
+ result.products = result.products.map(
47
+ (p) => normalizeProduct(p)
48
+ );
49
+ }
50
+ return result;
29
51
  }
30
- function getCollectionsClient(wixClient) {
31
- if (!instances.collectionsClientInstance) {
32
- instances.collectionsClientInstance = wixClient.use(collections);
52
+ async function getProduct(client, productId, options) {
53
+ const params = new URLSearchParams();
54
+ const qs = params.toString();
55
+ const result = await wixFetch(
56
+ client,
57
+ `/stores/v1/products/${productId}${qs ? "?" + qs : ""}`,
58
+ { method: "GET" }
59
+ );
60
+ if (result.product) {
61
+ const raw = result.product;
62
+ if (raw.id && !result.product._id) {
63
+ result.product._id = raw.id;
64
+ }
33
65
  }
34
- return instances.collectionsClientInstance;
66
+ return result;
67
+ }
68
+ function normalizeCollection(col) {
69
+ const c = { ...col };
70
+ if (col.id && !c._id) {
71
+ c._id = col.id;
72
+ }
73
+ return c;
74
+ }
75
+ async function queryCollections(client, request) {
76
+ const query = {};
77
+ const result = await wixFetch(
78
+ client,
79
+ "/stores/v1/collections/query",
80
+ {
81
+ method: "POST",
82
+ body: {
83
+ query,
84
+ includeNumberOfProducts: true,
85
+ includeDescription: true
86
+ }
87
+ }
88
+ );
89
+ if (result.collections) {
90
+ result.collections = result.collections.map(
91
+ (c) => normalizeCollection(c)
92
+ );
93
+ }
94
+ return result;
35
95
  }
36
96
  var MediaType = /* @__PURE__ */ ((MediaType2) => {
37
97
  MediaType2[MediaType2["IMAGE"] = 0] = "IMAGE";
@@ -71,6 +131,9 @@ var ChoiceType = /* @__PURE__ */ ((ChoiceType2) => {
71
131
  ChoiceType2[ChoiceType2["ONE_COLOR"] = 1] = "ONE_COLOR";
72
132
  return ChoiceType2;
73
133
  })(ChoiceType || {});
134
+ function stripWixMediaResize(url) {
135
+ return url.replace(/\/v1\/(?:fit|fill|crop)\/[^/]+\/file\.\w+$/, "");
136
+ }
74
137
  function mapAvailabilityStatus(status) {
75
138
  switch (status) {
76
139
  case "OUT_OF_STOCK":
@@ -145,14 +208,12 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH)
145
208
  slug,
146
209
  productUrl: slug ? `${productPagePath}/${slug}` : "",
147
210
  mainMedia: {
148
- // V1 provides complete URLs
149
- url: mainMedia?.image?.url || "",
211
+ url: stripWixMediaResize(mainMedia?.image?.url || ""),
150
212
  altText: mainMedia?.title || product.name || "",
151
213
  mediaType: mapMediaType(mainMedia?.mediaType)
152
214
  },
153
215
  thumbnail: {
154
- // V1 provides complete thumbnail URLs
155
- url: mainMedia?.thumbnail?.url || "",
216
+ url: stripWixMediaResize(mainMedia?.thumbnail?.url || ""),
156
217
  altText: mainMedia?.title || product.name || "",
157
218
  width: mainMedia?.thumbnail?.width || 300,
158
219
  height: mainMedia?.thumbnail?.height || 300
@@ -188,7 +249,7 @@ function mapCollectionToViewState(collection) {
188
249
  name: collection.name || "",
189
250
  slug: collection.slug || "",
190
251
  description: collection.description || "",
191
- imageUrl: collection.media?.mainMedia?.image?.url || "",
252
+ imageUrl: stripWixMediaResize(collection.media?.mainMedia?.image?.url || ""),
192
253
  productCount: collection.numberOfProducts || 0
193
254
  };
194
255
  }
@@ -197,15 +258,13 @@ function provideWixStoresV1Context() {
197
258
  const wixClientContext = useGlobalContext(WIX_CLIENT_CONTEXT);
198
259
  const wixClient = wixClientContext.client;
199
260
  const cartContext = useGlobalContext(WIX_CART_CONTEXT);
200
- const productsClient = getProductsClient(wixClient);
201
- const collectionsClient = getCollectionsClient(wixClient);
202
261
  const storesContext = registerReactiveGlobalContext(WIX_STORES_V1_CONTEXT, () => {
203
262
  async function addToCart(productId, quantity = 1, variantId) {
204
263
  console.log(`[WixStoresV1] Adding to cart: ${productId} x ${quantity}`);
205
264
  let finalVariantId = variantId;
206
265
  let productSlug;
207
266
  try {
208
- const productResult = await productsClient.getProduct(productId);
267
+ const productResult = await getProduct(wixClient, productId);
209
268
  const product = productResult.product;
210
269
  productSlug = product?.slug;
211
270
  if (!finalVariantId && product?.variants?.[0]) {
@@ -221,11 +280,14 @@ function provideWixStoresV1Context() {
221
280
  }
222
281
  async function loadMoreCollectionProducts(collectionId, page, pageSize) {
223
282
  try {
224
- const result = await productsClient.queryProducts().hasSome("collectionIds", [collectionId]).skip((page - 1) * pageSize).limit(pageSize).find();
225
- const products2 = (result.items || []).map((p) => mapProductToCard(p));
226
- const totalProducts = result.totalCount ?? products2.length;
283
+ const result = await queryProducts(wixClient, {
284
+ filter: { "collections.id": { $hasSome: [collectionId] } },
285
+ paging: { limit: pageSize, offset: (page - 1) * pageSize }
286
+ });
287
+ const products = (result.products || []).map((p) => mapProductToCard(p));
288
+ const totalProducts = result.totalResults ?? products.length;
227
289
  const hasMore = page * pageSize < totalProducts;
228
- return { products: products2, hasMore, totalProducts };
290
+ return { products, hasMore, totalProducts };
229
291
  } catch (error) {
230
292
  console.error("[WixStoresV1] Failed to load collection products:", error);
231
293
  return { products: [], hasMore: false, totalProducts: 0 };
@@ -233,8 +295,8 @@ function provideWixStoresV1Context() {
233
295
  }
234
296
  async function getCollections() {
235
297
  try {
236
- const result = await collectionsClient.queryCollections().find();
237
- return (result.items || []).map((col) => mapCollectionToViewState(col));
298
+ const result = await queryCollections(wixClient);
299
+ return (result.collections || []).map((col) => mapCollectionToViewState(col));
238
300
  } catch (error) {
239
301
  console.error("[WixStoresV1] Failed to load collections:", error);
240
302
  return [];
@@ -693,7 +755,7 @@ function ProductSearchInteractive(props, refs, viewStateSignals, fastCarryForwar
693
755
  const productSearch = makeJayStackComponent().withProps().withContexts(WIX_STORES_V1_CONTEXT).withInteractive(ProductSearchInteractive);
694
756
  const PAGE_SIZE = 20;
695
757
  function CollectionPageInteractive(_props, refs, viewStateSignals, fastCarryForward, storesContext) {
696
- const { products: [products2], loadedProducts: [loadedProducts, setLoadedProducts], hasMore: [hasMore, setHasMore], loadedCount: [loadedCount, setLoadedCount], isLoading: [isLoading, setIsLoading], hasProducts: [hasProducts, setHasProducts] } = viewStateSignals;
758
+ const { products: [products], loadedProducts: [loadedProducts, setLoadedProducts], hasMore: [hasMore, setHasMore], loadedCount: [loadedCount, setLoadedCount], isLoading: [isLoading, setIsLoading], hasProducts: [hasProducts, setHasProducts] } = viewStateSignals;
697
759
  const { collectionId, totalProducts } = fastCarryForward;
698
760
  let currentOffset = fastCarryForward.currentOffset;
699
761
  refs.loadMoreButton?.onclick(async () => {
@@ -763,7 +825,7 @@ function CollectionPageInteractive(_props, refs, viewStateSignals, fastCarryForw
763
825
  });
764
826
  return {
765
827
  render: () => ({
766
- products: products2(),
828
+ products: products(),
767
829
  loadedProducts: loadedProducts(),
768
830
  hasMore: hasMore(),
769
831
  loadedCount: loadedCount(),
package/dist/index.d.ts CHANGED
@@ -1,73 +1,17 @@
1
- import { getCurrentCartClient, CartState } from '@jay-framework/wix-cart';
1
+ import { CartState } from '@jay-framework/wix-cart';
2
2
  export { AddToCartOptions, CartIndicatorState, CartLineItem, CartState, CartSummary, WIX_CART_CONTEXT, WIX_CART_SERVICE, WixCartContext, WixCartService, cartIndicator, cartPage, provideWixCartContext, provideWixCartService } from '@jay-framework/wix-cart';
3
3
  import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
4
4
  import { Signals, PageProps, UrlParams } from '@jay-framework/fullstack-component';
5
5
  import { WixClient } from '@wix/sdk';
6
- import { products, collections, inventory } from '@wix/stores';
7
- import { BuildDescriptors } from '@wix/sdk-types';
8
6
  import * as _jay_framework_runtime from '@jay-framework/runtime';
9
7
  import { HTMLElementCollectionProxy, HTMLElementProxy } from '@jay-framework/runtime';
10
8
  import { Getter } from '@jay-framework/reactive';
11
9
  import * as _jay_framework_component from '@jay-framework/component';
12
10
 
13
- /**
14
- * Wix Store V1 API Client Factories
15
- *
16
- * These functions create singleton instances of Wix Catalog V1 API clients.
17
- * Used by both the server service and client context.
18
- *
19
- * Key difference from V3:
20
- * - Uses `products` module instead of `productsV3`
21
- * - Uses `collections` module instead of `@wix/categories`
22
- *
23
- * Note: Cart client is provided by @jay-framework/wix-cart package.
24
- */
25
-
26
- /**
27
- * Get a configured Wix Stores Products client (Catalog V1) (singleton)
28
- *
29
- * The Products API (V1) is the original Wix Stores catalog API.
30
- *
31
- * @returns Products client instance from @wix/stores
32
- * @see https://dev.wix.com/docs/sdk/backend-modules/stores/products/introduction
33
- */
34
- declare function getProductsClient(wixClient: WixClient): BuildDescriptors<typeof products, {}>;
35
- /**
36
- * Get a configured Wix Stores Collections client (singleton)
37
- *
38
- * The Collections API allows you to manage product Collections in your Wix store.
39
- * Note: V3 uses @wix/categories instead.
40
- *
41
- * @returns Collections client instance from @wix/stores
42
- * @see https://dev.wix.com/docs/sdk/backend-modules/stores/collections/introduction
43
- */
44
- declare function getCollectionsClient(wixClient: WixClient): BuildDescriptors<typeof collections, {}>;
45
- /**
46
- * Get a configured Wix Stores Inventory client (singleton)
47
- *
48
- * The Inventory API allows you to manage product inventory in your Wix store.
49
- *
50
- * @returns Inventory client instance from @wix/stores
51
- * @see https://dev.wix.com/docs/sdk/backend-modules/stores/inventory/introduction
52
- */
53
- declare function getInventoryClient(wixClient: WixClient): BuildDescriptors<typeof inventory, {}>;
54
-
55
11
  interface WixStoresV1Service {
56
- products: ReturnType<typeof getProductsClient>;
57
- collections: ReturnType<typeof getCollectionsClient>;
58
- inventory: ReturnType<typeof getInventoryClient>;
59
- /** @deprecated Use WIX_CART_SERVICE from @jay-framework/wix-cart instead */
60
- cart: ReturnType<typeof getCurrentCartClient>;
12
+ wixClient: WixClient;
61
13
  }
62
- /**
63
- * Server service marker for Wix Stores V1.
64
- * Use with `.withServices(WIX_STORES_V1_SERVICE_MARKER)` in component definitions.
65
- */
66
14
  declare const WIX_STORES_V1_SERVICE_MARKER: _jay_framework_fullstack_component.ServiceMarker<WixStoresV1Service>;
67
- /**
68
- * Creates, registers, and returns a Wix Stores V1 service instance.
69
- * Called during server initialization.
70
- */
71
15
  declare function provideWixStoresV1Service(wixClient: WixClient): WixStoresV1Service;
72
16
 
73
17
  declare enum OptionRenderType$1 {
@@ -188,6 +132,37 @@ interface ProductCardRepeatedRefs {
188
132
  quickOption: ProductOptionsRepeatedRefs
189
133
  }
190
134
 
135
+ interface V1Media {
136
+ mainMedia?: V1MediaItem;
137
+ items?: V1MediaItem[];
138
+ }
139
+ interface V1MediaItem {
140
+ _id?: string;
141
+ mediaType?: string;
142
+ title?: string;
143
+ image?: {
144
+ url?: string;
145
+ width?: number;
146
+ height?: number;
147
+ };
148
+ video?: {
149
+ url?: string;
150
+ };
151
+ thumbnail?: {
152
+ url?: string;
153
+ width?: number;
154
+ height?: number;
155
+ };
156
+ }
157
+ interface V1Collection {
158
+ _id?: string;
159
+ name?: string;
160
+ slug?: string;
161
+ description?: string;
162
+ media?: V1Media;
163
+ numberOfProducts?: number;
164
+ }
165
+
191
166
  /**
192
167
  * Wix Stores Catalog V1 Product Mapping Utilities
193
168
  *
@@ -203,20 +178,6 @@ interface ProductCardRepeatedRefs {
203
178
  * Reference: wix/exploration/query-products-catalog-v1/output/individual/*.json
204
179
  */
205
180
 
206
- interface V1Collection {
207
- _id?: string;
208
- name?: string;
209
- slug?: string;
210
- description?: string;
211
- media?: {
212
- mainMedia?: {
213
- image?: {
214
- url: string;
215
- };
216
- };
217
- };
218
- numberOfProducts?: number;
219
- }
220
181
  interface CollectionViewState {
221
182
  _id: string;
222
183
  name: string;
@@ -273,39 +234,19 @@ interface WixStoresV1Context {
273
234
  */
274
235
  declare const WIX_STORES_V1_CONTEXT: _jay_framework_runtime.ContextMarker<WixStoresV1Context>;
275
236
 
276
- /**
277
- * Sort options for product search
278
- */
279
237
  type ProductSortField = 'relevance' | 'price_asc' | 'price_desc' | 'name_asc' | 'name_desc' | 'newest';
280
- /**
281
- * Product search filters
282
- */
283
238
  interface ProductSearchFilters {
284
- /** Minimum price filter */
285
239
  minPrice?: number;
286
- /** Maximum price filter */
287
240
  maxPrice?: number;
288
- /** Filter by collection IDs (V1 uses collections, not categories) */
289
241
  collectionIds?: string[];
290
242
  }
291
- /**
292
- * Input for searchProducts action
293
- */
294
243
  interface SearchProductsInput {
295
- /** Search query text */
296
244
  query: string;
297
- /** Filters to apply */
298
245
  filters?: ProductSearchFilters;
299
- /** Sort order */
300
246
  sortBy?: ProductSortField;
301
- /** Page number for pagination (1-based) */
302
247
  page?: number;
303
- /** Items per page (default: 12) */
304
248
  pageSize?: number;
305
249
  }
306
- /**
307
- * Price range bucket for filter UI
308
- */
309
250
  interface PriceRangeBucket {
310
251
  rangeId: string;
311
252
  label: string;
@@ -313,96 +254,24 @@ interface PriceRangeBucket {
313
254
  maxValue: number | null;
314
255
  isSelected: boolean;
315
256
  }
316
- /**
317
- * Price aggregation data for filter UI
318
- */
319
257
  interface PriceAggregationData {
320
- /** Minimum price across all products */
321
258
  minBound: number;
322
- /** Maximum price across all products */
323
259
  maxBound: number;
324
- /** Computed price range buckets */
325
260
  ranges: PriceRangeBucket[];
326
261
  }
327
- /**
328
- * Output for searchProducts action
329
- */
330
262
  interface SearchProductsOutput {
331
- /** List of matching products */
332
263
  products: ProductCardViewState[];
333
- /** Total number of matching products */
334
264
  totalCount: number;
335
- /** Current page number */
336
265
  currentPage: number;
337
- /** Total number of pages */
338
266
  totalPages: number;
339
- /** Whether there are more results */
340
267
  hasMore: boolean;
341
- /** Price aggregation data (bounds and computed ranges) */
342
268
  priceAggregation: PriceAggregationData;
343
269
  }
344
- /**
345
- * Input for getProductBySlug action
346
- */
347
270
  interface GetProductBySlugInput {
348
- /** Product URL slug */
349
271
  slug: string;
350
272
  }
351
- /**
352
- * Search products using the Wix Stores Catalog V1 queryProducts API.
353
- *
354
- * V1 uses skip-based pagination and query builder syntax.
355
- *
356
- * Server-side filtering/sorting:
357
- * - name: startsWith() for text search
358
- * - collectionIds: hasSome() for collection filtering
359
- * - priceData.price: ge()/le() for price range filtering
360
- * - Sorting: ascending()/descending() on price, name, lastUpdated
361
- *
362
- * Also fetches min/max prices in parallel for price range filter UI.
363
- *
364
- * @see https://dev.wix.com/docs/sdk/backend-modules/stores/products/query-products
365
- *
366
- * @example
367
- * ```typescript
368
- * const results = await searchProducts({
369
- * query: 'whisky',
370
- * filters: { minPrice: 50, maxPrice: 200 },
371
- * sortBy: 'price_asc',
372
- * pageSize: 12,
373
- * page: 1
374
- * });
375
- * // results.priceAggregation = {
376
- * // minBound: 25,
377
- * // maxBound: 500,
378
- * // ranges: [
379
- * // { rangeId: 'all', label: 'Show all', minValue: null, maxValue: null, isSelected: true },
380
- * // { rangeId: '0-100', label: '$0 - $100', minValue: 0, maxValue: 100, isSelected: false },
381
- * // { rangeId: '100-200', label: '$100 - $200', ... },
382
- * // ...
383
- * // ]
384
- * // }
385
- * ```
386
- */
387
273
  declare const searchProducts: _jay_framework_fullstack_component.JayAction<SearchProductsInput, SearchProductsOutput> & _jay_framework_fullstack_component.JayActionDefinition<SearchProductsInput, SearchProductsOutput, [WixStoresV1Service]>;
388
- /**
389
- * Get a single product by its URL slug.
390
- *
391
- * @example
392
- * ```typescript
393
- * const product = await getProductBySlug({ slug: 'peat-s-beast-px-finish-54-1' });
394
- * ```
395
- */
396
274
  declare const getProductBySlug: _jay_framework_fullstack_component.JayAction<GetProductBySlugInput, ProductCardViewState> & _jay_framework_fullstack_component.JayActionDefinition<GetProductBySlugInput, ProductCardViewState, [WixStoresV1Service]>;
397
- /**
398
- * Get available collections for filtering.
399
- * V1 uses collections instead of categories.
400
- *
401
- * @example
402
- * ```typescript
403
- * const collections = await getCollections();
404
- * ```
405
- */
406
275
  declare const getCollections: _jay_framework_fullstack_component.JayAction<Record<string, never>, CollectionViewState[]> & _jay_framework_fullstack_component.JayActionDefinition<Record<string, never>, CollectionViewState[], [WixStoresV1Service]>;
407
276
 
408
277
  declare enum MediaType$1 {
package/dist/index.js CHANGED
@@ -1,46 +1,76 @@
1
- import { getCurrentCartClient } from "@jay-framework/wix-cart";
2
1
  import { WIX_CART_CONTEXT, WIX_CART_SERVICE, cartIndicator, cartPage, provideWixCartContext, provideWixCartService } from "@jay-framework/wix-cart";
3
- import { inventory, collections, products } from "@wix/stores";
4
2
  import { createJayService, makeJayQuery, ActionError, makeJayStackComponent, RenderPipeline, makeJayInit } from "@jay-framework/fullstack-component";
5
3
  import { registerService, getService } from "@jay-framework/stack-server-runtime";
6
4
  import { createJayContext } from "@jay-framework/runtime";
7
5
  import "@jay-framework/component";
8
- import { WIX_CLIENT_SERVICE } from "@jay-framework/wix-server-client";
9
- const instances = {
10
- productsClientInstance: void 0,
11
- collectionsClientInstance: void 0,
12
- inventoryClientInstance: void 0
13
- };
14
- function getProductsClient(wixClient) {
15
- if (!instances.productsClientInstance) {
16
- instances.productsClientInstance = wixClient.use(products);
17
- }
18
- return instances.productsClientInstance;
19
- }
20
- function getCollectionsClient(wixClient) {
21
- if (!instances.collectionsClientInstance) {
22
- instances.collectionsClientInstance = wixClient.use(collections);
23
- }
24
- return instances.collectionsClientInstance;
25
- }
26
- function getInventoryClient(wixClient) {
27
- if (!instances.inventoryClientInstance) {
28
- instances.inventoryClientInstance = wixClient.use(inventory);
29
- }
30
- return instances.inventoryClientInstance;
31
- }
6
+ import { wixFetch, WIX_CLIENT_SERVICE } from "@jay-framework/wix-server-client";
32
7
  const WIX_STORES_V1_SERVICE_MARKER = createJayService("Wix Store V1 Service");
33
8
  function provideWixStoresV1Service(wixClient) {
34
- const service = {
35
- products: getProductsClient(wixClient),
36
- collections: getCollectionsClient(wixClient),
37
- inventory: getInventoryClient(wixClient),
38
- // Keep cart for backward compatibility, but prefer WIX_CART_SERVICE
39
- cart: getCurrentCartClient(wixClient)
40
- };
9
+ const service = { wixClient };
41
10
  registerService(WIX_STORES_V1_SERVICE_MARKER, service);
42
11
  return service;
43
12
  }
13
+ function normalizeProduct(product) {
14
+ const p = { ...product };
15
+ const raw = product;
16
+ if (raw.id && !p._id) {
17
+ p._id = raw.id;
18
+ }
19
+ return p;
20
+ }
21
+ async function queryProducts(client, request) {
22
+ const query = {};
23
+ if (request?.filter) query.filter = JSON.stringify(request.filter);
24
+ if (request?.sort) {
25
+ const v1Sort = request.sort.map((s) => ({
26
+ [s.fieldName]: (s.order || "ASC").toLowerCase()
27
+ }));
28
+ query.sort = JSON.stringify(v1Sort);
29
+ }
30
+ if (request?.paging) query.paging = request.paging;
31
+ const result = await wixFetch(client, "/stores/v1/products/query", {
32
+ method: "POST",
33
+ body: {
34
+ query,
35
+ includeVariants: request?.includeVariants ?? true,
36
+ includeMerchantSpecificData: request?.includeMerchantSpecificData ?? true
37
+ }
38
+ });
39
+ if (result.products) {
40
+ result.products = result.products.map(
41
+ (p) => normalizeProduct(p)
42
+ );
43
+ }
44
+ return result;
45
+ }
46
+ function normalizeCollection(col) {
47
+ const c = { ...col };
48
+ if (col.id && !c._id) {
49
+ c._id = col.id;
50
+ }
51
+ return c;
52
+ }
53
+ async function queryCollections(client, request) {
54
+ const query = {};
55
+ const result = await wixFetch(
56
+ client,
57
+ "/stores/v1/collections/query",
58
+ {
59
+ method: "POST",
60
+ body: {
61
+ query,
62
+ includeNumberOfProducts: true,
63
+ includeDescription: true
64
+ }
65
+ }
66
+ );
67
+ if (result.collections) {
68
+ result.collections = result.collections.map(
69
+ (c) => normalizeCollection(c)
70
+ );
71
+ }
72
+ return result;
73
+ }
44
74
  var MediaType$2 = /* @__PURE__ */ ((MediaType2) => {
45
75
  MediaType2[MediaType2["IMAGE"] = 0] = "IMAGE";
46
76
  MediaType2[MediaType2["VIDEO"] = 1] = "VIDEO";
@@ -79,6 +109,9 @@ var ChoiceType$1 = /* @__PURE__ */ ((ChoiceType2) => {
79
109
  ChoiceType2[ChoiceType2["ONE_COLOR"] = 1] = "ONE_COLOR";
80
110
  return ChoiceType2;
81
111
  })(ChoiceType$1 || {});
112
+ function stripWixMediaResize(url) {
113
+ return url.replace(/\/v1\/(?:fit|fill|crop)\/[^/]+\/file\.\w+$/, "");
114
+ }
82
115
  function mapAvailabilityStatus(status) {
83
116
  switch (status) {
84
117
  case "OUT_OF_STOCK":
@@ -153,14 +186,12 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH)
153
186
  slug,
154
187
  productUrl: slug ? `${productPagePath}/${slug}` : "",
155
188
  mainMedia: {
156
- // V1 provides complete URLs
157
- url: mainMedia?.image?.url || "",
189
+ url: stripWixMediaResize(mainMedia?.image?.url || ""),
158
190
  altText: mainMedia?.title || product.name || "",
159
191
  mediaType: mapMediaType(mainMedia?.mediaType)
160
192
  },
161
193
  thumbnail: {
162
- // V1 provides complete thumbnail URLs
163
- url: mainMedia?.thumbnail?.url || "",
194
+ url: stripWixMediaResize(mainMedia?.thumbnail?.url || ""),
164
195
  altText: mainMedia?.title || product.name || "",
165
196
  width: mainMedia?.thumbnail?.width || 300,
166
197
  height: mainMedia?.thumbnail?.height || 300
@@ -196,7 +227,7 @@ function mapCollectionToViewState(collection) {
196
227
  name: collection.name || "",
197
228
  slug: collection.slug || "",
198
229
  description: collection.description || "",
199
- imageUrl: collection.media?.mainMedia?.image?.url || "",
230
+ imageUrl: stripWixMediaResize(collection.media?.mainMedia?.image?.url || ""),
200
231
  productCount: collection.numberOfProducts || 0
201
232
  };
202
233
  }
@@ -234,10 +265,9 @@ function generatePriceBuckets(minPrice, maxPrice, currencySymbol = "$") {
234
265
  const from = Math.round(allBoundaries[i]);
235
266
  const to = Math.round(allBoundaries[i + 1]);
236
267
  if (from < to) {
237
- const label = `${currencySymbol}${from} - ${currencySymbol}${to}`;
238
268
  buckets.push({
239
269
  rangeId: `${from}-${to}`,
240
- label,
270
+ label: `${currencySymbol}${from} - ${currencySymbol}${to}`,
241
271
  minValue: from,
242
272
  maxValue: to,
243
273
  isSelected: false
@@ -246,63 +276,81 @@ function generatePriceBuckets(minPrice, maxPrice, currencySymbol = "$") {
246
276
  }
247
277
  return buckets;
248
278
  }
279
+ function buildBaseFilter(query, filters) {
280
+ const conditions = [];
281
+ if (query && query.trim().length > 0) {
282
+ conditions.push({ name: { $startsWith: query.trim() } });
283
+ }
284
+ if (filters.collectionIds && filters.collectionIds.length > 0) {
285
+ conditions.push({ "collections.id": { $hasSome: filters.collectionIds } });
286
+ }
287
+ if (conditions.length === 0) return {};
288
+ if (conditions.length === 1) return conditions[0];
289
+ return { $and: conditions };
290
+ }
291
+ function buildPriceFilter(baseFilter, minPrice, maxPrice) {
292
+ const priceFilter = {};
293
+ if (minPrice !== void 0 && minPrice > 0) {
294
+ priceFilter.price = { $gte: minPrice };
295
+ }
296
+ if (maxPrice !== void 0 && maxPrice > 0) {
297
+ const existing = priceFilter.price;
298
+ priceFilter.price = typeof existing === "object" && existing !== null ? { ...existing, $lte: maxPrice } : { $lte: maxPrice };
299
+ }
300
+ if (Object.keys(priceFilter).length === 0) return baseFilter;
301
+ if (Object.keys(baseFilter).length === 0) return priceFilter;
302
+ return { $and: [baseFilter, priceFilter] };
303
+ }
249
304
  const searchProducts = makeJayQuery("wixStoresV1.searchProducts").withServices(WIX_STORES_V1_SERVICE_MARKER).withHandler(
250
305
  async (input, wixStores) => {
251
306
  const { query, filters = {}, sortBy = "relevance", page = 1, pageSize = 12 } = input;
252
307
  try {
253
- const buildBaseQuery = () => {
254
- let q = wixStores.products.queryProducts();
255
- if (query && query.trim().length > 0) {
256
- q = q.startsWith("name", query.trim());
257
- }
258
- if (filters.collectionIds && filters.collectionIds.length > 0) {
259
- q = q.hasSome("collectionIds", filters.collectionIds);
260
- }
261
- return q;
262
- };
263
- const buildProductsQuery = () => {
264
- let q = buildBaseQuery().limit(pageSize).skip((page - 1) * pageSize);
265
- if (filters.minPrice !== void 0 && filters.minPrice > 0) {
266
- q = q.ge("priceData.price", filters.minPrice);
267
- }
268
- if (filters.maxPrice !== void 0 && filters.maxPrice > 0) {
269
- q = q.le("priceData.price", filters.maxPrice);
270
- }
271
- switch (sortBy) {
272
- case "price_asc":
273
- q = q.ascending("price");
274
- break;
275
- case "price_desc":
276
- q = q.descending("price");
277
- break;
278
- case "name_asc":
279
- q = q.ascending("name");
280
- break;
281
- case "name_desc":
282
- q = q.descending("name");
283
- break;
284
- case "newest":
285
- q = q.descending("lastUpdated");
286
- break;
287
- }
288
- return q;
289
- };
290
- const minPriceQuery = buildBaseQuery().ascending("price").limit(1).find();
291
- const maxPriceQuery = buildBaseQuery().descending("price").limit(1).find();
308
+ const baseFilter = buildBaseFilter(query, filters);
309
+ const fullFilter = buildPriceFilter(baseFilter, filters.minPrice, filters.maxPrice);
310
+ const sort = [];
311
+ switch (sortBy) {
312
+ case "price_asc":
313
+ sort.push({ fieldName: "price", order: "ASC" });
314
+ break;
315
+ case "price_desc":
316
+ sort.push({ fieldName: "price", order: "DESC" });
317
+ break;
318
+ case "name_asc":
319
+ sort.push({ fieldName: "name", order: "ASC" });
320
+ break;
321
+ case "name_desc":
322
+ sort.push({ fieldName: "name", order: "DESC" });
323
+ break;
324
+ case "newest":
325
+ sort.push({ fieldName: "lastUpdated", order: "DESC" });
326
+ break;
327
+ }
292
328
  const [result, minPriceResult, maxPriceResult] = await Promise.all([
293
- buildProductsQuery().find(),
294
- minPriceQuery,
295
- maxPriceQuery
329
+ queryProducts(wixStores.wixClient, {
330
+ filter: fullFilter,
331
+ sort: sort.length > 0 ? sort : void 0,
332
+ paging: { limit: pageSize, offset: (page - 1) * pageSize }
333
+ }),
334
+ queryProducts(wixStores.wixClient, {
335
+ filter: baseFilter,
336
+ sort: [{ fieldName: "price", order: "ASC" }],
337
+ paging: { limit: 1 }
338
+ }),
339
+ queryProducts(wixStores.wixClient, {
340
+ filter: baseFilter,
341
+ sort: [{ fieldName: "price", order: "DESC" }],
342
+ paging: { limit: 1 }
343
+ })
296
344
  ]);
297
- const products2 = result.items || [];
298
- const minBound = minPriceResult.items?.[0]?.price?.price ?? minPriceResult.items?.[0]?.priceData?.price ?? 0;
299
- const maxBound = maxPriceResult.items?.[0]?.price?.price ?? maxPriceResult.items?.[0]?.priceData?.price ?? 0;
300
- const currency = products2[0]?.price?.currency || products2[0]?.priceData?.currency || minPriceResult.items?.[0]?.price?.currency;
345
+ const products = result.products || [];
346
+ const minBound = minPriceResult.products?.[0]?.price?.price ?? 0;
347
+ const maxBound = maxPriceResult.products?.[0]?.price?.price ?? 0;
348
+ const currency = products[0]?.price?.currency || minPriceResult.products?.[0]?.price?.currency;
301
349
  const currencySymbol = currency === "ILS" ? "₪" : currency === "USD" ? "$" : currency === "EUR" ? "€" : currency === "GBP" ? "£" : "$";
302
350
  const ranges = generatePriceBuckets(minBound, maxBound, currencySymbol);
303
- const totalCount = result.totalCount ?? result.items?.length ?? 0;
351
+ const totalCount = result.totalResults ?? products.length;
304
352
  const totalPages = Math.ceil(totalCount / pageSize);
305
- const mappedProducts = products2.map((p) => mapProductToCard(p));
353
+ const mappedProducts = products.map((p) => mapProductToCard(p));
306
354
  return {
307
355
  products: mappedProducts,
308
356
  totalCount,
@@ -324,14 +372,18 @@ const getProductBySlug = makeJayQuery("wixStoresV1.getProductBySlug").withServic
324
372
  throw new ActionError("INVALID_INPUT", "Product slug is required");
325
373
  }
326
374
  try {
327
- let result = await wixStores.products.queryProducts().eq("slug", slug).limit(1).find();
328
- if (!result.items?.length) {
329
- result = await wixStores.products.queryProducts().eq("_id", slug).limit(1).find();
330
- }
331
- const product = result.items?.[0];
332
- if (!product) {
333
- return null;
375
+ let result = await queryProducts(wixStores.wixClient, {
376
+ filter: { slug },
377
+ paging: { limit: 1 }
378
+ });
379
+ if (!result.products?.length) {
380
+ result = await queryProducts(wixStores.wixClient, {
381
+ filter: { id: slug },
382
+ paging: { limit: 1 }
383
+ });
334
384
  }
385
+ const product = result.products?.[0];
386
+ if (!product) return null;
335
387
  return mapProductToCard(product);
336
388
  } catch (error) {
337
389
  console.error("[wixStoresV1.getProductBySlug] Failed to get product:", error);
@@ -342,8 +394,8 @@ const getProductBySlug = makeJayQuery("wixStoresV1.getProductBySlug").withServic
342
394
  const getCollections = makeJayQuery("wixStoresV1.getCollections").withServices(WIX_STORES_V1_SERVICE_MARKER).withCaching({ maxAge: 3600 }).withHandler(
343
395
  async (_input, wixStores) => {
344
396
  try {
345
- const result = await wixStores.collections.queryCollections().find();
346
- return (result.items || []).map((col) => mapCollectionToViewState(col));
397
+ const result = await queryCollections(wixStores.wixClient);
398
+ return (result.collections || []).map((col) => mapCollectionToViewState(col));
347
399
  } catch (error) {
348
400
  console.error("[wixStoresV1.getCollections] Failed to load collections:", error);
349
401
  throw new ActionError("LOAD_FAILED", "Failed to load collections");
@@ -399,7 +451,7 @@ function mapInfoSections(sections) {
399
451
  function mapMedia(product) {
400
452
  const mainMedia = product.media?.mainMedia;
401
453
  const mediaItems = product.media?.items || [];
402
- const mainUrl = mainMedia?.image?.url || "";
454
+ const mainUrl = stripWixMediaResize(mainMedia?.image?.url || "");
403
455
  const mainMediaType = mainMedia?.mediaType === "video" ? MediaType$1.VIDEO : MediaType$1.IMAGE;
404
456
  return {
405
457
  selectedMedia: {
@@ -409,7 +461,7 @@ function mapMedia(product) {
409
461
  availableMedia: mediaItems.map((item, index) => ({
410
462
  mediaId: item._id || String(index),
411
463
  media: {
412
- url: item.image?.url || "",
464
+ url: stripWixMediaResize(item.image?.url || ""),
413
465
  mediaType: item.mediaType === "video" ? MediaType$1.VIDEO : MediaType$1.IMAGE
414
466
  },
415
467
  selected: item._id === mainMedia?._id ? Selected.selected : Selected.notSelected
@@ -478,12 +530,18 @@ function mapSeoHeadTags(product, seoData) {
478
530
  return headTags;
479
531
  }
480
532
  async function* loadProductParams([wixStores]) {
533
+ const PAGE_SIZE2 = 100;
481
534
  try {
482
- let result = await wixStores.products.queryProducts().find();
483
- yield result.items.map((product) => ({ slug: product.slug || "" }));
484
- while (result.hasNext()) {
485
- result = await result.next();
486
- yield result.items.map((product) => ({ slug: product.slug || "" }));
535
+ let offset = 0;
536
+ let hasMore = true;
537
+ while (hasMore) {
538
+ const result = await queryProducts(wixStores.wixClient, {
539
+ paging: { limit: PAGE_SIZE2, offset }
540
+ });
541
+ const products = result.products || [];
542
+ yield products.map((product) => ({ slug: product.slug || "" }));
543
+ offset += products.length;
544
+ hasMore = products.length === PAGE_SIZE2;
487
545
  }
488
546
  } catch (error) {
489
547
  console.error("[ProductPage V1] Failed to load product slugs:", error);
@@ -493,15 +551,21 @@ async function* loadProductParams([wixStores]) {
493
551
  async function renderSlowlyChanging$3(props, wixStores) {
494
552
  const Pipeline = RenderPipeline.for();
495
553
  return Pipeline.try(async () => {
496
- const bySlug = await wixStores.products.queryProducts().eq("slug", props.slug).limit(1).find();
497
- if (bySlug.items?.length)
554
+ const bySlug = await queryProducts(wixStores.wixClient, {
555
+ filter: { slug: props.slug },
556
+ paging: { limit: 1 }
557
+ });
558
+ if (bySlug.products?.length)
498
559
  return bySlug;
499
- return wixStores.products.queryProducts().eq("_id", props.slug).limit(1).find();
560
+ return queryProducts(wixStores.wixClient, {
561
+ filter: { id: props.slug },
562
+ paging: { limit: 1 }
563
+ });
500
564
  }).recover((error) => {
501
565
  console.error("[ProductPage V1] Error loading product:", error);
502
566
  return Pipeline.clientError(404, "Product not found");
503
567
  }).toPhaseOutput((result) => {
504
- const product = result.items?.[0];
568
+ const product = result.products?.[0];
505
569
  if (!product) {
506
570
  throw new Error("Product not found");
507
571
  }
@@ -570,13 +634,13 @@ const PAGE_SIZE$1 = 12;
570
634
  async function renderSlowlyChanging$2(props, wixStores) {
571
635
  const Pipeline = RenderPipeline.for();
572
636
  return Pipeline.try(async () => {
573
- const collectionsResult = await wixStores.collections.queryCollections().find();
574
- return collectionsResult.items || [];
637
+ const collectionsResult = await queryCollections(wixStores.wixClient);
638
+ return collectionsResult.collections || [];
575
639
  }).recover((error) => {
576
640
  console.error("[ProductSearch V1] Failed to load collections:", error);
577
641
  return Pipeline.ok([]);
578
- }).toPhaseOutput((collections2) => {
579
- const collectionInfos = collections2.map((col) => ({
642
+ }).toPhaseOutput((collections) => {
643
+ const collectionInfos = collections.map((col) => ({
580
644
  categoryId: col._id || "",
581
645
  categoryName: col.name || "",
582
646
  categorySlug: col.slug || ""
@@ -686,14 +750,14 @@ const productSearch = makeJayStackComponent().withProps().withServices(WIX_STORE
686
750
  async function renderSlowlyChanging$1(props, wixStores) {
687
751
  const Pipeline = RenderPipeline.for();
688
752
  return Pipeline.try(async () => {
689
- const result = await wixStores.collections.queryCollections().find();
690
- return result.items || [];
753
+ const result = await queryCollections(wixStores.wixClient);
754
+ return result.collections || [];
691
755
  }).recover((error) => {
692
756
  console.error("[CollectionList V1] Failed to load collections:", error);
693
757
  return Pipeline.ok([]);
694
- }).toPhaseOutput((collections2) => {
695
- const collectionItems = collections2.map((col) => {
696
- const imageUrl = col.media?.mainMedia?.image?.url || "";
758
+ }).toPhaseOutput((collections) => {
759
+ const collectionItems = collections.map((col) => {
760
+ const imageUrl = stripWixMediaResize(col.media?.mainMedia?.image?.url || "");
697
761
  return {
698
762
  _id: col._id || "",
699
763
  name: col.name || "",
@@ -735,19 +799,22 @@ var MediaType = /* @__PURE__ */ ((MediaType2) => {
735
799
  const PAGE_SIZE = 20;
736
800
  async function* loadCollectionParams([wixStores]) {
737
801
  try {
738
- const result = await wixStores.collections.queryCollections().find();
739
- yield (result.items || []).filter((col) => col.slug).map((col) => ({ slug: col.slug }));
802
+ const result = await queryCollections(wixStores.wixClient);
803
+ yield (result.collections || []).filter((col) => col.slug).map((col) => ({ slug: col.slug }));
740
804
  } catch (error) {
741
805
  console.error("[CollectionPage V1] Failed to load collection slugs:", error);
742
806
  yield [];
743
807
  }
744
808
  }
745
809
  async function loadCollectionProducts(collectionId, wixStores, offset = 0) {
746
- const result = await wixStores.products.queryProducts().hasSome("collectionIds", [collectionId]).skip(offset).limit(PAGE_SIZE).find();
747
- const products2 = (result.items || []).map((product) => mapProductToCard(product, "/products"));
810
+ const result = await queryProducts(wixStores.wixClient, {
811
+ filter: { "collections.id": { $hasSome: [collectionId] } },
812
+ paging: { limit: PAGE_SIZE, offset }
813
+ });
814
+ const products = (result.products || []).map((product) => mapProductToCard(product, "/products"));
748
815
  return {
749
- products: products2,
750
- total: result.totalCount || products2.length
816
+ products,
817
+ total: result.totalResults || products.length
751
818
  };
752
819
  }
753
820
  function mapCollectionMedia(collection) {
@@ -755,7 +822,7 @@ function mapCollectionMedia(collection) {
755
822
  return {
756
823
  mainMedia: mainMedia?.image?.url ? {
757
824
  _id: "",
758
- url: mainMedia.image.url,
825
+ url: stripWixMediaResize(mainMedia.image.url),
759
826
  altText: collection.name || "",
760
827
  mediaType: MediaType.IMAGE
761
828
  } : void 0,
@@ -765,8 +832,8 @@ function mapCollectionMedia(collection) {
765
832
  async function renderSlowlyChanging(props, wixStores) {
766
833
  const Pipeline = RenderPipeline.for();
767
834
  return Pipeline.try(async () => {
768
- const result = await wixStores.collections.queryCollections().find();
769
- const collection = (result.items || []).find((col) => col.slug === props.slug);
835
+ const result = await queryCollections(wixStores.wixClient);
836
+ const collection = (result.collections || []).find((col) => col.slug === props.slug);
770
837
  if (!collection) {
771
838
  throw new Error("Collection not found");
772
839
  }
@@ -871,7 +938,7 @@ async function setupWixStoresV1(ctx) {
871
938
  };
872
939
  }
873
940
  try {
874
- await service.products.queryProducts().limit(1).find();
941
+ await queryProducts(service.wixClient, { paging: { limit: 1 } });
875
942
  } catch (e) {
876
943
  const msg = e.message || "";
877
944
  const hint = msg.includes("404") || msg.includes("not found") ? "Wix Stores may not be installed on this site" : msg.includes("403") || msg.includes("permission") ? "API key may lack Wix Stores permissions" : "This package requires the Stores Catalog V1 API — if using Catalog V3, use @jay-framework/wix-stores instead";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/wix-stores-v1",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "description": "Wix Stores Catalog V1 API client for Jay Framework",
6
6
  "license": "Apache-2.0",
@@ -35,27 +35,25 @@
35
35
  "test": ":"
36
36
  },
37
37
  "dependencies": {
38
- "@jay-framework/component": "^0.20.0",
39
- "@jay-framework/fullstack-component": "^0.20.0",
40
- "@jay-framework/reactive": "^0.20.0",
41
- "@jay-framework/runtime": "^0.20.0",
42
- "@jay-framework/secure": "^0.20.0",
43
- "@jay-framework/stack-client-runtime": "^0.20.0",
44
- "@jay-framework/stack-server-runtime": "^0.20.0",
45
- "@jay-framework/wix-cart": "^0.20.0",
46
- "@jay-framework/wix-server-client": "^0.20.0",
47
- "@jay-framework/wix-utils": "^0.20.0",
48
- "@wix/sdk": "^1.21.5",
49
- "@wix/sdk-runtime": "^1.0.11",
50
- "@wix/stores": "^1.0.742"
38
+ "@jay-framework/component": "^0.22.0",
39
+ "@jay-framework/fullstack-component": "^0.22.0",
40
+ "@jay-framework/reactive": "^0.22.0",
41
+ "@jay-framework/runtime": "^0.22.0",
42
+ "@jay-framework/secure": "^0.22.0",
43
+ "@jay-framework/stack-client-runtime": "^0.22.0",
44
+ "@jay-framework/stack-server-runtime": "^0.22.0",
45
+ "@jay-framework/wix-cart": "^0.22.0",
46
+ "@jay-framework/wix-server-client": "^0.22.0",
47
+ "@jay-framework/wix-utils": "^0.22.0",
48
+ "@wix/sdk": "^1.21.5"
51
49
  },
52
50
  "devDependencies": {
53
51
  "@babel/core": "^7.23.7",
54
52
  "@babel/preset-env": "^7.23.8",
55
53
  "@babel/preset-typescript": "^7.23.3",
56
- "@jay-framework/compiler-jay-stack": "^0.20.0",
57
- "@jay-framework/jay-cli": "^0.20.0",
58
- "@jay-framework/vite-plugin": "^0.20.0",
54
+ "@jay-framework/compiler-jay-stack": "^0.22.0",
55
+ "@jay-framework/jay-cli": "^0.22.0",
56
+ "@jay-framework/vite-plugin": "^0.22.0",
59
57
  "nodemon": "^3.0.3",
60
58
  "rimraf": "^5.0.5",
61
59
  "tslib": "^2.6.2",
package/plugin.yaml CHANGED
@@ -41,7 +41,6 @@ contexts:
41
41
  marker: WIX_STORES_V1_CONTEXT
42
42
  description: Client-side V1 stores access with collection pagination and delegated cart operations
43
43
 
44
- setup:
45
- handler: setupWixStoresV1
44
+ setup: setupWixStoresV1
46
45
 
47
46
  # Plugin initialization uses makeJayInit pattern in lib/init.ts