@stacksjs/commerce 0.75.37 → 0.75.40
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/imports/http.d.ts +36 -0
- package/dist/imports/identity.d.ts +22 -0
- package/dist/imports/index.d.ts +37 -0
- package/dist/imports/money.d.ts +40 -0
- package/dist/imports/shopify.d.ts +40 -0
- package/dist/imports/shopware.d.ts +80 -0
- package/dist/imports/text.d.ts +6 -0
- package/dist/imports/types.d.ts +79 -0
- package/dist/imports/woocommerce.d.ts +62 -0
- package/dist/imports/writer.d.ts +202 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +19 -19
- package/dist/money.d.ts +107 -0
- package/dist/money.js +2 -0
- package/dist/orders/export.d.ts +25 -1
- package/package.json +12 -13
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FetchLike } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A store URL as a base to append API paths to.
|
|
4
|
+
*
|
|
5
|
+
* Accepts a bare host (`shop.example.com`), a full URL, or a WordPress install
|
|
6
|
+
* in a subdirectory (`https://example.com/shop`), whose path must be kept for
|
|
7
|
+
* `/wp-json` to resolve. Query string and fragment are dropped.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeStoreUrl(input: string): string;
|
|
10
|
+
/** The lowercased host of a normalized store URL, used to scope identities. */
|
|
11
|
+
export declare function storeHost(storeUrl: string): string;
|
|
12
|
+
/** Fetch a URL and parse it as JSON (GET unless `request` says otherwise), or throw a `CatalogFetchError` naming it. */
|
|
13
|
+
export declare function fetchJson<T = unknown>(url: string, fetcher?: FetchLike, hint?: string, request?: JsonRequest): Promise<JsonResponse<T>>;
|
|
14
|
+
export declare interface JsonResponse<T> {
|
|
15
|
+
data: T
|
|
16
|
+
headers: Headers
|
|
17
|
+
}
|
|
18
|
+
/** A request other than a plain GET: Shopware's Store API takes criteria as a POST body. */
|
|
19
|
+
export declare interface JsonRequest {
|
|
20
|
+
method?: 'GET' | 'POST'
|
|
21
|
+
headers?: Record<string, string>
|
|
22
|
+
json?: unknown
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The importer's only network primitive.
|
|
26
|
+
*
|
|
27
|
+
* Every failure names the URL that failed. "fetch failed" on its own leaves
|
|
28
|
+
* the operator guessing which of forty pages broke, and whether it was the
|
|
29
|
+
* store, the network or the importer.
|
|
30
|
+
*/
|
|
31
|
+
export declare class CatalogFetchError extends Error {
|
|
32
|
+
public readonly url: string;
|
|
33
|
+
public readonly status?: number;
|
|
34
|
+
public readonly body?: string;
|
|
35
|
+
constructor(url: string, message: string, status?: number, body?: string);
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** RFC 9562 version 5 (SHA-1, name-based) UUID. */
|
|
2
|
+
export declare function uuidV5(name: string, namespace?: string): string;
|
|
3
|
+
/** The deterministic `uuid` for one imported record. */
|
|
4
|
+
export declare function catalogUuid(source: string, host: string, kind: CatalogRecordKind, externalId: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Stable row identities for imported records, without a schema change.
|
|
7
|
+
*
|
|
8
|
+
* Re-running an import must update what the last run wrote rather than add a
|
|
9
|
+
* second copy, so every imported row needs a key that survives between runs.
|
|
10
|
+
* `products` has no slug, SKU or external-id column to hold one, and names
|
|
11
|
+
* are not unique on any platform.
|
|
12
|
+
*
|
|
13
|
+
* Every commerce table already has a unique `uuid` column (the `useUuid`
|
|
14
|
+
* trait). A name-based UUID (RFC 9562 version 5) of
|
|
15
|
+
* `<source>:<store host>:<kind>:<external id>` is the same value on every run
|
|
16
|
+
* and unique per source record, so the existing column doubles as the
|
|
17
|
+
* external-id index. Rows created any other way keep their random v7 UUIDs and
|
|
18
|
+
* can never collide with these.
|
|
19
|
+
*/
|
|
20
|
+
/** Namespace for every UUID the catalog importer derives. Never change it. */
|
|
21
|
+
export declare const CATALOG_IMPORT_NAMESPACE: '5d0c8a3e-6f1b-4c2a-9e47-b18f3d2a6c90';
|
|
22
|
+
export type CatalogRecordKind = 'product' | 'variant';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CatalogAdapter } from './types';
|
|
2
|
+
export type {
|
|
3
|
+
CatalogAdapter,
|
|
4
|
+
CatalogCategory,
|
|
5
|
+
CatalogFetchOptions,
|
|
6
|
+
CatalogImage,
|
|
7
|
+
CatalogPage,
|
|
8
|
+
CatalogProduct,
|
|
9
|
+
CatalogSourceName,
|
|
10
|
+
CatalogVariant,
|
|
11
|
+
CatalogWarning,
|
|
12
|
+
FetchLike,
|
|
13
|
+
MappedProduct,
|
|
14
|
+
} from './types';
|
|
15
|
+
/** The adapter registered under `name`, case-insensitively. */
|
|
16
|
+
export declare function catalogAdapter(name: string): CatalogAdapter | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Catalog import from other commerce platforms (stacksjs/stacks#1319, #1320,
|
|
19
|
+
* #1321).
|
|
20
|
+
*
|
|
21
|
+
* Catalog only, from each platform's public storefront API: no credentials
|
|
22
|
+
* (Shopware's Store API wants its public sales channel access key), and
|
|
23
|
+
* therefore no customers or orders. Run it with
|
|
24
|
+
* `buddy commerce:import <store-url> --from shopify|woocommerce|shopware`.
|
|
25
|
+
*
|
|
26
|
+
* Adding a platform means writing a `CatalogAdapter` (a pure payload mapper
|
|
27
|
+
* plus a pager) and registering it in `catalogAdapters`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const catalogAdapters: Readonly<Record<string, CatalogAdapter>>;
|
|
30
|
+
export * from './http';
|
|
31
|
+
export * from './identity';
|
|
32
|
+
export * from './money';
|
|
33
|
+
export * from './shopify';
|
|
34
|
+
export * from './shopware';
|
|
35
|
+
export * from './text';
|
|
36
|
+
export * from './woocommerce';
|
|
37
|
+
export * from './writer';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { decimalToMinor, PriceFormatError } from '../money';
|
|
2
|
+
/**
|
|
3
|
+
* A JSON number as the plain decimal string it was written as.
|
|
4
|
+
*
|
|
5
|
+
* `String()` gives the shortest decimal that parses back to the same double,
|
|
6
|
+
* which is the literal the source serialized: `19.99` is `"19.99"`, not the
|
|
7
|
+
* `19.989999999999998` the double actually holds. Only the exponent form
|
|
8
|
+
* (`1e-7`, `1e+21`) needs expanding, and that is done on the digits, not by
|
|
9
|
+
* arithmetic.
|
|
10
|
+
*/
|
|
11
|
+
export declare function numberToDecimalString(value: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* A decimal amount that arrived as a JSON number (or a numeric string) as
|
|
14
|
+
* integer minor units, or null when it is missing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function numberToMinor(value: number | string | null | undefined, exponent?: number): number | null;
|
|
17
|
+
/**
|
|
18
|
+
* Money at the import boundary.
|
|
19
|
+
*
|
|
20
|
+
* Sources hand prices over in three shapes. Shopify sends decimal strings in
|
|
21
|
+
* the shop's currency (`"19.99"`); the WooCommerce Store API sends an integer
|
|
22
|
+
* string already in minor units, plus how many decimals the STORE is
|
|
23
|
+
* configured with (`currency_minor_unit`), which is not always the ISO number;
|
|
24
|
+
* the Shopware Store API sends JSON numbers (`19.99`, `0.1`, `1999.5`).
|
|
25
|
+
*
|
|
26
|
+
* Both string shapes are converted by the commerce-wide money module
|
|
27
|
+
* (`../money`), which the dashboard and the order export use as well, so an
|
|
28
|
+
* imported `1999` and a dashboard-entered `19.99` land as the same integer and
|
|
29
|
+
* read back the same way (stacksjs/stacks#2851). JSON numbers are first
|
|
30
|
+
* written out as the decimal string they were parsed from, then go the same
|
|
31
|
+
* way, so no amount is ever multiplied as a float.
|
|
32
|
+
*/
|
|
33
|
+
export {
|
|
34
|
+
currencyExponent,
|
|
35
|
+
decimalToMinor,
|
|
36
|
+
formatMinor,
|
|
37
|
+
minorToDecimal,
|
|
38
|
+
PriceFormatError,
|
|
39
|
+
rescaleMinor,
|
|
40
|
+
} from '../money';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CatalogAdapter, CatalogFetchOptions, MappedProduct } from './types';
|
|
2
|
+
declare function price(value: unknown, exponent: number, label: string, externalId: string, warnings: CatalogWarning[]): number | null;
|
|
3
|
+
/** One `products.json` entry as a normalized catalog product. Pure. */
|
|
4
|
+
export declare function mapShopifyProduct(raw: ShopifyProductPayload, context: ShopifyMapContext): MappedProduct;
|
|
5
|
+
/** The shop's currency from `/meta.json`, or null if the store will not say. */
|
|
6
|
+
export declare function detectShopifyCurrency(storeUrl: string, fetcher?: CatalogFetchOptions['fetch']): Promise<string | null>;
|
|
7
|
+
export declare const shopifyAdapter: CatalogAdapter;
|
|
8
|
+
export declare interface ShopifyVariantPayload {
|
|
9
|
+
id: number | string
|
|
10
|
+
title?: string | null
|
|
11
|
+
sku?: string | null
|
|
12
|
+
price?: string | number | null
|
|
13
|
+
compare_at_price?: string | number | null
|
|
14
|
+
available?: boolean | null
|
|
15
|
+
inventory_quantity?: number | null
|
|
16
|
+
option1?: string | null
|
|
17
|
+
option2?: string | null
|
|
18
|
+
option3?: string | null
|
|
19
|
+
}
|
|
20
|
+
export declare interface ShopifyImagePayload {
|
|
21
|
+
src?: string | null
|
|
22
|
+
position?: number | null
|
|
23
|
+
alt?: string | null
|
|
24
|
+
}
|
|
25
|
+
export declare interface ShopifyProductPayload {
|
|
26
|
+
id: number | string
|
|
27
|
+
title?: string | null
|
|
28
|
+
handle?: string | null
|
|
29
|
+
body_html?: string | null
|
|
30
|
+
vendor?: string | null
|
|
31
|
+
product_type?: string | null
|
|
32
|
+
tags?: string[] | string | null
|
|
33
|
+
variants?: ShopifyVariantPayload[] | null
|
|
34
|
+
images?: ShopifyImagePayload[] | null
|
|
35
|
+
options?: Array<{ name?: string | null, values?: string[] | null }> | null
|
|
36
|
+
}
|
|
37
|
+
export declare interface ShopifyMapContext {
|
|
38
|
+
storeUrl: string
|
|
39
|
+
currency: string | null
|
|
40
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CatalogAdapter, FetchLike, MappedProduct } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* `translated.<field>` (the value in the sales channel's language, with
|
|
4
|
+
* inheritance from the parent already applied), else the entity's own field.
|
|
5
|
+
* Shopware serializes an empty `translated` as `[]`, hence the object check.
|
|
6
|
+
*/
|
|
7
|
+
declare function translated(entity: object | null | undefined, field: string): string | null;
|
|
8
|
+
/** One Store API parent product, plus its children, as a catalog product. Pure. */
|
|
9
|
+
export declare function mapShopwareProduct(raw: ShopwareProductPayload, context: ShopwareMapContext): MappedProduct;
|
|
10
|
+
/**
|
|
11
|
+
* A store URL as the base the Store API hangs off. Accepts the storefront URL
|
|
12
|
+
* or the API endpoint itself (`https://shop.example.com/store-api`), which is
|
|
13
|
+
* what headless storefront configs list.
|
|
14
|
+
*/
|
|
15
|
+
export declare function shopwareBaseUrl(storeUrl: string): string;
|
|
16
|
+
/** Why `--access-key` is needed, before any request is made. */
|
|
17
|
+
export declare function missingAccessKeyMessage(): string;
|
|
18
|
+
/** The sales channel currency from `/store-api/context`, or null if it will not say. */
|
|
19
|
+
export declare function detectShopwareCurrency(base: string, accessKey: string, fetcher?: FetchLike): Promise<string | null>;
|
|
20
|
+
/** Criteria for one page of parent products (variants come separately). */
|
|
21
|
+
export declare function shopwareProductCriteria(page: number, limit: number): Record<string, unknown>;
|
|
22
|
+
/** Criteria for one page of the variants of `parentIds`. */
|
|
23
|
+
export declare function shopwareVariantCriteria(parentIds: string[], page: number): Record<string, unknown>;
|
|
24
|
+
/** Where an operator finds the key, for every message that asks for it. */
|
|
25
|
+
export declare const SHOPWARE_ACCESS_KEY_HELP: 'Find it in the Shopware Administration under Sales Channels: open the storefront or headless sales channel and copy the key from its "API access" card. A headless storefront also ships it in its page source.';
|
|
26
|
+
export declare const shopwareAdapter: CatalogAdapter;
|
|
27
|
+
export declare interface ShopwareNamedPayload {
|
|
28
|
+
id?: string | null
|
|
29
|
+
name?: string | null
|
|
30
|
+
translated?: { name?: string | null } | unknown[] | null
|
|
31
|
+
}
|
|
32
|
+
export declare interface ShopwarePropertyGroupPayload extends ShopwareNamedPayload {
|
|
33
|
+
position?: number | null
|
|
34
|
+
}
|
|
35
|
+
export declare interface ShopwareOptionPayload extends ShopwareNamedPayload {
|
|
36
|
+
groupId?: string | null
|
|
37
|
+
position?: number | null
|
|
38
|
+
group?: ShopwarePropertyGroupPayload | null
|
|
39
|
+
}
|
|
40
|
+
export declare interface ShopwareMediaPayload {
|
|
41
|
+
url?: string | null
|
|
42
|
+
alt?: string | null
|
|
43
|
+
title?: string | null
|
|
44
|
+
translated?: { alt?: string | null, title?: string | null } | unknown[] | null
|
|
45
|
+
}
|
|
46
|
+
export declare interface ShopwareProductMediaPayload {
|
|
47
|
+
position?: number | null
|
|
48
|
+
media?: ShopwareMediaPayload | null
|
|
49
|
+
}
|
|
50
|
+
export declare interface ShopwareCalculatedPricePayload {
|
|
51
|
+
unitPrice?: number | string | null
|
|
52
|
+
quantity?: number | null
|
|
53
|
+
listPrice?: { price?: number | string | null } | null
|
|
54
|
+
}
|
|
55
|
+
export declare interface ShopwareProductPayload {
|
|
56
|
+
id: string
|
|
57
|
+
parentId?: string | null
|
|
58
|
+
productNumber?: string | null
|
|
59
|
+
name?: string | null
|
|
60
|
+
description?: string | null
|
|
61
|
+
translated?: { name?: string | null, description?: string | null } | unknown[] | null
|
|
62
|
+
calculatedPrice?: ShopwareCalculatedPricePayload | null
|
|
63
|
+
stock?: number | null
|
|
64
|
+
availableStock?: number | null
|
|
65
|
+
available?: boolean | null
|
|
66
|
+
active?: boolean | null
|
|
67
|
+
childCount?: number | null
|
|
68
|
+
manufacturer?: ShopwareNamedPayload | null
|
|
69
|
+
seoCategory?: ShopwareNamedPayload | null
|
|
70
|
+
categories?: ShopwareNamedPayload[] | null
|
|
71
|
+
tags?: Array<{ name?: string | null }> | null
|
|
72
|
+
cover?: ShopwareProductMediaPayload | null
|
|
73
|
+
media?: ShopwareProductMediaPayload[] | null
|
|
74
|
+
options?: ShopwareOptionPayload[] | null
|
|
75
|
+
}
|
|
76
|
+
export declare interface ShopwareMapContext {
|
|
77
|
+
storeUrl: string
|
|
78
|
+
currency: string | null
|
|
79
|
+
children?: ShopwareProductPayload[]
|
|
80
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Decode HTML character references (named, decimal and hex) in one pass. */
|
|
2
|
+
export declare function decodeHtmlEntities(input: string): string;
|
|
3
|
+
/** A name for storage: entities decoded, whitespace collapsed, trimmed. */
|
|
4
|
+
export declare function cleanName(input: unknown): string;
|
|
5
|
+
/** A non-empty string, or null. */
|
|
6
|
+
export declare function optionalString(input: unknown): string | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare interface CatalogImage {
|
|
2
|
+
src: string
|
|
3
|
+
alt: string | null
|
|
4
|
+
}
|
|
5
|
+
export declare interface CatalogCategory {
|
|
6
|
+
name: string
|
|
7
|
+
slug: string
|
|
8
|
+
}
|
|
9
|
+
export declare interface CatalogVariant {
|
|
10
|
+
externalId: string
|
|
11
|
+
title: string
|
|
12
|
+
sku: string | null
|
|
13
|
+
priceMinor: number | null
|
|
14
|
+
compareAtMinor: number | null
|
|
15
|
+
available: boolean
|
|
16
|
+
inventory: number | null
|
|
17
|
+
optionValues: string[]
|
|
18
|
+
}
|
|
19
|
+
export declare interface CatalogProduct {
|
|
20
|
+
source: CatalogSourceName
|
|
21
|
+
externalId: string
|
|
22
|
+
name: string
|
|
23
|
+
handle: string
|
|
24
|
+
descriptionHtml: string | null
|
|
25
|
+
vendor: string | null
|
|
26
|
+
categories: CatalogCategory[]
|
|
27
|
+
tags: string[]
|
|
28
|
+
currency: string | null
|
|
29
|
+
images: CatalogImage[]
|
|
30
|
+
optionNames: string[]
|
|
31
|
+
variants: CatalogVariant[]
|
|
32
|
+
hasOptions: boolean
|
|
33
|
+
url: string | null
|
|
34
|
+
}
|
|
35
|
+
/** A non-fatal problem found while mapping or fetching. */
|
|
36
|
+
export declare interface CatalogWarning {
|
|
37
|
+
externalId?: string
|
|
38
|
+
message: string
|
|
39
|
+
}
|
|
40
|
+
/** A source payload mapped to a product, with what the mapping noticed. */
|
|
41
|
+
export declare interface MappedProduct {
|
|
42
|
+
product: CatalogProduct
|
|
43
|
+
warnings: CatalogWarning[]
|
|
44
|
+
}
|
|
45
|
+
/** One page of a source's catalog, already normalized. */
|
|
46
|
+
export declare interface CatalogPage {
|
|
47
|
+
url: string
|
|
48
|
+
products: CatalogProduct[]
|
|
49
|
+
warnings: CatalogWarning[]
|
|
50
|
+
}
|
|
51
|
+
export declare interface CatalogFetchOptions {
|
|
52
|
+
limit?: number
|
|
53
|
+
fetch?: FetchLike
|
|
54
|
+
currency?: string
|
|
55
|
+
accessKey?: string
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A source of catalog data. Implement this to add a platform: map its payload
|
|
59
|
+
* to `CatalogProduct` in a pure function, and page through it here.
|
|
60
|
+
*/
|
|
61
|
+
export declare interface CatalogAdapter {
|
|
62
|
+
name: CatalogSourceName
|
|
63
|
+
pages: (storeUrl: string, options?: CatalogFetchOptions) => AsyncIterable<CatalogPage>
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The normalized catalog every import source maps into.
|
|
67
|
+
*
|
|
68
|
+
* Each source (Shopify, WooCommerce, and whatever comes next) is an adapter
|
|
69
|
+
* that turns its own payloads into these records. The writer only ever sees
|
|
70
|
+
* this shape, so adding a source never touches the code that talks to the
|
|
71
|
+
* database, and the mapping is testable from recorded JSON with no I/O.
|
|
72
|
+
*
|
|
73
|
+
* Money is integer minor units of `currency` throughout. A source that hands
|
|
74
|
+
* out decimal strings converts at its own boundary (see `money.ts`).
|
|
75
|
+
*/
|
|
76
|
+
/** A source registered with the importer. */
|
|
77
|
+
export type CatalogSourceName = 'shopify' | 'woocommerce' | 'shopware' | (string & {});
|
|
78
|
+
/** `fetch`, narrowed to what the importer uses, so tests can inject one. */
|
|
79
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { slug } from '@stacksjs/strings';
|
|
2
|
+
import type { CatalogAdapter, MappedProduct } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Store API prices as minor units of the currency's ISO precision.
|
|
5
|
+
*
|
|
6
|
+
* `prices.price` is already an integer, but at the STORE's configured decimals
|
|
7
|
+
* (`currency_minor_unit`), which a merchant can set to anything. A sale price
|
|
8
|
+
* makes the regular price the compare-at price.
|
|
9
|
+
*/
|
|
10
|
+
export declare function wooPrices(prices: WooPricesPayload | null | undefined): Prices;
|
|
11
|
+
declare function terms(list: WooTermPayload[] | null | undefined): Array<{ name: string, slug: string }>;
|
|
12
|
+
/** One Store API product, plus its fetched variations, as a catalog product. Pure. */
|
|
13
|
+
export declare function mapWooProduct(raw: WooProductPayload, context?: WooMapContext): MappedProduct;
|
|
14
|
+
export declare const wooCommerceAdapter: CatalogAdapter;
|
|
15
|
+
export declare interface WooPricesPayload {
|
|
16
|
+
price?: string | null
|
|
17
|
+
regular_price?: string | null
|
|
18
|
+
sale_price?: string | null
|
|
19
|
+
currency_code?: string | null
|
|
20
|
+
currency_minor_unit?: number | string | null
|
|
21
|
+
}
|
|
22
|
+
export declare interface WooTermPayload {
|
|
23
|
+
id?: number
|
|
24
|
+
name?: string | null
|
|
25
|
+
slug?: string | null
|
|
26
|
+
}
|
|
27
|
+
export declare interface WooAttributePayload {
|
|
28
|
+
id?: number
|
|
29
|
+
name?: string | null
|
|
30
|
+
taxonomy?: string | null
|
|
31
|
+
has_variations?: boolean
|
|
32
|
+
terms?: WooTermPayload[] | null
|
|
33
|
+
}
|
|
34
|
+
export declare interface WooProductPayload {
|
|
35
|
+
id: number | string
|
|
36
|
+
name?: string | null
|
|
37
|
+
slug?: string | null
|
|
38
|
+
type?: string | null
|
|
39
|
+
parent?: number | null
|
|
40
|
+
permalink?: string | null
|
|
41
|
+
sku?: string | null
|
|
42
|
+
description?: string | null
|
|
43
|
+
short_description?: string | null
|
|
44
|
+
prices?: WooPricesPayload | null
|
|
45
|
+
images?: Array<{ src?: string | null, alt?: string | null }> | null
|
|
46
|
+
categories?: WooTermPayload[] | null
|
|
47
|
+
tags?: WooTermPayload[] | null
|
|
48
|
+
brands?: WooTermPayload[] | null
|
|
49
|
+
attributes?: WooAttributePayload[] | null
|
|
50
|
+
variations?: Array<{ id: number | string, attributes?: Array<{ name?: string | null, value?: string | null }> | null }> | null
|
|
51
|
+
is_in_stock?: boolean | null
|
|
52
|
+
is_purchasable?: boolean | null
|
|
53
|
+
low_stock_remaining?: number | null
|
|
54
|
+
}
|
|
55
|
+
export declare interface WooMapContext {
|
|
56
|
+
variations?: Map<string, WooProductPayload>
|
|
57
|
+
}
|
|
58
|
+
declare interface Prices {
|
|
59
|
+
currency: string | null
|
|
60
|
+
priceMinor: number | null
|
|
61
|
+
compareAtMinor: number | null
|
|
62
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { CatalogAdapter, CatalogFetchOptions, CatalogProduct, CatalogVariant, CatalogWarning } from './types';
|
|
2
|
+
/** The lowest price among a product's priced variants. */
|
|
3
|
+
export declare function lowestPrice(variants: CatalogVariant[]): number | null;
|
|
4
|
+
/** Total stock, or null unless every variant reports its count. */
|
|
5
|
+
export declare function totalInventory(variants: CatalogVariant[]): number | null;
|
|
6
|
+
/**
|
|
7
|
+
* The SKU each of a product's variants may write, keyed by variant id.
|
|
8
|
+
*
|
|
9
|
+
* `product_variants.sku` is unique per product, but no source enforces that:
|
|
10
|
+
* Shopify accepts the same SKU on every variant. The first variant to carry a
|
|
11
|
+
* SKU keeps it; a later duplicate, or a SKU too long for the column, is
|
|
12
|
+
* written as no SKU and reported, rather than failing the whole product.
|
|
13
|
+
*/
|
|
14
|
+
export declare function variantSkus(product: CatalogProduct): { skus: Map<string, string | null>, warnings: CatalogWarning[] };
|
|
15
|
+
/** The `products` row for a catalog product. Pure. */
|
|
16
|
+
export declare function productRow(product: CatalogProduct, host: string, refs: ProductRefs, now: string, mode: 'create' | 'update'): ProductRow;
|
|
17
|
+
/**
|
|
18
|
+
* The `product_variants` row for one variant of a catalog product. Pure.
|
|
19
|
+
*
|
|
20
|
+
* `sku` is the SKU this variant may claim (see `variantSkus`), which can be
|
|
21
|
+
* null even when the source reported one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function variantRow(product: CatalogProduct, variant: CatalogVariant, host: string, productId: number, now: string, mode: 'create' | 'update', sku?: string | null): VariantRow;
|
|
24
|
+
/**
|
|
25
|
+
* Fetch a store's catalog through `adapter` and upsert it via `repository`.
|
|
26
|
+
*
|
|
27
|
+
* Idempotent: products and variants are matched on their derived `uuid`,
|
|
28
|
+
* categories on slug (then name) and manufacturers on name, so a second run
|
|
29
|
+
* over an unchanged store creates nothing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function importCatalog(options: ImportCatalogOptions): Promise<ImportResult>;
|
|
32
|
+
/**
|
|
33
|
+
* A repository over the commerce tables, via the query builder.
|
|
34
|
+
*
|
|
35
|
+
* Inserts are read back by `uuid`, which works on every dialect: Postgres
|
|
36
|
+
* reports no insert id without `RETURNING`, and a row count is never an id.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createDatabaseRepository(connection?: any): CatalogRepository;
|
|
39
|
+
/**
|
|
40
|
+
* A repository that keeps rows in memory. Backs the importer's own tests and
|
|
41
|
+
* a `--dry-run` with no database to compare against.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createMemoryRepository(): CatalogRepository & { tables: Record<string, Array<Record<string, any>>> };
|
|
44
|
+
/**
|
|
45
|
+
* Reads through to `inner`, writes nothing. What `--dry-run` uses, so its
|
|
46
|
+
* report says "create" or "update" exactly as a real run would.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createReadOnlyRepository(inner: CatalogRepository): CatalogRepository;
|
|
49
|
+
/** Manufacturers require a country; the source never says which. */
|
|
50
|
+
export declare const UNKNOWN_COUNTRY: 'Unknown';
|
|
51
|
+
/** `product_variants.sku` is a 100-character column (see the model). */
|
|
52
|
+
export declare const SKU_MAX: 100;
|
|
53
|
+
/**
|
|
54
|
+
* Writing a normalized catalog into the built-in commerce tables.
|
|
55
|
+
*
|
|
56
|
+
* Where each field lands, given the columns those tables actually have:
|
|
57
|
+
*
|
|
58
|
+
* | Source | Column |
|
|
59
|
+
* |-----------------------------|------------------------------------------------|
|
|
60
|
+
* | product id + store host | `products.uuid` (UUIDv5, see `identity.ts`) |
|
|
61
|
+
* | title / name | `products.name` |
|
|
62
|
+
* | description (HTML) | `products.description`, unchanged |
|
|
63
|
+
* | lowest variant price | `products.price`, integer minor units |
|
|
64
|
+
* | first image URL | `products.image_url` (never downloaded) |
|
|
65
|
+
* | any variant available | `products.is_available` |
|
|
66
|
+
* | summed stock, when exposed | `products.inventory_count` |
|
|
67
|
+
* | first product type/category | `categories` by slug, then `category_id` |
|
|
68
|
+
* | vendor / brand | `manufacturers` by name, then `manufacturer_id` |
|
|
69
|
+
* | each real option variant | `product_variants` (uuid from the variant id) |
|
|
70
|
+
* | variant SKU | `product_variants.sku`, unique per product |
|
|
71
|
+
* | variant price / compare-at | `product_variants.price` / `compare_at_price` |
|
|
72
|
+
* | variant stock, when exposed | `product_variants.inventory_count` |
|
|
73
|
+
*
|
|
74
|
+
* Variant money is integer minor units, like `products.price`. A NULL variant
|
|
75
|
+
* `price` means "inherits the product price"; a NULL `inventory_count` means
|
|
76
|
+
* stock is not tracked. `options` holds the option values as the JSON string
|
|
77
|
+
* array the dashboard reads. A variant's `description` is never written: the
|
|
78
|
+
* source has no per-variant description, so the column is the merchant's.
|
|
79
|
+
*
|
|
80
|
+
* Re-running updates the rows a previous run wrote (matched on `uuid`) and
|
|
81
|
+
* leaves alone the columns an operator owns: `preparation_time`, `allergens`,
|
|
82
|
+
* `nutritional_info`, a variant's `description`, and a product's or variant's
|
|
83
|
+
* `inventory_count` or a variant's `sku` when the source does not report one.
|
|
84
|
+
*
|
|
85
|
+
* The source wins a SKU. Before a product's variants are updated, the SKUs it
|
|
86
|
+
* is about to write are released from whichever of that product's variants
|
|
87
|
+
* holds them (see `releaseVariantSkus`), so a SKU that moved between variants
|
|
88
|
+
* at the source, or still sits on a variant the source has since deleted,
|
|
89
|
+
* cannot trip the per-product unique index halfway through the product.
|
|
90
|
+
*/
|
|
91
|
+
export declare interface ProductRow {
|
|
92
|
+
uuid: string
|
|
93
|
+
name: string
|
|
94
|
+
description: string | null
|
|
95
|
+
price: number
|
|
96
|
+
image_url: string | null
|
|
97
|
+
is_available: boolean
|
|
98
|
+
inventory_count?: number
|
|
99
|
+
preparation_time?: number
|
|
100
|
+
category_id?: number | null
|
|
101
|
+
manufacturer_id?: number | null
|
|
102
|
+
created_at?: string
|
|
103
|
+
updated_at: string
|
|
104
|
+
}
|
|
105
|
+
export declare interface VariantRow {
|
|
106
|
+
uuid: string
|
|
107
|
+
product_id: number
|
|
108
|
+
variant: string
|
|
109
|
+
type: string
|
|
110
|
+
options: string
|
|
111
|
+
status: 'active' | 'inactive'
|
|
112
|
+
sku?: string | null
|
|
113
|
+
price: number | null
|
|
114
|
+
compare_at_price: number | null
|
|
115
|
+
inventory_count?: number | null
|
|
116
|
+
created_at?: string
|
|
117
|
+
updated_at: string
|
|
118
|
+
}
|
|
119
|
+
export declare interface CategoryRow {
|
|
120
|
+
uuid: string
|
|
121
|
+
name: string
|
|
122
|
+
slug: string
|
|
123
|
+
description: null
|
|
124
|
+
image_url: null
|
|
125
|
+
is_active: boolean
|
|
126
|
+
parent_category_id: null
|
|
127
|
+
display_order: number
|
|
128
|
+
created_at: string
|
|
129
|
+
updated_at: string
|
|
130
|
+
}
|
|
131
|
+
export declare interface ManufacturerRow {
|
|
132
|
+
uuid: string
|
|
133
|
+
manufacturer: string
|
|
134
|
+
description: null
|
|
135
|
+
country: string
|
|
136
|
+
featured: boolean
|
|
137
|
+
created_at: string
|
|
138
|
+
updated_at: string
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Where the importer reads and writes. The database implementation is below;
|
|
142
|
+
* tests and `--dry-run` use the in-memory and read-only ones.
|
|
143
|
+
*/
|
|
144
|
+
export declare interface CatalogRepository {
|
|
145
|
+
findCategory: (slug: string, name: string) => Promise<number | undefined>
|
|
146
|
+
createCategory: (row: CategoryRow) => Promise<number>
|
|
147
|
+
findManufacturer: (name: string) => Promise<number | undefined>
|
|
148
|
+
createManufacturer: (row: ManufacturerRow) => Promise<number>
|
|
149
|
+
findProducts: (uuids: string[]) => Promise<Map<string, number>>
|
|
150
|
+
createProduct: (row: ProductRow) => Promise<number>
|
|
151
|
+
updateProduct: (id: number, row: Omit<ProductRow, 'uuid'>) => Promise<void>
|
|
152
|
+
findVariants: (uuids: string[]) => Promise<Map<string, number>>
|
|
153
|
+
createVariant: (row: VariantRow) => Promise<number>
|
|
154
|
+
updateVariant: (id: number, row: Omit<VariantRow, 'uuid'>) => Promise<void>
|
|
155
|
+
releaseVariantSkus: (productId: number, skus: string[]) => Promise<void>
|
|
156
|
+
transaction?: <T>(work: (repository: CatalogRepository) => Promise<T>) => Promise<T>
|
|
157
|
+
}
|
|
158
|
+
export declare interface ProductRefs {
|
|
159
|
+
categoryId: number | null
|
|
160
|
+
manufacturerId: number | null
|
|
161
|
+
}
|
|
162
|
+
export declare interface ImportedProduct {
|
|
163
|
+
action: ImportAction
|
|
164
|
+
externalId: string
|
|
165
|
+
name: string
|
|
166
|
+
handle: string
|
|
167
|
+
priceMinor: number | null
|
|
168
|
+
currency: string | null
|
|
169
|
+
variants: { create: number, update: number }
|
|
170
|
+
category: string | null
|
|
171
|
+
vendor: string | null
|
|
172
|
+
imageCount: number
|
|
173
|
+
}
|
|
174
|
+
export declare interface ImportCounts {
|
|
175
|
+
created: number
|
|
176
|
+
updated: number
|
|
177
|
+
}
|
|
178
|
+
export declare interface ImportResult {
|
|
179
|
+
source: string
|
|
180
|
+
storeUrl: string
|
|
181
|
+
dryRun: boolean
|
|
182
|
+
pages: string[]
|
|
183
|
+
products: ImportedProduct[]
|
|
184
|
+
counts: {
|
|
185
|
+
products: ImportCounts
|
|
186
|
+
variants: ImportCounts
|
|
187
|
+
categories: ImportCounts
|
|
188
|
+
manufacturers: ImportCounts
|
|
189
|
+
duplicates: number
|
|
190
|
+
}
|
|
191
|
+
currencies: string[]
|
|
192
|
+
warnings: CatalogWarning[]
|
|
193
|
+
}
|
|
194
|
+
export declare interface ImportCatalogOptions extends CatalogFetchOptions {
|
|
195
|
+
adapter: CatalogAdapter
|
|
196
|
+
storeUrl: string
|
|
197
|
+
repository: CatalogRepository
|
|
198
|
+
dryRun?: boolean
|
|
199
|
+
onProduct?: (product: ImportedProduct) => void
|
|
200
|
+
now?: () => Date
|
|
201
|
+
}
|
|
202
|
+
export type ImportAction = 'create' | 'update';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as auctions from './auctions/index';
|
|
2
2
|
import * as carts from './carts/index';
|
|
3
|
+
import * as catalogImport from './imports/index';
|
|
3
4
|
import * as coupons from './coupons/index';
|
|
4
5
|
import * as customers from './customers/index';
|
|
5
6
|
import * as devices from './devices/index';
|
|
6
7
|
import * as errors from './errors/index';
|
|
7
8
|
import * as giftCards from './gift-cards/index';
|
|
9
|
+
import * as money from './money';
|
|
8
10
|
import * as orders from './orders/index';
|
|
9
11
|
import * as payments from './payments/index';
|
|
10
12
|
import * as products from './products/index';
|
|
@@ -13,6 +15,7 @@ import * as restaurant from './waitlists/restaurant/index';
|
|
|
13
15
|
import * as shippings from './shippings/index';
|
|
14
16
|
import * as tax from './tax/index';
|
|
15
17
|
import * as waitlists from './waitlists/index';
|
|
18
|
+
export type { MoneyInputOptions } from './money';
|
|
16
19
|
export declare const commerce: CommerceNamespace;
|
|
17
20
|
export declare interface CommerceNamespace {
|
|
18
21
|
auctions: AuctionsModule
|
|
@@ -46,14 +49,18 @@ declare type TaxModule = typeof tax;
|
|
|
46
49
|
declare type WaitlistsModule = typeof waitlists;
|
|
47
50
|
declare type DevicesModule = typeof devices;
|
|
48
51
|
declare type ReceiptsModule = typeof receipts;
|
|
52
|
+
// Catalog import (Shopify, WooCommerce) is a tool rather than a store module,
|
|
53
|
+
// so it is exported beside the namespace rather than inside it.
|
|
49
54
|
export {
|
|
50
55
|
auctions,
|
|
51
56
|
carts,
|
|
57
|
+
catalogImport,
|
|
52
58
|
coupons,
|
|
53
59
|
customers,
|
|
54
60
|
devices,
|
|
55
61
|
errors,
|
|
56
62
|
giftCards,
|
|
63
|
+
money,
|
|
57
64
|
orders,
|
|
58
65
|
payments,
|
|
59
66
|
products,
|
|
@@ -63,4 +70,20 @@ export {
|
|
|
63
70
|
tax,
|
|
64
71
|
waitlists,
|
|
65
72
|
};
|
|
73
|
+
// Money conversion at the display and input boundary: stored amounts are
|
|
74
|
+
// integer minor units everywhere (stacksjs/stacks#2851). Browser code imports
|
|
75
|
+
// the same functions from `@stacksjs/commerce/money`, which pulls in nothing else.
|
|
76
|
+
export {
|
|
77
|
+
currencyExponent,
|
|
78
|
+
decimalToMinor,
|
|
79
|
+
formatCurrency,
|
|
80
|
+
formatMinor,
|
|
81
|
+
minorToDecimal,
|
|
82
|
+
minorToInput,
|
|
83
|
+
minorToMajor,
|
|
84
|
+
moneyInputError,
|
|
85
|
+
moneyInputStep,
|
|
86
|
+
parseMoneyInput,
|
|
87
|
+
PriceFormatError,
|
|
88
|
+
} from './money';
|
|
66
89
|
export default commerce;
|