@decocms/apps-shopify 8.1.0-next.0 → 8.1.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +36 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +82 -0
- package/dist/client.js.map +1 -0
- package/dist/graphqlOperationName.d.ts +36 -0
- package/dist/graphqlOperationName.d.ts.map +1 -0
- package/dist/graphqlOperationName.js +59 -0
- package/dist/graphqlOperationName.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/package.json +16 -29
- package/src/client.ts +113 -47
- package/src/index.ts +9 -51
- package/src/actions/cart/addItems.ts +0 -36
- package/src/actions/cart/updateCoupons.ts +0 -31
- package/src/actions/cart/updateItems.ts +0 -31
- package/src/actions/user/signIn.ts +0 -40
- package/src/actions/user/signUp.ts +0 -31
- package/src/init.ts +0 -39
- package/src/loaders/ProductDetailsPage.ts +0 -31
- package/src/loaders/ProductList.ts +0 -102
- package/src/loaders/ProductListingPage.ts +0 -172
- package/src/loaders/RelatedProducts.ts +0 -42
- package/src/loaders/cart.ts +0 -74
- package/src/loaders/shop.ts +0 -35
- package/src/loaders/user.ts +0 -39
- package/src/manifest.gen.ts +0 -39
- package/src/mod.ts +0 -67
- package/src/registry.ts +0 -9
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -84
- package/src/utils/__tests__/operationRouter.test.ts +0 -56
- package/src/utils/__tests__/transform.test.ts +0 -216
- package/src/utils/admin/admin.ts +0 -57
- package/src/utils/admin/queries.ts +0 -29
- package/src/utils/cart.ts +0 -32
- package/src/utils/cookies.ts +0 -82
- package/src/utils/enums.ts +0 -438
- package/src/utils/graphql.ts +0 -70
- package/src/utils/graphqlOperationName.ts +0 -2
- package/src/utils/instrumentedFetch.ts +0 -56
- package/src/utils/operationRouter.ts +0 -70
- package/src/utils/storefront/queries.ts +0 -525
- package/src/utils/storefront/storefront.graphql.gen.ts +0 -129
- package/src/utils/transform.ts +0 -430
- package/src/utils/types.ts +0 -191
- package/src/utils/user.ts +0 -23
- package/src/utils/utils.ts +0 -159
- package/src/v8/client.test.ts +0 -136
- package/src/v8/client.ts +0 -125
- package/src/v8/graphqlOperationName.test.ts +0 -80
- package/tsconfig.json +0 -7
- /package/src/{v8/graphqlOperationName.ts → graphqlOperationName.ts} +0 -0
package/src/mod.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shopify app module — standard autoconfig contract.
|
|
3
|
-
*
|
|
4
|
-
* Exports `configure` following the AppModContract pattern.
|
|
5
|
-
* The framework's `autoconfigApps()` calls these generically.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* import * as shopifyApp from "@decocms/apps/shopify/mod";
|
|
10
|
-
*
|
|
11
|
-
* const app = await shopifyApp.configure(blocks["deco-shopify"], resolveSecret);
|
|
12
|
-
* if (app) {
|
|
13
|
-
* // app.manifest, app.state are available
|
|
14
|
-
* }
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { AppDefinition, ResolveSecretFn } from "@decocms/apps-commerce/app-types";
|
|
19
|
-
import { configureShopify, type ShopifyConfig } from "./client";
|
|
20
|
-
import manifest from "./manifest.gen";
|
|
21
|
-
|
|
22
|
-
// -------------------------------------------------------------------------
|
|
23
|
-
// State
|
|
24
|
-
// -------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
export interface ShopifyState {
|
|
27
|
-
config: ShopifyConfig;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// -------------------------------------------------------------------------
|
|
31
|
-
// Configure
|
|
32
|
-
// -------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Configure the Shopify app from CMS block data.
|
|
36
|
-
* Returns an AppDefinition or null if required fields are missing.
|
|
37
|
-
*/
|
|
38
|
-
export async function configure(
|
|
39
|
-
block: Record<string, unknown>,
|
|
40
|
-
resolveSecret: ResolveSecretFn,
|
|
41
|
-
): Promise<AppDefinition<ShopifyState> | null> {
|
|
42
|
-
if (!block?.storeName) return null;
|
|
43
|
-
|
|
44
|
-
const storefrontAccessToken =
|
|
45
|
-
(await resolveSecret(block.storefrontAccessToken, "SHOPIFY_STOREFRONT_TOKEN")) ??
|
|
46
|
-
(typeof block.storefrontAccessToken === "string" ? block.storefrontAccessToken : null);
|
|
47
|
-
|
|
48
|
-
if (!storefrontAccessToken) return null;
|
|
49
|
-
|
|
50
|
-
const config: ShopifyConfig = {
|
|
51
|
-
storeName: block.storeName as string,
|
|
52
|
-
storefrontAccessToken,
|
|
53
|
-
publicUrl: block.publicUrl as string | undefined,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Bridge: maintain global singleton for backward compat
|
|
57
|
-
configureShopify(config);
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
name: "shopify",
|
|
61
|
-
manifest,
|
|
62
|
-
state: { config },
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Placeholder preview for CMS editor — evolves when admin supports it. */
|
|
67
|
-
export const preview = undefined;
|
package/src/registry.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { AppRegistryEntry } from "@decocms/apps-commerce/registry";
|
|
2
|
-
|
|
3
|
-
export const SHOPIFY_REGISTRY_ENTRY: AppRegistryEntry = {
|
|
4
|
-
blockKey: "deco-shopify",
|
|
5
|
-
module: () => import("./mod"),
|
|
6
|
-
displayName: "Shopify",
|
|
7
|
-
category: "commerce",
|
|
8
|
-
description: "Shopify Storefront API commerce integration",
|
|
9
|
-
};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Smoke tests for the pre-wired Shopify fetch factory. Same wiring
|
|
3
|
-
* assertions as `vtex/utils/__tests__/instrumentedFetch.test.ts`.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { configureMeter, type MeterAdapter } from "@decocms/blocks/sdk/observability";
|
|
7
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
-
import { createShopifyFetch } from "../instrumentedFetch";
|
|
9
|
-
|
|
10
|
-
type Labels = Record<string, string | number | boolean>;
|
|
11
|
-
|
|
12
|
-
function captureHistogram(): {
|
|
13
|
-
calls: { name: string; value: number; attrs: Labels }[];
|
|
14
|
-
meter: MeterAdapter;
|
|
15
|
-
} {
|
|
16
|
-
const calls: { name: string; value: number; attrs: Labels }[] = [];
|
|
17
|
-
const meter: MeterAdapter = {
|
|
18
|
-
counterInc: vi.fn(),
|
|
19
|
-
gaugeSet: vi.fn(),
|
|
20
|
-
histogramRecord: (name, value, attrs) => {
|
|
21
|
-
calls.push({ name, value, attrs: attrs ?? {} });
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
return { calls, meter };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe("createShopifyFetch", () => {
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
vi.restoreAllMocks();
|
|
30
|
-
configureMeter({
|
|
31
|
-
counterInc: () => {},
|
|
32
|
-
gaugeSet: () => {},
|
|
33
|
-
histogramRecord: () => {},
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("emits http.client.request.duration with provider=shopify on success", async () => {
|
|
38
|
-
const { calls, meter } = captureHistogram();
|
|
39
|
-
configureMeter(meter);
|
|
40
|
-
|
|
41
|
-
const baseFetch = vi.fn(async () => new Response("{}", { status: 200 }));
|
|
42
|
-
const fetchFn = createShopifyFetch({ baseFetch: baseFetch as typeof fetch });
|
|
43
|
-
|
|
44
|
-
await fetchFn("https://store.myshopify.com/api/2025-04/graphql.json", { method: "POST" });
|
|
45
|
-
|
|
46
|
-
expect(calls).toHaveLength(1);
|
|
47
|
-
expect(calls[0].name).toBe("http.client.request.duration");
|
|
48
|
-
expect(calls[0].attrs).toMatchObject({
|
|
49
|
-
provider: "shopify",
|
|
50
|
-
operation: "storefront.graphql",
|
|
51
|
-
status_class: "2xx",
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("honors init.operation (used by the GraphQL client to stamp <OperationName>)", async () => {
|
|
56
|
-
const { calls, meter } = captureHistogram();
|
|
57
|
-
configureMeter(meter);
|
|
58
|
-
|
|
59
|
-
const baseFetch = vi.fn(async () => new Response("{}", { status: 200 }));
|
|
60
|
-
const fetchFn = createShopifyFetch({ baseFetch: baseFetch as typeof fetch });
|
|
61
|
-
|
|
62
|
-
await fetchFn("https://store.myshopify.com/api/2025-04/graphql.json", {
|
|
63
|
-
method: "POST",
|
|
64
|
-
operation: "ProductBySlug",
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(calls[0].attrs.operation).toBe("ProductBySlug");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("skips histogram emission when disableHistogram is true", async () => {
|
|
71
|
-
const { calls, meter } = captureHistogram();
|
|
72
|
-
configureMeter(meter);
|
|
73
|
-
|
|
74
|
-
const baseFetch = vi.fn(async () => new Response("{}", { status: 200 }));
|
|
75
|
-
const fetchFn = createShopifyFetch({
|
|
76
|
-
baseFetch: baseFetch as typeof fetch,
|
|
77
|
-
disableHistogram: true,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
await fetchFn("https://store.myshopify.com/api/2025-04/graphql.json", { method: "POST" });
|
|
81
|
-
|
|
82
|
-
expect(calls).toHaveLength(0);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { shopifyOperationRouter } from "../operationRouter";
|
|
3
|
-
|
|
4
|
-
describe("shopifyOperationRouter", () => {
|
|
5
|
-
const store = "https://acme.myshopify.com";
|
|
6
|
-
|
|
7
|
-
it("recognizes the storefront GraphQL endpoint", () => {
|
|
8
|
-
expect(shopifyOperationRouter(`${store}/api/2025-04/graphql.json`, "POST")).toBe(
|
|
9
|
-
"storefront.graphql",
|
|
10
|
-
);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("recognizes the admin GraphQL endpoint", () => {
|
|
14
|
-
expect(shopifyOperationRouter(`${store}/admin/api/2025-04/graphql.json`, "POST")).toBe(
|
|
15
|
-
"admin.graphql",
|
|
16
|
-
);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("maps admin REST product / order / customer / inventory endpoints", () => {
|
|
20
|
-
expect(shopifyOperationRouter(`${store}/admin/api/2025-04/products.json`, "GET")).toBe(
|
|
21
|
-
"admin.products",
|
|
22
|
-
);
|
|
23
|
-
expect(shopifyOperationRouter(`${store}/admin/api/2025-04/orders/123.json`, "GET")).toBe(
|
|
24
|
-
"admin.orders",
|
|
25
|
-
);
|
|
26
|
-
expect(shopifyOperationRouter(`${store}/admin/api/2025-04/customers.json`, "GET")).toBe(
|
|
27
|
-
"admin.customers",
|
|
28
|
-
);
|
|
29
|
-
expect(shopifyOperationRouter(`${store}/admin/api/2025-04/inventory_levels.json`, "GET")).toBe(
|
|
30
|
-
"admin.inventory",
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("maps storefront checkout + cart endpoints", () => {
|
|
35
|
-
expect(shopifyOperationRouter(`${store}/api/2025-04/checkouts/abc.json`, "POST")).toBe(
|
|
36
|
-
"storefront.checkout",
|
|
37
|
-
);
|
|
38
|
-
expect(shopifyOperationRouter(`${store}/cart.js`, "GET")).toBe("storefront.cart");
|
|
39
|
-
expect(shopifyOperationRouter(`${store}/cart/add.js`, "POST")).toBe("storefront.cart");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("returns undefined for unrecognized paths", () => {
|
|
43
|
-
expect(shopifyOperationRouter(`${store}/random/path`, "GET")).toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("does not throw on unparseable URLs", () => {
|
|
47
|
-
expect(shopifyOperationRouter("not-a-url", "GET")).toBeUndefined();
|
|
48
|
-
expect(shopifyOperationRouter("/api/2025-04/graphql.json", "POST")).toBe("storefront.graphql");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("is case-insensitive on method (no behavioral impact today; future-proofing)", () => {
|
|
52
|
-
expect(shopifyOperationRouter(`${store}/api/2025-04/graphql.json`, "post")).toBe(
|
|
53
|
-
"storefront.graphql",
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { ProductShopify, SkuShopify } from "../transform";
|
|
3
|
-
import {
|
|
4
|
-
toBreadcrumbItem,
|
|
5
|
-
toBreadcrumbList,
|
|
6
|
-
toFilter,
|
|
7
|
-
toProduct,
|
|
8
|
-
toProductPage,
|
|
9
|
-
} from "../transform";
|
|
10
|
-
|
|
11
|
-
const makeSku = (overrides?: Partial<SkuShopify>): SkuShopify => ({
|
|
12
|
-
id: "gid://shopify/ProductVariant/12345",
|
|
13
|
-
title: "Default / Small",
|
|
14
|
-
availableForSale: true,
|
|
15
|
-
quantityAvailable: 10,
|
|
16
|
-
barcode: "123456789",
|
|
17
|
-
sku: "SKU-001",
|
|
18
|
-
image: { url: "https://cdn.shopify.com/img.jpg", altText: "Product image" },
|
|
19
|
-
price: { amount: "99.90", currencyCode: "BRL" },
|
|
20
|
-
compareAtPrice: { amount: "129.90", currencyCode: "BRL" },
|
|
21
|
-
selectedOptions: [{ name: "Size", value: "Small" }],
|
|
22
|
-
...overrides,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const makeProduct = (overrides?: Partial<ProductShopify>): ProductShopify => ({
|
|
26
|
-
id: "gid://shopify/Product/1",
|
|
27
|
-
handle: "test-product",
|
|
28
|
-
title: "Test Product",
|
|
29
|
-
description: "A test product",
|
|
30
|
-
descriptionHtml: "<p>A test product</p>",
|
|
31
|
-
createdAt: "2024-01-01",
|
|
32
|
-
tags: ["sale", "new"],
|
|
33
|
-
vendor: "TestBrand",
|
|
34
|
-
productType: "Clothing",
|
|
35
|
-
seo: { title: "Test Product SEO", description: "SEO description" },
|
|
36
|
-
images: { nodes: [{ url: "https://cdn.shopify.com/img.jpg", altText: "Product" }] },
|
|
37
|
-
media: { nodes: [] },
|
|
38
|
-
variants: { nodes: [makeSku()] },
|
|
39
|
-
collections: {
|
|
40
|
-
nodes: [{ handle: "clothing", title: "Clothing", id: "col1" }],
|
|
41
|
-
},
|
|
42
|
-
...overrides,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("toBreadcrumbItem", () => {
|
|
46
|
-
it("creates breadcrumb item", () => {
|
|
47
|
-
const result = toBreadcrumbItem({ name: "Home", position: 1, item: "/" });
|
|
48
|
-
expect(result).toEqual({
|
|
49
|
-
"@type": "ListItem",
|
|
50
|
-
name: "Home",
|
|
51
|
-
position: 1,
|
|
52
|
-
item: "/",
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("decodes URI-encoded names", () => {
|
|
57
|
-
const result = toBreadcrumbItem({ name: "Cal%C3%A7ados", position: 1, item: "/calcados" });
|
|
58
|
-
expect(result.name).toBe("Calçados");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("toBreadcrumbList", () => {
|
|
63
|
-
it("includes collection in breadcrumb when present", () => {
|
|
64
|
-
const product = makeProduct();
|
|
65
|
-
const sku = makeSku();
|
|
66
|
-
const result = toBreadcrumbList(product, sku);
|
|
67
|
-
expect(result.itemListElement).toHaveLength(2);
|
|
68
|
-
expect(result.itemListElement[0].name).toBe("Clothing");
|
|
69
|
-
expect(result.itemListElement[1].name).toBe("Test Product");
|
|
70
|
-
expect(result.numberOfItems).toBe(2);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("omits collection when not present", () => {
|
|
74
|
-
const product = makeProduct({ collections: undefined });
|
|
75
|
-
const sku = makeSku();
|
|
76
|
-
const result = toBreadcrumbList(product, sku);
|
|
77
|
-
expect(result.itemListElement).toHaveLength(1);
|
|
78
|
-
expect(result.itemListElement[0].name).toBe("Test Product");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("toProduct", () => {
|
|
83
|
-
it("transforms Shopify product to schema.org Product", () => {
|
|
84
|
-
const product = makeProduct();
|
|
85
|
-
const sku = makeSku();
|
|
86
|
-
const url = new URL("https://example.com/products/test-product-12345");
|
|
87
|
-
const result = toProduct(product, sku, url);
|
|
88
|
-
|
|
89
|
-
expect(result["@type"]).toBe("Product");
|
|
90
|
-
expect(result.productID).toBe("gid://shopify/ProductVariant/12345");
|
|
91
|
-
expect(result.name).toBe("Default / Small");
|
|
92
|
-
expect(result.description).toBe("A test product");
|
|
93
|
-
expect(result.brand).toEqual({ "@type": "Brand", name: "TestBrand" });
|
|
94
|
-
expect(result.offers?.priceCurrency).toBe("BRL");
|
|
95
|
-
expect(result.offers?.lowPrice).toBe(99.9);
|
|
96
|
-
expect(result.offers?.highPrice).toBe(129.9);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("sets availability based on availableForSale", () => {
|
|
100
|
-
const product = makeProduct();
|
|
101
|
-
const sku = makeSku({ availableForSale: false });
|
|
102
|
-
const url = new URL("https://example.com");
|
|
103
|
-
const result = toProduct(product, sku, url);
|
|
104
|
-
expect(result.offers?.offers[0]?.availability).toBe("https://schema.org/OutOfStock");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("uses sale price as high price when no compare at price", () => {
|
|
108
|
-
const product = makeProduct();
|
|
109
|
-
const sku = makeSku({ compareAtPrice: null });
|
|
110
|
-
const url = new URL("https://example.com");
|
|
111
|
-
const result = toProduct(product, sku, url);
|
|
112
|
-
expect(result.offers?.highPrice).toBe(99.9);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("includes variants at level 0", () => {
|
|
116
|
-
const product = makeProduct();
|
|
117
|
-
const sku = makeSku();
|
|
118
|
-
const url = new URL("https://example.com");
|
|
119
|
-
const result = toProduct(product, sku, url, 0);
|
|
120
|
-
expect(result.isVariantOf?.hasVariant).toHaveLength(1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("does not include variants at level 1", () => {
|
|
124
|
-
const product = makeProduct();
|
|
125
|
-
const sku = makeSku();
|
|
126
|
-
const url = new URL("https://example.com");
|
|
127
|
-
const result = toProduct(product, sku, url, 1);
|
|
128
|
-
expect(result.isVariantOf?.hasVariant).toEqual([]);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("toProductPage", () => {
|
|
133
|
-
it("creates a full ProductDetailsPage", () => {
|
|
134
|
-
const product = makeProduct();
|
|
135
|
-
const url = new URL("https://example.com/products/test-product-12345");
|
|
136
|
-
const result = toProductPage(product, url);
|
|
137
|
-
|
|
138
|
-
expect(result["@type"]).toBe("ProductDetailsPage");
|
|
139
|
-
expect(result.breadcrumbList["@type"]).toBe("BreadcrumbList");
|
|
140
|
-
expect(result.product["@type"]).toBe("Product");
|
|
141
|
-
expect(result.seo?.title).toBe("Test Product SEO");
|
|
142
|
-
expect(result.seo?.description).toBe("SEO description");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("falls back to product title/description for SEO", () => {
|
|
146
|
-
const product = makeProduct({ seo: { title: null, description: null } });
|
|
147
|
-
const url = new URL("https://example.com");
|
|
148
|
-
const result = toProductPage(product, url);
|
|
149
|
-
expect(result.seo?.title).toBe("Test Product");
|
|
150
|
-
expect(result.seo?.description).toBe("A test product");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("selects specified variant by ID", () => {
|
|
154
|
-
const sku1 = makeSku({ id: "gid://shopify/ProductVariant/111", title: "Red" });
|
|
155
|
-
const sku2 = makeSku({ id: "gid://shopify/ProductVariant/222", title: "Blue" });
|
|
156
|
-
const product = makeProduct({ variants: { nodes: [sku1, sku2] } });
|
|
157
|
-
const url = new URL("https://example.com");
|
|
158
|
-
const result = toProductPage(product, url, 222);
|
|
159
|
-
expect(result.product.name).toBe("Blue");
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe("toFilter", () => {
|
|
164
|
-
it("creates toggle filter", () => {
|
|
165
|
-
const filter = {
|
|
166
|
-
id: "filter.v.color",
|
|
167
|
-
label: "Color",
|
|
168
|
-
type: "LIST",
|
|
169
|
-
values: [
|
|
170
|
-
{ id: "1", label: "Red", count: 5, input: '{"productVendor":"Red"}' },
|
|
171
|
-
{ id: "2", label: "Blue", count: 3, input: '{"productVendor":"Blue"}' },
|
|
172
|
-
],
|
|
173
|
-
};
|
|
174
|
-
const url = new URL("https://example.com/collections/all");
|
|
175
|
-
const result = toFilter(filter, url);
|
|
176
|
-
|
|
177
|
-
expect(result["@type"]).toBe("FilterToggle");
|
|
178
|
-
expect(result.label).toBe("Color");
|
|
179
|
-
if (result["@type"] === "FilterToggle") {
|
|
180
|
-
expect(result.values).toHaveLength(2);
|
|
181
|
-
expect(result.values[0].label).toBe("Red");
|
|
182
|
-
expect(result.values[0].selected).toBe(false);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("creates range filter for PRICE_RANGE type", () => {
|
|
187
|
-
const filter = {
|
|
188
|
-
id: "filter.v.price",
|
|
189
|
-
label: "Price",
|
|
190
|
-
type: "PRICE_RANGE",
|
|
191
|
-
values: [{ id: "1", label: "0-100", count: 10, input: '{"min":0,"max":100}' }],
|
|
192
|
-
};
|
|
193
|
-
const url = new URL("https://example.com");
|
|
194
|
-
const result = toFilter(filter, url);
|
|
195
|
-
|
|
196
|
-
expect(result["@type"]).toBe("FilterRange");
|
|
197
|
-
if (result["@type"] === "FilterRange") {
|
|
198
|
-
expect(result.values.min).toBe(0);
|
|
199
|
-
expect(result.values.max).toBe(100);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("marks filter as selected when URL contains it", () => {
|
|
204
|
-
const filter = {
|
|
205
|
-
id: "filter.v.vendor",
|
|
206
|
-
label: "Brand",
|
|
207
|
-
type: "LIST",
|
|
208
|
-
values: [{ id: "1", label: "Nike", count: 5, input: '{"productVendor":"Nike"}' }],
|
|
209
|
-
};
|
|
210
|
-
const url = new URL("https://example.com?filter.v.vendor=Nike");
|
|
211
|
-
const result = toFilter(filter, url);
|
|
212
|
-
if (result["@type"] === "FilterToggle") {
|
|
213
|
-
expect(result.values[0].selected).toBe(true);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
});
|
package/src/utils/admin/admin.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// deno-fmt-ignore-file
|
|
2
|
-
// deno-lint-ignore-file no-explicit-any ban-types ban-unused-ignore
|
|
3
|
-
// biome-ignore-all lint/suspicious/noExplicitAny: generated GraphQL scalar types
|
|
4
|
-
|
|
5
|
-
export type Maybe<T> = T | null;
|
|
6
|
-
|
|
7
|
-
export type Scalars = {
|
|
8
|
-
ID: { input: string; output: string };
|
|
9
|
-
String: { input: string; output: string };
|
|
10
|
-
Boolean: { input: boolean; output: boolean };
|
|
11
|
-
Int: { input: number; output: number };
|
|
12
|
-
Float: { input: number; output: number };
|
|
13
|
-
ARN: { input: any; output: any };
|
|
14
|
-
Date: { input: any; output: any };
|
|
15
|
-
DateTime: { input: any; output: any };
|
|
16
|
-
Decimal: { input: any; output: any };
|
|
17
|
-
FormattedString: { input: any; output: any };
|
|
18
|
-
HTML: { input: any; output: any };
|
|
19
|
-
JSON: { input: any; output: any };
|
|
20
|
-
Money: { input: any; output: any };
|
|
21
|
-
StorefrontID: { input: any; output: any };
|
|
22
|
-
URL: { input: any; output: any };
|
|
23
|
-
UnsignedInt64: { input: any; output: any };
|
|
24
|
-
UtcOffset: { input: any; output: any };
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** Return type for `draftOrderCalculate` mutation. */
|
|
28
|
-
export type DraftOrderCalculatePayload = {
|
|
29
|
-
/** The calculated properties for a draft order. */
|
|
30
|
-
calculatedDraftOrder?: Maybe<CalculatedDraftOrder>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export interface CalculatedDraftOrder {
|
|
34
|
-
/** The available shipping rates for the draft order. Requires a customer with a valid shipping address and at least one line item. */
|
|
35
|
-
availableShippingRates: Array<ShippingRate>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type ShippingRate = {
|
|
39
|
-
/** Human-readable unique identifier for this shipping rate. */
|
|
40
|
-
handle: Scalars["String"]["output"];
|
|
41
|
-
/** The cost associated with the shipping rate. */
|
|
42
|
-
price: MoneyV2;
|
|
43
|
-
/** The name of the shipping rate. */
|
|
44
|
-
title: Scalars["String"]["output"];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type MoneyV2 = {
|
|
48
|
-
/** Decimal money amount. */
|
|
49
|
-
amount: Scalars["Decimal"]["output"];
|
|
50
|
-
/** Currency of the money. */
|
|
51
|
-
currencyCode: CurrencyCode;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export enum CurrencyCode {
|
|
55
|
-
CAD = "CAD",
|
|
56
|
-
Usd = "USD",
|
|
57
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { gql } from "../graphql";
|
|
2
|
-
|
|
3
|
-
// Fixme: This is to avoid typescript generation errors
|
|
4
|
-
// because it does not accept generating an empty schema
|
|
5
|
-
// TODO: Remove this once you add any other query
|
|
6
|
-
export const Noop = {
|
|
7
|
-
query: gql`query Noop { app(id: "") { description } }`,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const draftOrderCalculate = {
|
|
11
|
-
query: gql` mutation draftOrderCalculate($input: DraftOrderInput!) {
|
|
12
|
-
calculatedDraftOrder: draftOrderCalculate(input: $input) {
|
|
13
|
-
calculatedDraftOrder {
|
|
14
|
-
availableShippingRates {
|
|
15
|
-
title
|
|
16
|
-
handle
|
|
17
|
-
price {
|
|
18
|
-
amount
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
userErrors {
|
|
23
|
-
field
|
|
24
|
-
message
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
`,
|
|
29
|
-
};
|
package/src/utils/cart.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { getCookies, setCookie } from "./cookies";
|
|
2
|
-
|
|
3
|
-
const CART_COOKIE = "cart";
|
|
4
|
-
const SHOPIFY_PREFIX = "gid://shopify/Cart/";
|
|
5
|
-
|
|
6
|
-
const ONE_WEEK_MS = 7 * 24 * 3600 * 1_000;
|
|
7
|
-
|
|
8
|
-
export const getCartCookie = (headers: Headers): string | null => {
|
|
9
|
-
const cookies = getCookies(headers);
|
|
10
|
-
|
|
11
|
-
if (!cookies[CART_COOKIE]) {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
return decodeURIComponent(`${SHOPIFY_PREFIX}${cookies[CART_COOKIE]}`);
|
|
17
|
-
} catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const setCartCookie = (headers: Headers, cartId: string) => {
|
|
23
|
-
setCookie(headers, {
|
|
24
|
-
name: CART_COOKIE,
|
|
25
|
-
value: cartId.replace(SHOPIFY_PREFIX, ""),
|
|
26
|
-
path: "/",
|
|
27
|
-
expires: new Date(Date.now() + ONE_WEEK_MS),
|
|
28
|
-
httpOnly: true,
|
|
29
|
-
secure: true,
|
|
30
|
-
sameSite: "Lax",
|
|
31
|
-
});
|
|
32
|
-
};
|
package/src/utils/cookies.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Node.js-compatible cookie helpers.
|
|
3
|
-
* Replaces Deno's std/http/cookie.ts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export function getCookies(headers: Headers): Record<string, string> {
|
|
7
|
-
const cookieHeader = headers.get("cookie") || "";
|
|
8
|
-
const cookies: Record<string, string> = {};
|
|
9
|
-
for (const pair of cookieHeader.split(";")) {
|
|
10
|
-
const [key, ...rest] = pair.trim().split("=");
|
|
11
|
-
if (key) {
|
|
12
|
-
cookies[key.trim()] = decodeURIComponent(rest.join("=").trim());
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return cookies;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function setCookie(
|
|
19
|
-
headers: Headers,
|
|
20
|
-
options: {
|
|
21
|
-
name: string;
|
|
22
|
-
value: string;
|
|
23
|
-
path?: string;
|
|
24
|
-
expires?: Date;
|
|
25
|
-
maxAge?: number;
|
|
26
|
-
httpOnly?: boolean;
|
|
27
|
-
secure?: boolean;
|
|
28
|
-
sameSite?: "Strict" | "Lax" | "None";
|
|
29
|
-
},
|
|
30
|
-
) {
|
|
31
|
-
const parts = [`${options.name}=${encodeURIComponent(options.value)}`];
|
|
32
|
-
if (options.path) parts.push(`Path=${options.path}`);
|
|
33
|
-
if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
34
|
-
if (options.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);
|
|
35
|
-
if (options.httpOnly) parts.push("HttpOnly");
|
|
36
|
-
if (options.secure) parts.push("Secure");
|
|
37
|
-
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
|
|
38
|
-
|
|
39
|
-
headers.append("Set-Cookie", parts.join("; "));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface Cookie {
|
|
43
|
-
name: string;
|
|
44
|
-
value: string;
|
|
45
|
-
path?: string;
|
|
46
|
-
expires?: Date;
|
|
47
|
-
maxAge?: number;
|
|
48
|
-
httpOnly?: boolean;
|
|
49
|
-
secure?: boolean;
|
|
50
|
-
sameSite?: "Strict" | "Lax" | "None";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function getSetCookies(headers: Headers): Cookie[] {
|
|
54
|
-
const cookies: Cookie[] = [];
|
|
55
|
-
const setCookieHeaders = headers.getSetCookie?.() ?? [];
|
|
56
|
-
for (const header of setCookieHeaders) {
|
|
57
|
-
const parts = header.split(";").map((p) => p.trim());
|
|
58
|
-
const [nameValue, ...attrs] = parts;
|
|
59
|
-
const [name, ...rest] = nameValue.split("=");
|
|
60
|
-
const value = rest.join("=");
|
|
61
|
-
const cookie: Cookie = { name: name.trim(), value };
|
|
62
|
-
for (const attr of attrs) {
|
|
63
|
-
const [k, v] = attr.split("=");
|
|
64
|
-
const key = k.trim().toLowerCase();
|
|
65
|
-
if (key === "path") cookie.path = v?.trim();
|
|
66
|
-
if (key === "httponly") cookie.httpOnly = true;
|
|
67
|
-
if (key === "secure") cookie.secure = true;
|
|
68
|
-
if (key === "samesite") cookie.sameSite = v?.trim() as any;
|
|
69
|
-
if (key === "max-age") cookie.maxAge = Number(v?.trim());
|
|
70
|
-
}
|
|
71
|
-
cookies.push(cookie);
|
|
72
|
-
}
|
|
73
|
-
return cookies;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
|
|
77
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
78
|
-
return ((...args: any[]) => {
|
|
79
|
-
clearTimeout(timer);
|
|
80
|
-
timer = setTimeout(() => fn(...args), delay);
|
|
81
|
-
}) as T;
|
|
82
|
-
}
|