@porulle/import-woocommerce 0.1.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.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @porulle/import-woocommerce
2
+
3
+ Import a WooCommerce store's catalog into Porulle. Maps WooCommerce's product / variation / attribute / image model onto Porulle's catalog.
4
+
5
+ ## Usage
6
+
7
+ Export your WooCommerce catalog as JSON (via the WooCommerce REST API or the WP-CLI):
8
+
9
+ ```bash
10
+ wp wc product list --user=admin --format=json > woo-catalog.json
11
+ porulle import ./woo-catalog.json --format woocommerce
12
+ ```
13
+
14
+ Or programmatically:
15
+
16
+ ```ts
17
+ import { importWooProducts } from "@porulle/import-woocommerce";
18
+ import { commerce } from "./server";
19
+
20
+ const products = JSON.parse(await fs.readFile("./woo-catalog.json", "utf-8"));
21
+ const result = await importWooProducts(commerce.api, products);
22
+ if (!result.ok) {
23
+ console.error("import failed:", result.error);
24
+ process.exit(1);
25
+ }
26
+ ```
27
+
28
+ ## What it maps
29
+
30
+ | WooCommerce | Porulle |
31
+ |---|---|
32
+ | `product` (simple, variable) | `sellable_entities` (type: `product`) |
33
+ | `variation` | `variants` |
34
+ | `attribute` (pa_size, pa_color, …) | `option_types` + `option_values` |
35
+ | `images[]` | `media_assets` + `entity_media` |
36
+ | `categories[]` | `entity_categories` (creates `categories` rows on first sight) |
37
+ | `tags[]` | `metadata.tags` |
38
+ | `meta_data[]` | `metadata.<key>` |
39
+
40
+ ## What it doesn't map
41
+
42
+ - Customers, orders, coupons — same reasoning as the Shopify importer
43
+ - Tax rates — Porulle's tax adapter handles this; configure separately
44
+ - Shipping zones — set up via `config.shipping`
45
+ - WP users / themes / plugins — out of scope
46
+
47
+ ## See also
48
+
49
+ - `@porulle/import-flat` — neutral JSON format
50
+ - `@porulle/import-shopify` — for Shopify origins
51
+ - [WooCommerce REST API — Products](https://woocommerce.github.io/woocommerce-rest-api-docs/#products)
@@ -0,0 +1,160 @@
1
+ import { type Result } from "@porulle/core";
2
+ export interface WooImage {
3
+ id: number;
4
+ src: string;
5
+ alt?: string;
6
+ }
7
+ export interface WooAttribute {
8
+ id?: number;
9
+ name: string;
10
+ variation?: boolean;
11
+ options?: string[];
12
+ option?: string;
13
+ }
14
+ export interface WooProductVariation {
15
+ id: number;
16
+ sku?: string;
17
+ price?: string;
18
+ attributes?: WooAttribute[];
19
+ }
20
+ export interface WooProduct {
21
+ id: number;
22
+ name: string;
23
+ slug?: string;
24
+ description?: string;
25
+ short_description?: string;
26
+ type?: string;
27
+ sku?: string;
28
+ categories?: Array<{
29
+ id: number;
30
+ name: string;
31
+ slug?: string;
32
+ }>;
33
+ attributes?: WooAttribute[];
34
+ images?: WooImage[];
35
+ variationsData?: WooProductVariation[];
36
+ }
37
+ export interface WooCustomer {
38
+ id: number;
39
+ email?: string;
40
+ first_name?: string;
41
+ last_name?: string;
42
+ billing?: {
43
+ phone?: string;
44
+ address_1?: string;
45
+ address_2?: string;
46
+ city?: string;
47
+ state?: string;
48
+ postcode?: string;
49
+ country?: string;
50
+ };
51
+ shipping?: {
52
+ address_1?: string;
53
+ address_2?: string;
54
+ city?: string;
55
+ state?: string;
56
+ postcode?: string;
57
+ country?: string;
58
+ };
59
+ }
60
+ export interface WooImportTarget {
61
+ createEntity(input: {
62
+ type: string;
63
+ slug: string;
64
+ attributes: {
65
+ title: string;
66
+ description?: string;
67
+ subtitle?: string;
68
+ };
69
+ metadata?: Record<string, unknown>;
70
+ }): Promise<{
71
+ id: string;
72
+ }>;
73
+ createOptionType?(input: {
74
+ entityId: string;
75
+ name: string;
76
+ displayName: string;
77
+ sortOrder?: number;
78
+ }): Promise<{
79
+ id: string;
80
+ }>;
81
+ createOptionValue?(input: {
82
+ optionTypeId: string;
83
+ value: string;
84
+ displayValue: string;
85
+ sortOrder?: number;
86
+ }): Promise<{
87
+ id: string;
88
+ }>;
89
+ createVariant?(input: {
90
+ entityId: string;
91
+ optionValueIds: string[];
92
+ sku?: string;
93
+ metadata?: Record<string, unknown>;
94
+ }): Promise<{
95
+ id: string;
96
+ }>;
97
+ uploadMedia?(input: {
98
+ filename: string;
99
+ contentType: string;
100
+ data: ArrayBuffer;
101
+ alt?: string;
102
+ metadata?: Record<string, unknown>;
103
+ }): Promise<{
104
+ id: string;
105
+ url: string;
106
+ }>;
107
+ attachMedia?(input: {
108
+ entityId: string;
109
+ mediaAssetId: string;
110
+ role: "primary" | "gallery";
111
+ }): Promise<void>;
112
+ upsertCustomer?(input: {
113
+ userId: string;
114
+ email?: string;
115
+ phone?: string;
116
+ firstName?: string;
117
+ lastName?: string;
118
+ addresses?: Array<{
119
+ type: "shipping" | "billing";
120
+ isDefault: boolean;
121
+ firstName: string;
122
+ lastName: string;
123
+ line1: string;
124
+ line2?: string;
125
+ city: string;
126
+ state?: string;
127
+ postalCode?: string;
128
+ country: string;
129
+ phone?: string;
130
+ }>;
131
+ metadata?: Record<string, unknown>;
132
+ }): Promise<void>;
133
+ }
134
+ export interface WooImportOptions {
135
+ target: WooImportTarget;
136
+ storeUrl?: string;
137
+ consumerKey?: string;
138
+ consumerSecret?: string;
139
+ products?: WooProduct[];
140
+ customers?: WooCustomer[];
141
+ fetchImpl?: typeof fetch;
142
+ mediaFetcher?: (url: string) => Promise<{
143
+ data: ArrayBuffer;
144
+ contentType: string;
145
+ filename?: string;
146
+ }>;
147
+ entityType?: string;
148
+ }
149
+ export interface WooImportSummary {
150
+ entitiesImported: number;
151
+ variantsImported: number;
152
+ mediaImported: number;
153
+ customersImported: number;
154
+ errors: Array<{
155
+ scope: "entity" | "variant" | "media" | "customer";
156
+ message: string;
157
+ }>;
158
+ }
159
+ export declare function importWooCommerceCatalog(options: WooImportOptions): Promise<Result<WooImportSummary>>;
160
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,258 @@
1
+ import { Err, Ok } from "@porulle/core";
2
+ function slugify(value) {
3
+ return value
4
+ .toLowerCase()
5
+ .trim()
6
+ .replace(/[^a-z0-9]+/g, "-")
7
+ .replace(/^-+|-+$/g, "") || `product-${crypto.randomUUID().slice(0, 8)}`;
8
+ }
9
+ function buildWooUrl(base, path, key, secret) {
10
+ const url = new URL(path, base.replace(/\/$/, "/"));
11
+ url.searchParams.set("consumer_key", key);
12
+ url.searchParams.set("consumer_secret", secret);
13
+ url.searchParams.set("per_page", "100");
14
+ return url.toString();
15
+ }
16
+ async function fetchJson(fetchImpl, url) {
17
+ try {
18
+ const response = await fetchImpl(url, { headers: { accept: "application/json" } });
19
+ if (!response.ok) {
20
+ return Err({
21
+ code: "WOO_API_FAILED",
22
+ message: `WooCommerce request failed (${response.status}) for ${url}.`,
23
+ });
24
+ }
25
+ return Ok((await response.json()));
26
+ }
27
+ catch (error) {
28
+ return Err({
29
+ code: "WOO_API_FAILED",
30
+ message: error instanceof Error ? error.message : "WooCommerce request failed.",
31
+ });
32
+ }
33
+ }
34
+ async function loadProducts(options) {
35
+ if (options.products)
36
+ return Ok(options.products);
37
+ if (!options.storeUrl || !options.consumerKey || !options.consumerSecret) {
38
+ return Err({
39
+ code: "WOO_INPUT_REQUIRED",
40
+ message: "Provide products or storeUrl + consumerKey + consumerSecret.",
41
+ });
42
+ }
43
+ const fetchImpl = options.fetchImpl ?? fetch;
44
+ return fetchJson(fetchImpl, buildWooUrl(options.storeUrl, "/wp-json/wc/v3/products", options.consumerKey, options.consumerSecret));
45
+ }
46
+ async function loadCustomers(options) {
47
+ if (options.customers)
48
+ return Ok(options.customers);
49
+ if (!options.storeUrl || !options.consumerKey || !options.consumerSecret) {
50
+ return Ok([]);
51
+ }
52
+ const fetchImpl = options.fetchImpl ?? fetch;
53
+ return fetchJson(fetchImpl, buildWooUrl(options.storeUrl, "/wp-json/wc/v3/customers", options.consumerKey, options.consumerSecret));
54
+ }
55
+ function filenameFromUrl(url) {
56
+ try {
57
+ const parsed = new URL(url);
58
+ return parsed.pathname.split("/").filter(Boolean).pop() ?? `media-${crypto.randomUUID().slice(0, 8)}.bin`;
59
+ }
60
+ catch {
61
+ return `media-${crypto.randomUUID().slice(0, 8)}.bin`;
62
+ }
63
+ }
64
+ async function defaultMediaFetcher(fetchImpl, url) {
65
+ const response = await fetchImpl(url);
66
+ if (!response.ok) {
67
+ throw new Error(`Failed to fetch media (${response.status}) from ${url}`);
68
+ }
69
+ return {
70
+ data: await response.arrayBuffer(),
71
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
72
+ filename: filenameFromUrl(url),
73
+ };
74
+ }
75
+ export async function importWooCommerceCatalog(options) {
76
+ const products = await loadProducts(options);
77
+ if (!products.ok)
78
+ return products;
79
+ const customers = await loadCustomers(options);
80
+ if (!customers.ok)
81
+ return customers;
82
+ const summary = {
83
+ entitiesImported: 0,
84
+ variantsImported: 0,
85
+ mediaImported: 0,
86
+ customersImported: 0,
87
+ errors: [],
88
+ };
89
+ const mediaFetcher = options.mediaFetcher
90
+ ? options.mediaFetcher
91
+ : async (url) => defaultMediaFetcher(options.fetchImpl ?? fetch, url);
92
+ for (const product of products.value) {
93
+ try {
94
+ const createdEntity = await options.target.createEntity({
95
+ type: options.entityType ?? "product",
96
+ slug: product.slug ? slugify(product.slug) : slugify(product.name),
97
+ attributes: {
98
+ title: product.name,
99
+ ...(product.description ? { description: product.description } : {}),
100
+ ...(product.short_description ? { subtitle: product.short_description } : {}),
101
+ },
102
+ metadata: {
103
+ source: "woocommerce",
104
+ wooProductId: product.id,
105
+ productType: product.type,
106
+ categories: (product.categories ?? []).map((category) => category.slug ?? category.name),
107
+ },
108
+ });
109
+ summary.entitiesImported += 1;
110
+ const optionTypeIdByName = new Map();
111
+ const optionValueIdByKey = new Map();
112
+ if (options.target.createOptionType && options.target.createOptionValue) {
113
+ const attributes = (product.attributes ?? []).filter((attribute) => attribute.variation);
114
+ for (let attrIndex = 0; attrIndex < attributes.length; attrIndex += 1) {
115
+ const attribute = attributes[attrIndex];
116
+ const createdType = await options.target.createOptionType({
117
+ entityId: createdEntity.id,
118
+ name: attribute.name,
119
+ displayName: attribute.name,
120
+ sortOrder: attrIndex,
121
+ });
122
+ optionTypeIdByName.set(attribute.name, createdType.id);
123
+ for (let valueIndex = 0; valueIndex < (attribute.options ?? []).length; valueIndex += 1) {
124
+ const value = attribute.options[valueIndex];
125
+ const createdValue = await options.target.createOptionValue({
126
+ optionTypeId: createdType.id,
127
+ value,
128
+ displayValue: value,
129
+ sortOrder: valueIndex,
130
+ });
131
+ optionValueIdByKey.set(`${attribute.name}::${value}`, createdValue.id);
132
+ }
133
+ }
134
+ }
135
+ if (options.target.createVariant) {
136
+ const variationData = product.variationsData ?? [];
137
+ for (const variant of variationData) {
138
+ try {
139
+ const optionValueIds = (variant.attributes ?? [])
140
+ .map((attribute) => optionValueIdByKey.get(`${attribute.name}::${attribute.option ?? ""}`))
141
+ .filter((value) => typeof value === "string");
142
+ await options.target.createVariant({
143
+ entityId: createdEntity.id,
144
+ optionValueIds,
145
+ ...(variant.sku ? { sku: variant.sku } : {}),
146
+ metadata: {
147
+ source: "woocommerce",
148
+ wooVariationId: variant.id,
149
+ price: variant.price,
150
+ },
151
+ });
152
+ summary.variantsImported += 1;
153
+ }
154
+ catch (error) {
155
+ summary.errors.push({
156
+ scope: "variant",
157
+ message: error instanceof Error ? error.message : "Variation import failed.",
158
+ });
159
+ }
160
+ }
161
+ }
162
+ if (options.target.uploadMedia && options.target.attachMedia) {
163
+ for (let imageIndex = 0; imageIndex < (product.images ?? []).length; imageIndex += 1) {
164
+ const image = product.images[imageIndex];
165
+ try {
166
+ const downloaded = await mediaFetcher(image.src);
167
+ const uploaded = await options.target.uploadMedia({
168
+ filename: downloaded.filename ?? filenameFromUrl(image.src),
169
+ contentType: downloaded.contentType,
170
+ data: downloaded.data,
171
+ ...(image.alt ? { alt: image.alt } : {}),
172
+ metadata: {
173
+ source: "woocommerce",
174
+ wooImageId: image.id,
175
+ },
176
+ });
177
+ await options.target.attachMedia({
178
+ entityId: createdEntity.id,
179
+ mediaAssetId: uploaded.id,
180
+ role: imageIndex === 0 ? "primary" : "gallery",
181
+ });
182
+ summary.mediaImported += 1;
183
+ }
184
+ catch (error) {
185
+ summary.errors.push({
186
+ scope: "media",
187
+ message: error instanceof Error ? error.message : "Media import failed.",
188
+ });
189
+ }
190
+ }
191
+ }
192
+ }
193
+ catch (error) {
194
+ summary.errors.push({
195
+ scope: "entity",
196
+ message: error instanceof Error ? error.message : "Entity import failed.",
197
+ });
198
+ }
199
+ }
200
+ if (options.target.upsertCustomer) {
201
+ for (const customer of customers.value) {
202
+ try {
203
+ const billing = customer.billing;
204
+ const shipping = customer.shipping;
205
+ const addresses = [];
206
+ if (billing?.address_1) {
207
+ addresses.push({
208
+ type: "billing",
209
+ isDefault: true,
210
+ firstName: customer.first_name ?? "",
211
+ lastName: customer.last_name ?? "",
212
+ line1: billing.address_1,
213
+ ...(billing.address_2 ? { line2: billing.address_2 } : {}),
214
+ city: billing.city ?? "",
215
+ ...(billing.state ? { state: billing.state } : {}),
216
+ ...(billing.postcode ? { postalCode: billing.postcode } : {}),
217
+ country: billing.country ?? "US",
218
+ ...(billing.phone ? { phone: billing.phone } : {}),
219
+ });
220
+ }
221
+ if (shipping?.address_1) {
222
+ addresses.push({
223
+ type: "shipping",
224
+ isDefault: addresses.length === 0,
225
+ firstName: customer.first_name ?? "",
226
+ lastName: customer.last_name ?? "",
227
+ line1: shipping.address_1,
228
+ ...(shipping.address_2 ? { line2: shipping.address_2 } : {}),
229
+ city: shipping.city ?? "",
230
+ ...(shipping.state ? { state: shipping.state } : {}),
231
+ ...(shipping.postcode ? { postalCode: shipping.postcode } : {}),
232
+ country: shipping.country ?? "US",
233
+ });
234
+ }
235
+ await options.target.upsertCustomer({
236
+ userId: `woocommerce:${customer.id}`,
237
+ ...(customer.email ? { email: customer.email } : {}),
238
+ ...(billing?.phone ? { phone: billing.phone } : {}),
239
+ ...(customer.first_name ? { firstName: customer.first_name } : {}),
240
+ ...(customer.last_name ? { lastName: customer.last_name } : {}),
241
+ addresses,
242
+ metadata: {
243
+ source: "woocommerce",
244
+ wooCustomerId: customer.id,
245
+ },
246
+ });
247
+ summary.customersImported += 1;
248
+ }
249
+ catch (error) {
250
+ summary.errors.push({
251
+ scope: "customer",
252
+ message: error instanceof Error ? error.message : "Customer import failed.",
253
+ });
254
+ }
255
+ }
256
+ }
257
+ return Ok(summary);
258
+ }